@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
package/src/config.ts CHANGED
@@ -1158,6 +1158,43 @@ export interface Config extends CoreConfig {
1158
1158
  /** The bot login's single refresher (`src/loginrefresh/`). Optional
1159
1159
  * in the file; off unless the cutover's config turns it on. */
1160
1160
  login_refresh: LoginRefreshConfig;
1161
+ /** The home, the app's own pi a bare `qcr` opens (src/home/launch.ts).
1162
+ * Optional in the file and here, so hand-rolled configs keep compiling;
1163
+ * the parser fills every key. */
1164
+ home?: HomeConfig;
1165
+ }
1166
+
1167
+ /** Which tools the home's model may call: `reviewer` is the plugin's tools,
1168
+ * the home's own `home_*` tools and pi's read tools; `full` is all of pi's. */
1169
+ export type HomeToolSet = "reviewer" | "full";
1170
+
1171
+ /** `[home]`: the home a bare `qcr` opens (src/home/launch.ts reads it). */
1172
+ export interface HomeConfig {
1173
+ /** The chat's model, `provider/id`. The panel's login line counts only
1174
+ * this model's provider as signed in. */
1175
+ model: string;
1176
+ tools: HomeToolSet;
1177
+ }
1178
+
1179
+ export const HOME_DEFAULTS: Readonly<HomeConfig> = {
1180
+ model: "openai-codex/gpt-6.1-sol",
1181
+ tools: "reviewer",
1182
+ };
1183
+
1184
+ /** `[home]`, every key defaulted; a value of the wrong kind is refused. */
1185
+ export function parseHomeSection(raw: unknown): HomeConfig {
1186
+ const t = (raw ?? {}) as Record<string, unknown>;
1187
+ const model = t["model"] ?? HOME_DEFAULTS.model;
1188
+ if (typeof model !== "string" || !/^[^/\s]+\/\S+$/.test(model))
1189
+ throw new Error(
1190
+ `config.toml [home]: model must be "provider/id" (got ${JSON.stringify(model)})`,
1191
+ );
1192
+ const tools = t["tools"] ?? HOME_DEFAULTS.tools;
1193
+ if (tools !== "reviewer" && tools !== "full")
1194
+ throw new Error(
1195
+ `config.toml [home]: tools must be "reviewer" or "full" (got ${JSON.stringify(tools)})`,
1196
+ );
1197
+ return { model, tools };
1161
1198
  }
1162
1199
 
1163
1200
  /**
@@ -1822,6 +1859,7 @@ export function parseConfig(toml: string): Config {
1822
1859
  (raw.login_refresh ?? {}) as Record<string, unknown>,
1823
1860
  Number((req(raw, "daemon") as { round_deadline_min?: number }).round_deadline_min ?? 75),
1824
1861
  ),
1862
+ home: parseHomeSection(raw.home),
1825
1863
  };
1826
1864
  validateReviewSection(cfg);
1827
1865
  foldLegacyOwnerSlackId(raw, cfg.owner);
package/src/daemon.ts CHANGED
@@ -832,6 +832,8 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
832
832
 
833
833
  /** The open PRs the last listing reconcile pass saw (status.json `open_prs`). */
834
834
  let openPrs: OpenPrsSnapshot | undefined;
835
+ /** The last reconcile pass (status.json `poll`): null until one has ended. */
836
+ let lastPoll: { at: string; ok: boolean; error?: string } | null = null;
835
837
  let firstReconcileSeen = false;
836
838
  let firstHealthyReconcileAt: Date | null = null;
837
839
  // Deploy-window drain auto-lift: only the boot's first reconcile pass may
@@ -888,6 +890,7 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
888
890
  const bootFirstPass = !firstReconcileSeen;
889
891
  firstReconcileSeen = true;
890
892
  let completed = false;
893
+ let thrown: unknown = undefined;
891
894
  await guard("reconcile", async () => {
892
895
  const report = await reconcileOnce({
893
896
  cfg,
@@ -914,7 +917,15 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
914
917
  // sweep reads session transcripts and is not needed for that, so it
915
918
  // starts on the next pass to keep the boot pass short.
916
919
  receiptSweep: !bootFirstPass,
920
+ }).catch((err: unknown) => {
921
+ thrown = err;
922
+ throw err;
917
923
  });
924
+ lastPoll = {
925
+ at: report.ran_at,
926
+ ok: report.errors.length === 0,
927
+ ...(report.errors[0] !== undefined ? { error: report.errors[0] } : {}),
928
+ };
918
929
  // The board's "review a PR" list: kept from the last pass that listed.
919
930
  if (report.open_list) openPrs = { listed_at: report.ran_at, prs: report.open_list };
920
931
  log(
@@ -957,6 +968,15 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
957
968
  }
958
969
  completed = true;
959
970
  });
971
+ if (thrown !== undefined)
972
+ lastPoll = {
973
+ at: (deps.now ? deps.now() : new Date()).toISOString(),
974
+ ok: false,
975
+ error: `the reconcile pass failed: ${thrown instanceof Error ? thrown.message : JSON.stringify(thrown)}`,
976
+ };
977
+ // The poll's outcome reaches status.json now, not at the next heartbeat:
978
+ // "working" is judged on it.
979
+ writeStatus();
960
980
  return completed;
961
981
  };
962
982
 
@@ -1122,6 +1142,12 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
1122
1142
  };
1123
1143
  }
1124
1144
  if (openPrs) snapshot.open_prs = openPrs;
1145
+ snapshot.poll = {
1146
+ last_at: lastPoll?.at ?? null,
1147
+ ok: lastPoll?.ok ?? null,
1148
+ ...(lastPoll?.error !== undefined ? { error: lastPoll.error } : {}),
1149
+ interval_s: cfg.daemon.poll_interval_s,
1150
+ };
1125
1151
  if (deps.runHost) snapshot.run_host = deps.runHost;
1126
1152
  writeStatusJson(stateDir, snapshot);
1127
1153
  } catch (err) {
package/src/debuground.ts CHANGED
@@ -123,6 +123,8 @@ export interface DebugRoundSpec {
123
123
  cwd: string;
124
124
  /** The login's owner dir: read-only, borrowed from. */
125
125
  agent_dir: string;
126
+ /** The login's dir when it is not `agent_dir` (the worker spec's). */
127
+ login_dir?: string;
126
128
  /** Where the pi's own agent dir (the blanked login copy) is made. */
127
129
  borrow_dir: string;
128
130
  /** The window the borrowed login must outlast. */
@@ -603,6 +605,7 @@ export class DebugRoundManager {
603
605
  root: spec.root,
604
606
  cwd: spec.cwd,
605
607
  agent_dir: spec.agent_dir,
608
+ ...(spec.login_dir !== undefined ? { login_dir: spec.login_dir } : {}),
606
609
  borrow_dir: borrow,
607
610
  idle_ms: this.idleMs,
608
611
  ...(file !== undefined ? { session_file: file } : {}),
@@ -83,6 +83,7 @@ export function readDebugSpec(file: string): DebugRoundSpec {
83
83
  raw.session !== debugSession(raw.pr) ||
84
84
  typeof raw.command_id !== "string" ||
85
85
  typeof raw.agent_dir !== "string" ||
86
+ (raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
86
87
  typeof raw.borrow_dir !== "string" ||
87
88
  typeof raw.session_dir !== "string" ||
88
89
  typeof raw.model !== "string" ||
@@ -1,12 +1,6 @@
1
- // The scan's leaf runner on the SDK: SetupEngine's `CaseRunner` port
2
- // (src/setup/engine.ts) over the runtime port, the in-process twin of
3
- // `piCaseRunner` (src/setup/caserunner.ts). One case is one session on the
4
- // runtime, with the same pieces the child process gets: the agent file's model,
5
- // tools and body (appended to pi's own system prompt), bash-guard the one
6
- // extension, no skill and no context file, a cwd that is first-party ground and
7
- // never the reviewed checkout, and the allowlisted env. The outcome, the reads
8
- // and the kept transcript are read off the same event stream by the same
9
- // reader (`caseReadout`), so a case answers identically whichever host ran it.
1
+ // The scan driver's SDK leaves: the leaf runner on the SDK (scanleaves.ts
2
+ // `sdkCaseRunner`, the in-process twin of `piCaseRunner`) over a host this
3
+ // module builds per case, on the owner's borrowed login.
10
4
  //
11
5
  // WHERE IT RUNS. Only in scancli's detached driver, a disposable one-scan
12
6
  // process (the scan sandbox or the host process `detachDriver` starts), whose
@@ -20,27 +14,10 @@
20
14
  // login (engineCredentials): a token the companion refreshed mid-scan is
21
15
  // picked up at the next case, a token the case would not outlive is refused
22
16
  // before it starts, and nothing is ever refreshed or written here.
17
+ import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { isAbsolute, join } from "node:path";
23
20
  import {
24
- appendFileSync,
25
- copyFileSync,
26
- existsSync,
27
- mkdirSync,
28
- mkdtempSync,
29
- readFileSync,
30
- rmSync,
31
- writeFileSync,
32
- } from "node:fs";
33
- import { homedir, tmpdir } from "node:os";
34
- import { basename, dirname, isAbsolute, join } from "node:path";
35
- import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
36
- import {
37
- bashGuardConfigFromEnv,
38
- createBashGuard,
39
- } from "@quickchatai/pi-quickreview/extensions/bash-guard.js";
40
- import { CASE_STOP_GRACE_MS } from "@quickchatai/pi-quickreview/src/casebudget.js";
41
- import { splitModelSpec } from "../config.js";
42
- import {
43
- AuthError,
44
21
  AuthExpiring,
45
22
  SCAN_AUTH_SRC_ENV,
46
23
  SCAN_CASE_AUTH_BUDGET_MS,
@@ -48,293 +25,30 @@ import {
48
25
  scanAuthExpiring,
49
26
  scanAuthMissing,
50
27
  } from "@quickchatai/pi-quickreview/src/engine/auth.js";
51
- import {
52
- TRANSCRIPT_RUNS_KEPT,
53
- caseReadout,
54
- leafEnv,
55
- parseAgentFile,
56
- pruneTranscripts,
57
- stderrExcerpt,
58
- transcriptFile,
59
- type AgentFile,
60
- } from "../setup/caserunner.js";
61
- import type { CaseOutcome, CaseRunner, CaseSpec } from "../setup/engine.js";
62
- import { LENS_SESSIONS_DIR } from "@quickchatai/pi-quickreview/src/engine/openable.js";
63
28
  import { PiHost } from "@quickchatai/pi-quickreview/src/engine/pisdk.js";
64
- import type {
65
- Runtime,
66
- SessionSpec,
67
- TurnResult,
68
- } from "@quickchatai/pi-quickreview/src/engine/port.js";
69
- import {
70
- sdkRuntime,
71
- type SessionExtras,
72
- } from "@quickchatai/pi-quickreview/src/engine/sdkruntime.js";
29
+ import { sdkRuntime } from "@quickchatai/pi-quickreview/src/engine/sdkruntime.js";
73
30
  import { BUDGET_EXIT_CODE } from "@quickchatai/pi-quickreview/src/engine/budget.js";
74
-
75
- /** A leaf turn's hard wall clock. The login's own case budget
76
- * (`SCAN_CASE_AUTH_BUDGET_MS`) is the usual bound and aborts first; this one
77
- * only backs it where no login budget applies, and is kept past it so the
78
- * budget's last-resort step (it ends the process) never races the abort. */
79
- export const LEAF_TURN_TIMEOUT_MS = SCAN_CASE_AUTH_BUDGET_MS + 15 * 60_000;
80
-
81
- /** What the model's bash may draw from the leaf env, beyond pi's defaults:
82
- * certificates, proxies, XDG dirs and the temp dirs. Never a provider key. */
83
- const LEAF_BASH_ENV_ALLOW: readonly string[] = [
84
- "TMP",
85
- "TEMP",
86
- "XDG_*",
87
- "SSL_CERT_FILE",
88
- "SSL_CERT_DIR",
89
- "NODE_EXTRA_CA_CERTS",
90
- "HTTP_PROXY",
91
- "HTTPS_PROXY",
92
- "NO_PROXY",
93
- "ALL_PROXY",
94
- "http_proxy",
95
- "https_proxy",
96
- "no_proxy",
97
- "all_proxy",
98
- ];
99
-
100
- /** The per-session pieces the runner hands whoever builds the runtime: the
101
- * `extras` of `sdkRuntime` (packages/pi-plugin/src/engine/sdkruntime.ts). */
102
- export type LeafExtras = (spec: SessionSpec) => SessionExtras;
103
-
104
- export interface SdkCaseRunnerOptions {
105
- /** The scan agent build: `<agent>.md` files. */
106
- agentsDir: string;
107
- /** The leaf's working directory: first-party ground, never the reviewed
108
- * checkout (whose own pi config must not load). Also what a relative read is
109
- * resolved against. */
110
- cwd: string;
111
- /** Where each case's event stream is kept; absent, none is. */
112
- transcriptDir?: string;
113
- /** The env the leaf's env is filtered from. Omitted: process.env. */
114
- env?: NodeJS.ProcessEnv;
115
- /** The model of an agent that names none. */
116
- defaultModel?: string;
117
- /** The runtime this case runs on, built with the case's `extras`. Called once
118
- * per case: a route that must re-read its login per case builds its host
119
- * here. */
120
- runtime: (extras: LeafExtras) => Runtime | Promise<Runtime>;
121
- /** The guard extension. Omitted: bash-guard over the leaf env. */
122
- guard?: ExtensionFactory;
123
- /** Wall clock for a case's turn. Omitted: `LEAF_TURN_TIMEOUT_MS`. */
124
- timeoutMs?: number;
125
- }
126
-
127
- /** A transcript's path under `dir`, relative and absolute, with its run
128
- * directory made (and the old runs pruned when it is new); null when it cannot
129
- * be made. A transcript is evidence, never a reason to fail a case. */
130
- function openTranscript(dir: string, id: string): { rel: string; at: string } | null {
131
- try {
132
- const rel = transcriptFile(id);
133
- const at = join(dir, rel);
134
- const runDir = rel.includes("/") ? rel.slice(0, rel.indexOf("/")) : null;
135
- const fresh = runDir !== null && !existsSync(join(dir, runDir));
136
- mkdirSync(dirname(at), { recursive: true });
137
- if (fresh) pruneTranscripts(dir, TRANSCRIPT_RUNS_KEPT, runDir);
138
- writeFileSync(at, "", { mode: 0o600 });
139
- return { rel, at };
140
- } catch {
141
- return null;
142
- }
143
- }
144
-
145
- /** Where a case's session JSONL goes: `<run>/lens-sessions/<case>/` beside the
146
- * transcript `rel` names (`<run>/<case>.jsonl`). */
147
- export function caseSessionDir(transcriptDir: string, rel: string): string {
148
- return join(transcriptDir, dirname(rel), LENS_SESSIONS_DIR, basename(rel, ".jsonl"));
149
- }
150
-
151
- /**
152
- * Runs a case as one SDK session, the way `piCaseRunner` runs it as one child:
153
- * the agent's model (its `:thinking` suffix as the session's thinking level),
154
- * its tools, its body appended to pi's system prompt, the task as the one
155
- * message. The answer is the last assistant message's text; the cost is pi's
156
- * own per-message accounting summed (null when it reported none); `reads` is the
157
- * `read`/`grep` calls that finished without an error, resolved against `cwd`.
158
- * With `transcriptDir` the session's whole event stream is kept at
159
- * `transcriptFile(spec.id)` under it, the format `pi --mode json` prints, and
160
- * its session JSONL beside it (`caseSessionDir`). An
161
- * abort stops the session (abort, then dispose after the stop grace). Every
162
- * outcome resolves — a runtime that cannot be built or a session that cannot
163
- * start is a failed case, never a rejection.
164
- */
165
- export function sdkCaseRunner(opts: SdkCaseRunnerOptions): CaseRunner {
166
- return {
167
- run: async (
168
- spec: CaseSpec,
169
- signal: AbortSignal,
170
- onCost?: (spentSoFar: number) => void,
171
- ): Promise<CaseOutcome> => {
172
- if (!/^[a-z][a-z0-9-]*$/.test(spec.agent))
173
- return {
174
- ok: false,
175
- reason: `not an agent name: ${JSON.stringify(spec.agent)}`,
176
- cost_usd: 0,
177
- };
178
- let agent: AgentFile | null;
179
- try {
180
- agent = parseAgentFile(readFileSync(join(opts.agentsDir, `${spec.agent}.md`), "utf8"));
181
- } catch {
182
- return { ok: false, reason: `no agent ${spec.agent} in ${opts.agentsDir}`, cost_usd: 0 };
183
- }
184
- if (agent === null || agent.name !== spec.agent)
185
- return { ok: false, reason: `${spec.agent}.md is not an agent file`, cost_usd: 0 };
186
- const modelSpec = agent.model ?? opts.defaultModel;
187
- if (!modelSpec)
188
- return { ok: false, reason: `${spec.agent} names no model and none is set`, cost_usd: 0 };
189
- if (signal.aborted) return { ok: false, reason: "stopped before it started", cost_usd: 0 };
190
-
191
- let tmp: string;
192
- try {
193
- tmp = mkdtempSync(join(tmpdir(), "qcr-case-"));
194
- } catch (err) {
195
- return { ok: false, reason: `no temp directory for the case: ${String(err)}`, cost_usd: 0 };
196
- }
197
- let session: Awaited<ReturnType<Runtime["create"]>> | undefined;
198
- try {
199
- const env = leafEnv(opts.env);
200
- const { model, thinking } = splitModelSpec(modelSpec);
201
- const guard = opts.guard ?? createBashGuard(bashGuardConfigFromEnv(env, opts.cwd));
202
- const extras: LeafExtras = () => ({
203
- extensions: [guard],
204
- appendSystemPrompt: [agent.systemPrompt],
205
- bashEnv: { allow: LEAF_BASH_ENV_ALLOW },
206
- ...(thinking !== null
207
- ? { thinkingLevel: thinking as NonNullable<SessionExtras["thinkingLevel"]> }
208
- : {}),
209
- });
210
- // The transcript: opened before the session, appended event by event,
211
- // abandoned (and its name dropped from the outcome) at the first write
212
- // that fails.
213
- const opened =
214
- opts.transcriptDir !== undefined ? openTranscript(opts.transcriptDir, spec.id) : null;
215
- const sessionSpec: SessionSpec = {
216
- cwd: opts.cwd,
217
- env,
218
- model,
219
- ...(agent.tools.length > 0 ? { tools: agent.tools } : {}),
220
- name: spec.id,
221
- // The case's session JSONL beside its transcript, so the case
222
- // opens in pi: `qcr open <run dir> --lens <case>`.
223
- ...(opts.transcriptDir !== undefined && opened !== null
224
- ? { sessionDir: caseSessionDir(opts.transcriptDir, opened.rel) }
225
- : {}),
226
- eventsFile: join(tmp, "events.jsonl"),
227
- stderrFile: join(tmp, "stderr.txt"),
228
- limits: { timeoutMs: opts.timeoutMs ?? LEAF_TURN_TIMEOUT_MS },
229
- };
230
-
231
- let transcript: string | undefined = opened?.rel;
232
- let transcriptAt: string | null = opened?.at ?? null;
233
- const keep = (event: unknown): void => {
234
- if (transcriptAt === null) return;
235
- try {
236
- appendFileSync(transcriptAt, JSON.stringify(event) + "\n");
237
- } catch {
238
- transcriptAt = null;
239
- transcript = undefined;
240
- }
241
- };
242
- const readout = caseReadout(opts.cwd, onCost);
243
- const finish = <T extends CaseOutcome>(o: T): T =>
244
- transcript === undefined ? o : { ...o, transcript };
245
-
246
- try {
247
- const runtime = await opts.runtime(extras);
248
- session = await runtime.create(sessionSpec);
249
- } catch (err) {
250
- const reason = err instanceof Error ? err.message : String(err);
251
- // A refused login already says what to do; anything else is the host's.
252
- return {
253
- ok: false,
254
- reason: err instanceof AuthError ? reason : `could not start the session: ${reason}`,
255
- cost_usd: 0,
256
- };
257
- }
258
- session.subscribe((event) => {
259
- keep(event);
260
- readout.feed(event);
261
- });
262
-
263
- let stopped = false;
264
- let hardStop: ((o: CaseOutcome) => void) | undefined;
265
- const gone = new Promise<CaseOutcome>((resolve) => {
266
- hardStop = resolve;
267
- });
268
- const onAbort = (): void => {
269
- stopped = true;
270
- const live = session;
271
- void live?.abort().catch(() => {});
272
- // The stop grace, then the session is released; a turn still not
273
- // settled a grace later is given up on, so the case resolves.
274
- setTimeout(() => void live?.dispose().catch(() => {}), CASE_STOP_GRACE_MS).unref();
275
- setTimeout(
276
- () => hardStop?.({ ok: false, reason: "stopped", cost_usd: readout.spent() }),
277
- 2 * CASE_STOP_GRACE_MS,
278
- ).unref();
279
- };
280
- signal.addEventListener("abort", onAbort, { once: true });
281
- if (signal.aborted) {
282
- onAbort();
283
- // Cancelled while the session was being created: no turn starts.
284
- return finish({ ok: false, reason: "stopped", cost_usd: 0 });
285
- }
286
-
287
- const turn = (async (): Promise<CaseOutcome> => {
288
- let result: TurnResult;
289
- try {
290
- result = await session.prompt(`Task: ${spec.prompt}`);
291
- } catch (err) {
292
- return {
293
- ok: false,
294
- reason: `the session failed: ${err instanceof Error ? err.message : String(err)}`,
295
- cost_usd: readout.spent(),
296
- };
297
- }
298
- // What a case that ended without an answer had spent: a failure costs
299
- // money too, and a caller holding a budget must count it.
300
- const cost_usd = readout.spent();
301
- if (stopped) return { ok: false, reason: "stopped", cost_usd };
302
- if (result.timedOut)
303
- return { ok: false, reason: "the case ran past its wall clock", cost_usd };
304
- // The model's own reason leads: a provider error also ends the turn non-zero.
305
- if (readout.failure !== null) return { ok: false, reason: readout.failure, cost_usd };
306
- if (result.code !== 0) {
307
- let tail = "";
308
- try {
309
- tail = stderrExcerpt(readFileSync(sessionSpec.stderrFile, "utf8"));
310
- } catch {
311
- // No stderr to quote.
312
- }
313
- return {
314
- ok: false,
315
- reason: `the session ended without a result${tail ? `: ${tail}` : ""}`,
316
- cost_usd,
317
- };
318
- }
319
- if (readout.final.trim() === "")
320
- return { ok: false, reason: "the subagent gave no answer", cost_usd };
321
- return { ok: true, output: readout.final, cost_usd, reads: readout.reads };
322
- })();
323
- try {
324
- return finish(await Promise.race([turn, gone]));
325
- } finally {
326
- signal.removeEventListener("abort", onAbort);
327
- }
328
- } finally {
329
- try {
330
- await session?.dispose();
331
- } catch {
332
- // Already released.
333
- }
334
- rmSync(tmp, { recursive: true, force: true });
335
- }
336
- },
337
- };
31
+ import type { CaseRunner } from "@quickchatai/pi-quickreview/src/scan/scanports.js";
32
+ import {
33
+ LEAF_TURN_TIMEOUT_MS,
34
+ sdkCaseRunner as sdkCaseRunnerOver,
35
+ type SdkCaseRunnerOptions,
36
+ } from "@quickchatai/pi-quickreview/src/scan/scanleaves.js";
37
+
38
+ export {
39
+ LEAF_TURN_TIMEOUT_MS,
40
+ caseSessionDir,
41
+ type LeafExtras,
42
+ type SdkCaseRunnerOptions,
43
+ } from "@quickchatai/pi-quickreview/src/scan/scanleaves.js";
44
+
45
+ /** `sdkCaseRunner` (scanleaves.ts) in this process: the leaf env filtered
46
+ * from this process's env and a read's `~` taken as this process's home,
47
+ * unless told otherwise. */
48
+ export function sdkCaseRunner(
49
+ opts: Omit<SdkCaseRunnerOptions, "env" | "home"> & { env?: NodeJS.ProcessEnv; home?: string },
50
+ ): CaseRunner {
51
+ return sdkCaseRunnerOver({ ...opts, env: opts.env ?? process.env, home: opts.home ?? homedir() });
338
52
  }
339
53
 
340
54
  // ---- the scan driver's host ----------------------------------------------------
@@ -380,6 +94,7 @@ export function scanSdkCases(o: ScanSdkCasesOptions): CaseRunner {
380
94
  throw new Error(`the SDK host's agentDir must be absolute: ${o.agentDir}`);
381
95
  return sdkCaseRunner({
382
96
  agentsDir: o.agentsDir,
97
+ home: homedir(),
383
98
  cwd: o.cwd,
384
99
  ...(o.transcriptDir !== undefined ? { transcriptDir: o.transcriptDir } : {}),
385
100
  env: o.env,