@quickchatai/quickreview 0.0.0-stage → 1.2.0

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 (1479) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -2
  3. package/THIRD-PARTY-NOTICES.md +134 -0
  4. package/agents/pass-abstraction.md +64 -0
  5. package/agents/pass-ask-human.md +134 -0
  6. package/agents/pass-backcompat.md +67 -0
  7. package/agents/pass-baseline.md +61 -0
  8. package/agents/pass-behaviour-delta.md +64 -0
  9. package/agents/pass-bounds.md +36 -0
  10. package/agents/pass-claims.md +31 -0
  11. package/agents/pass-concurrency.md +168 -0
  12. package/agents/pass-consistency.md +57 -0
  13. package/agents/pass-contracts.md +30 -0
  14. package/agents/pass-data-safety.md +54 -0
  15. package/agents/pass-deferred.md +35 -0
  16. package/agents/pass-delivery.md +37 -0
  17. package/agents/pass-duplication.md +60 -0
  18. package/agents/pass-intent.md +63 -0
  19. package/agents/pass-lint-conformance.md +46 -0
  20. package/agents/pass-maintainability.md +67 -0
  21. package/agents/pass-placement.md +68 -0
  22. package/agents/pass-prod-risk.md +24 -0
  23. package/agents/pass-rollout.md +32 -0
  24. package/agents/pass-roundtrip.md +46 -0
  25. package/agents/pass-security.md +31 -0
  26. package/agents/pass-simplicity.md +68 -0
  27. package/agents/pass-statelessness.md +27 -0
  28. package/agents/pass-style.md +37 -0
  29. package/agents/pass-sweep.md +57 -0
  30. package/agents/pass-tests.md +58 -0
  31. package/agents/pass-ui.md +23 -0
  32. package/agents/pass-validate.md +50 -0
  33. package/agents/pass-widened.md +46 -0
  34. package/agents/scan/pass-scan.md +86 -0
  35. package/bin/qcr.js +11 -0
  36. package/config.example.toml +1099 -0
  37. package/deploy/ansible/README.md +356 -0
  38. package/deploy/ansible/group_vars/all.yml +212 -0
  39. package/deploy/ansible/inventory.example.ini +25 -0
  40. package/deploy/ansible/playbook.yml +360 -0
  41. package/deploy/collect-secrets.sh +289 -0
  42. package/deploy/collect-slack-secrets.mjs +1190 -0
  43. package/deploy/github-app-manifest.json +31 -0
  44. package/deploy/manifest.schema.json +75 -0
  45. package/deploy/qcr-update.sh +803 -0
  46. package/deploy/reviewer/AGENTS.override.md +12 -0
  47. package/dist/extensions/cli/bash-guard-ambient.js +42 -0
  48. package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
  49. package/dist/extensions/cli/bash-guard.js +5 -0
  50. package/dist/extensions/cli/bash-guard.js.map +1 -0
  51. package/dist/extensions/cli/companion-guard.js +5 -0
  52. package/dist/extensions/cli/companion-guard.js.map +1 -0
  53. package/dist/extensions/cli/debug-round.js +8 -0
  54. package/dist/extensions/cli/debug-round.js.map +1 -0
  55. package/dist/extensions/cli/inject-bridge.js +5 -0
  56. package/dist/extensions/cli/inject-bridge.js.map +1 -0
  57. package/dist/extensions/cli/ledger-pin.js +5 -0
  58. package/dist/extensions/cli/ledger-pin.js.map +1 -0
  59. package/dist/extensions/cli/lens.js +13 -0
  60. package/dist/extensions/cli/lens.js.map +1 -0
  61. package/dist/extensions/cli/login-route.js +5 -0
  62. package/dist/extensions/cli/login-route.js.map +1 -0
  63. package/dist/extensions/cli/muted-theme.js +5 -0
  64. package/dist/extensions/cli/muted-theme.js.map +1 -0
  65. package/dist/extensions/cli/onboarding.js +5 -0
  66. package/dist/extensions/cli/onboarding.js.map +1 -0
  67. package/dist/extensions/cli/setup.js +5 -0
  68. package/dist/extensions/cli/setup.js.map +1 -0
  69. package/dist/extensions/cli/tui-view.js +5 -0
  70. package/dist/extensions/cli/tui-view.js.map +1 -0
  71. package/dist/extensions/companion-guard.js +361 -0
  72. package/dist/extensions/companion-guard.js.map +1 -0
  73. package/dist/extensions/debug-round.js +204 -0
  74. package/dist/extensions/debug-round.js.map +1 -0
  75. package/dist/extensions/inject-bridge.js +603 -0
  76. package/dist/extensions/inject-bridge.js.map +1 -0
  77. package/dist/extensions/login-route.js +180 -0
  78. package/dist/extensions/login-route.js.map +1 -0
  79. package/dist/extensions/muted-theme.js +12 -0
  80. package/dist/extensions/muted-theme.js.map +1 -0
  81. package/dist/extensions/onboarding.js +198 -0
  82. package/dist/extensions/onboarding.js.map +1 -0
  83. package/dist/extensions/setup-replay.js +269 -0
  84. package/dist/extensions/setup-replay.js.map +1 -0
  85. package/dist/extensions/setup-scan.js +998 -0
  86. package/dist/extensions/setup-scan.js.map +1 -0
  87. package/dist/extensions/setup-triage.js +175 -0
  88. package/dist/extensions/setup-triage.js.map +1 -0
  89. package/dist/extensions/setup.js +1160 -0
  90. package/dist/extensions/setup.js.map +1 -0
  91. package/dist/extensions/tui-view.js +389 -0
  92. package/dist/extensions/tui-view.js.map +1 -0
  93. package/dist/extensions/vendor/openviking-pi/client.js +376 -0
  94. package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
  95. package/dist/extensions/vendor/openviking-pi/config.js +189 -0
  96. package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
  97. package/dist/extensions/vendor/openviking-pi/index.js +421 -0
  98. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
  99. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
  100. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
  101. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
  102. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
  103. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
  104. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
  105. package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
  106. package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
  107. package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
  108. package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
  109. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
  110. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
  111. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
  112. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
  113. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
  114. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
  115. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
  116. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
  117. package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
  118. package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
  119. package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
  120. package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
  121. package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
  122. package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
  123. package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
  124. package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
  125. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
  126. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
  127. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
  128. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
  129. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
  130. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
  131. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
  132. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
  133. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
  134. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
  135. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
  136. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
  137. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
  138. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
  139. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
  140. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
  141. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
  142. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
  143. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  144. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
  145. package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
  146. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
  147. package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
  148. package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
  149. package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
  150. package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
  151. package/dist/src/anomalies.js +483 -0
  152. package/dist/src/anomalies.js.map +1 -0
  153. package/dist/src/answerworker.js +180 -0
  154. package/dist/src/answerworker.js.map +1 -0
  155. package/dist/src/appstamp.js +68 -0
  156. package/dist/src/appstamp.js.map +1 -0
  157. package/dist/src/archive/admission.js +3 -0
  158. package/dist/src/archive/admission.js.map +1 -0
  159. package/dist/src/archive/archivedb.js +474 -0
  160. package/dist/src/archive/archivedb.js.map +1 -0
  161. package/dist/src/archive/archiver.js +1770 -0
  162. package/dist/src/archive/archiver.js.map +1 -0
  163. package/dist/src/archive/archivewrite.js +198 -0
  164. package/dist/src/archive/archivewrite.js.map +1 -0
  165. package/dist/src/archive/bundle.js +266 -0
  166. package/dist/src/archive/bundle.js.map +1 -0
  167. package/dist/src/archive/dbmove.js +638 -0
  168. package/dist/src/archive/dbmove.js.map +1 -0
  169. package/dist/src/archive/duplicatecounts.js +80 -0
  170. package/dist/src/archive/duplicatecounts.js.map +1 -0
  171. package/dist/src/archive/eligibility.js +113 -0
  172. package/dist/src/archive/eligibility.js.map +1 -0
  173. package/dist/src/archive/extract.js +288 -0
  174. package/dist/src/archive/extract.js.map +1 -0
  175. package/dist/src/archive/fsutil.js +330 -0
  176. package/dist/src/archive/fsutil.js.map +1 -0
  177. package/dist/src/archive/gzfile.js +277 -0
  178. package/dist/src/archive/gzfile.js.map +1 -0
  179. package/dist/src/archive/kinds.js +22 -0
  180. package/dist/src/archive/kinds.js.map +1 -0
  181. package/dist/src/archive/livecheckpoint.js +178 -0
  182. package/dist/src/archive/livecheckpoint.js.map +1 -0
  183. package/dist/src/archive/liveretention.js +145 -0
  184. package/dist/src/archive/liveretention.js.map +1 -0
  185. package/dist/src/archive/manifest.js +284 -0
  186. package/dist/src/archive/manifest.js.map +1 -0
  187. package/dist/src/archive/overseer.js +283 -0
  188. package/dist/src/archive/overseer.js.map +1 -0
  189. package/dist/src/archive/piglobal.js +129 -0
  190. package/dist/src/archive/piglobal.js.map +1 -0
  191. package/dist/src/archive/plan.js +398 -0
  192. package/dist/src/archive/plan.js.map +1 -0
  193. package/dist/src/archive/prarchive.js +831 -0
  194. package/dist/src/archive/prarchive.js.map +1 -0
  195. package/dist/src/archive/prfiles.js +303 -0
  196. package/dist/src/archive/prfiles.js.map +1 -0
  197. package/dist/src/archive/report.js +169 -0
  198. package/dist/src/archive/report.js.map +1 -0
  199. package/dist/src/archive/restore.js +276 -0
  200. package/dist/src/archive/restore.js.map +1 -0
  201. package/dist/src/archive/rows.js +106 -0
  202. package/dist/src/archive/rows.js.map +1 -0
  203. package/dist/src/archive/seal.js +480 -0
  204. package/dist/src/archive/seal.js.map +1 -0
  205. package/dist/src/archive/status.js +528 -0
  206. package/dist/src/archive/status.js.map +1 -0
  207. package/dist/src/archive/storepass.js +409 -0
  208. package/dist/src/archive/storepass.js.map +1 -0
  209. package/dist/src/backfill.js +217 -0
  210. package/dist/src/backfill.js.map +1 -0
  211. package/dist/src/bench/cost.js +87 -0
  212. package/dist/src/bench/exec.js +76 -0
  213. package/dist/src/bench/values.js +14 -0
  214. package/dist/src/botcontrol.js +127 -0
  215. package/dist/src/botcontrol.js.map +1 -0
  216. package/dist/src/botworker.js +805 -0
  217. package/dist/src/botworker.js.map +1 -0
  218. package/dist/src/capture.js +234 -0
  219. package/dist/src/capture.js.map +1 -0
  220. package/dist/src/cases.js +1491 -0
  221. package/dist/src/cases.js.map +1 -0
  222. package/dist/src/channelfs.js +27 -0
  223. package/dist/src/channelfs.js.map +1 -0
  224. package/dist/src/checklist.js +312 -0
  225. package/dist/src/checklist.js.map +1 -0
  226. package/dist/src/ciretry.js +92 -0
  227. package/dist/src/ciretry.js.map +1 -0
  228. package/dist/src/cli.js +1947 -0
  229. package/dist/src/cli.js.map +1 -0
  230. package/dist/src/cliops.js +334 -0
  231. package/dist/src/cliops.js.map +1 -0
  232. package/dist/src/clonedir.js +15 -0
  233. package/dist/src/clonedir.js.map +1 -0
  234. package/dist/src/commandeffects.js +43 -0
  235. package/dist/src/commandeffects.js.map +1 -0
  236. package/dist/src/commands.js +1335 -0
  237. package/dist/src/commands.js.map +1 -0
  238. package/dist/src/companioninbox.js +203 -0
  239. package/dist/src/companioninbox.js.map +1 -0
  240. package/dist/src/compare.js +673 -0
  241. package/dist/src/compare.js.map +1 -0
  242. package/dist/src/config.js +1041 -0
  243. package/dist/src/config.js.map +1 -0
  244. package/dist/src/configcheck.js +111 -0
  245. package/dist/src/configcheck.js.map +1 -0
  246. package/dist/src/coreext.js +43 -0
  247. package/dist/src/coreext.js.map +1 -0
  248. package/dist/src/daemon.js +945 -0
  249. package/dist/src/daemon.js.map +1 -0
  250. package/dist/src/debuground.js +731 -0
  251. package/dist/src/debuground.js.map +1 -0
  252. package/dist/src/debugroundpi.js +223 -0
  253. package/dist/src/debugroundpi.js.map +1 -0
  254. package/dist/src/diagnostics.js +14 -0
  255. package/dist/src/diagnostics.js.map +1 -0
  256. package/dist/src/digest.js +139 -0
  257. package/dist/src/digest.js.map +1 -0
  258. package/dist/src/dispatch.js +1727 -0
  259. package/dist/src/dispatch.js.map +1 -0
  260. package/dist/src/engine/answerguard.js +18 -0
  261. package/dist/src/engine/answerguard.js.map +1 -0
  262. package/dist/src/engine/botsdk.js +262 -0
  263. package/dist/src/engine/botsdk.js.map +1 -0
  264. package/dist/src/engine/caserunner-sdk.js +332 -0
  265. package/dist/src/engine/caserunner-sdk.js.map +1 -0
  266. package/dist/src/engine/debugpi.js +206 -0
  267. package/dist/src/engine/debugpi.js.map +1 -0
  268. package/dist/src/eod.js +473 -0
  269. package/dist/src/eod.js.map +1 -0
  270. package/dist/src/eodpinwatch.js +391 -0
  271. package/dist/src/eodpinwatch.js.map +1 -0
  272. package/dist/src/escalation.js +1009 -0
  273. package/dist/src/escalation.js.map +1 -0
  274. package/dist/src/eventloop.js +779 -0
  275. package/dist/src/eventloop.js.map +1 -0
  276. package/dist/src/execboundary.js +437 -0
  277. package/dist/src/execboundary.js.map +1 -0
  278. package/dist/src/feedback.js +915 -0
  279. package/dist/src/feedback.js.map +1 -0
  280. package/dist/src/findingscheck.js +200 -0
  281. package/dist/src/findingscheck.js.map +1 -0
  282. package/dist/src/fsdenied.js +26 -0
  283. package/dist/src/fsdenied.js.map +1 -0
  284. package/dist/src/ghread.js +181 -0
  285. package/dist/src/ghread.js.map +1 -0
  286. package/dist/src/github.js +1098 -0
  287. package/dist/src/github.js.map +1 -0
  288. package/dist/src/githubtoken.js +346 -0
  289. package/dist/src/githubtoken.js.map +1 -0
  290. package/dist/src/homes.js +103 -0
  291. package/dist/src/homes.js.map +1 -0
  292. package/dist/src/hostroute.js +24 -0
  293. package/dist/src/hostroute.js.map +1 -0
  294. package/dist/src/improvements.js +625 -0
  295. package/dist/src/improvements.js.map +1 -0
  296. package/dist/src/install.js +946 -0
  297. package/dist/src/install.js.map +1 -0
  298. package/dist/src/installkind.js +40 -0
  299. package/dist/src/installkind.js.map +1 -0
  300. package/dist/src/ipc/capability.js +286 -0
  301. package/dist/src/ipc/capability.js.map +1 -0
  302. package/dist/src/ipc/client.js +226 -0
  303. package/dist/src/ipc/client.js.map +1 -0
  304. package/dist/src/ipc/control.js +70 -0
  305. package/dist/src/ipc/control.js.map +1 -0
  306. package/dist/src/ipc/ghreadshim.js +77 -0
  307. package/dist/src/ipc/ghreadshim.js.map +1 -0
  308. package/dist/src/ipc/launcher.js +65 -0
  309. package/dist/src/ipc/launcher.js.map +1 -0
  310. package/dist/src/ipc/permits.js +293 -0
  311. package/dist/src/ipc/permits.js.map +1 -0
  312. package/dist/src/ipc/postshim.js +137 -0
  313. package/dist/src/ipc/postshim.js.map +1 -0
  314. package/dist/src/ipc/protocol.js +239 -0
  315. package/dist/src/ipc/protocol.js.map +1 -0
  316. package/dist/src/ipc/server.js +312 -0
  317. package/dist/src/ipc/server.js.map +1 -0
  318. package/dist/src/ipc/workerspec.js +148 -0
  319. package/dist/src/ipc/workerspec.js.map +1 -0
  320. package/dist/src/learner/learner.js +1272 -0
  321. package/dist/src/learner/learner.js.map +1 -0
  322. package/dist/src/learner/miner.js +948 -0
  323. package/dist/src/learner/miner.js.map +1 -0
  324. package/dist/src/learner/packet.js +216 -0
  325. package/dist/src/learner/packet.js.map +1 -0
  326. package/dist/src/learner/receipt.js +135 -0
  327. package/dist/src/learner/receipt.js.map +1 -0
  328. package/dist/src/learner/status.js +38 -0
  329. package/dist/src/learner/status.js.map +1 -0
  330. package/dist/src/ledgercheck.js +120 -0
  331. package/dist/src/ledgercheck.js.map +1 -0
  332. package/dist/src/lessonledger.js +451 -0
  333. package/dist/src/lessonledger.js.map +1 -0
  334. package/dist/src/lessonselect.js +639 -0
  335. package/dist/src/lessonselect.js.map +1 -0
  336. package/dist/src/lessonshadow.js +540 -0
  337. package/dist/src/lessonshadow.js.map +1 -0
  338. package/dist/src/levers.js +302 -0
  339. package/dist/src/levers.js.map +1 -0
  340. package/dist/src/live/projection.js +450 -0
  341. package/dist/src/live/projection.js.map +1 -0
  342. package/dist/src/live/publish.js +83 -0
  343. package/dist/src/live/publish.js.map +1 -0
  344. package/dist/src/live/read.js +60 -0
  345. package/dist/src/live/read.js.map +1 -0
  346. package/dist/src/livecli.js +288 -0
  347. package/dist/src/livecli.js.map +1 -0
  348. package/dist/src/loginhint.js +47 -0
  349. package/dist/src/loginhint.js.map +1 -0
  350. package/dist/src/loginrefresh/atomic.js +66 -0
  351. package/dist/src/loginrefresh/atomic.js.map +1 -0
  352. package/dist/src/loginrefresh/lock.js +93 -0
  353. package/dist/src/loginrefresh/lock.js.map +1 -0
  354. package/dist/src/loginrefresh/main.js +58 -0
  355. package/dist/src/loginrefresh/main.js.map +1 -0
  356. package/dist/src/loginrefresh/pirefresh.js +52 -0
  357. package/dist/src/loginrefresh/pirefresh.js.map +1 -0
  358. package/dist/src/loginrefresh/refresher.js +286 -0
  359. package/dist/src/loginrefresh/refresher.js.map +1 -0
  360. package/dist/src/loginrefresh/supervisor.js +253 -0
  361. package/dist/src/loginrefresh/supervisor.js.map +1 -0
  362. package/dist/src/loginrefresh/types.js +4 -0
  363. package/dist/src/loginrefresh/types.js.map +1 -0
  364. package/dist/src/memory/context.js +251 -0
  365. package/dist/src/memory/context.js.map +1 -0
  366. package/dist/src/memory/datafence.js +5 -0
  367. package/dist/src/memory/datafence.js.map +1 -0
  368. package/dist/src/memory/delivery.js +131 -0
  369. package/dist/src/memory/delivery.js.map +1 -0
  370. package/dist/src/memory/eventtags.js +81 -0
  371. package/dist/src/memory/eventtags.js.map +1 -0
  372. package/dist/src/memory/health.js +713 -0
  373. package/dist/src/memory/health.js.map +1 -0
  374. package/dist/src/memory/import.js +504 -0
  375. package/dist/src/memory/import.js.map +1 -0
  376. package/dist/src/memory/layout.js +252 -0
  377. package/dist/src/memory/layout.js.map +1 -0
  378. package/dist/src/memory/legacy-file.js +100 -0
  379. package/dist/src/memory/legacy-file.js.map +1 -0
  380. package/dist/src/memory/lessons.js +370 -0
  381. package/dist/src/memory/lessons.js.map +1 -0
  382. package/dist/src/memory/ovsession.js +449 -0
  383. package/dist/src/memory/ovsession.js.map +1 -0
  384. package/dist/src/memory/relayout.js +259 -0
  385. package/dist/src/memory/relayout.js.map +1 -0
  386. package/dist/src/memory/roundactivity.js +180 -0
  387. package/dist/src/memory/roundactivity.js.map +1 -0
  388. package/dist/src/memory/selection.js +690 -0
  389. package/dist/src/memory/selection.js.map +1 -0
  390. package/dist/src/memory/shadowstatus.js +69 -0
  391. package/dist/src/memory/shadowstatus.js.map +1 -0
  392. package/dist/src/memory/store.js +83 -0
  393. package/dist/src/memory/store.js.map +1 -0
  394. package/dist/src/memory/textidentity.js +8 -0
  395. package/dist/src/memory/textidentity.js.map +1 -0
  396. package/dist/src/metrics.js +773 -0
  397. package/dist/src/metrics.js.map +1 -0
  398. package/dist/src/missingconfig.js +8 -0
  399. package/dist/src/missingconfig.js.map +1 -0
  400. package/dist/src/npmverbs.js +228 -0
  401. package/dist/src/npmverbs.js.map +1 -0
  402. package/dist/src/onboard/choices.js +219 -0
  403. package/dist/src/onboard/choices.js.map +1 -0
  404. package/dist/src/onboard/engine.js +329 -0
  405. package/dist/src/onboard/engine.js.map +1 -0
  406. package/dist/src/onboard/launch.js +75 -0
  407. package/dist/src/onboard/launch.js.map +1 -0
  408. package/dist/src/onboard/pipin.js +22 -0
  409. package/dist/src/onboard/pipin.js.map +1 -0
  410. package/dist/src/onboard/stepforward.js +41 -0
  411. package/dist/src/onboard/stepforward.js.map +1 -0
  412. package/dist/src/onboard/steps/appslug.js +214 -0
  413. package/dist/src/onboard/steps/appslug.js.map +1 -0
  414. package/dist/src/onboard/steps/bringup.js +408 -0
  415. package/dist/src/onboard/steps/bringup.js.map +1 -0
  416. package/dist/src/onboard/steps/compose.js +372 -0
  417. package/dist/src/onboard/steps/compose.js.map +1 -0
  418. package/dist/src/onboard/steps/configdelivery.js +50 -0
  419. package/dist/src/onboard/steps/configdelivery.js.map +1 -0
  420. package/dist/src/onboard/steps/configfill.js +1218 -0
  421. package/dist/src/onboard/steps/configfill.js.map +1 -0
  422. package/dist/src/onboard/steps/deploysh.js +80 -0
  423. package/dist/src/onboard/steps/deploysh.js.map +1 -0
  424. package/dist/src/onboard/steps/embeddings.js +472 -0
  425. package/dist/src/onboard/steps/embeddings.js.map +1 -0
  426. package/dist/src/onboard/steps/escalation.js +571 -0
  427. package/dist/src/onboard/steps/escalation.js.map +1 -0
  428. package/dist/src/onboard/steps/githubaccount.js +595 -0
  429. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  430. package/dist/src/onboard/steps/githubapp.js +1150 -0
  431. package/dist/src/onboard/steps/githubapp.js.map +1 -0
  432. package/dist/src/onboard/steps/golive.js +508 -0
  433. package/dist/src/onboard/steps/golive.js.map +1 -0
  434. package/dist/src/onboard/steps/index.js +78 -0
  435. package/dist/src/onboard/steps/index.js.map +1 -0
  436. package/dist/src/onboard/steps/ingress.js +817 -0
  437. package/dist/src/onboard/steps/ingress.js.map +1 -0
  438. package/dist/src/onboard/steps/inventory.js +420 -0
  439. package/dist/src/onboard/steps/inventory.js.map +1 -0
  440. package/dist/src/onboard/steps/llmauth.js +966 -0
  441. package/dist/src/onboard/steps/llmauth.js.map +1 -0
  442. package/dist/src/onboard/steps/memory.js +1112 -0
  443. package/dist/src/onboard/steps/memory.js.map +1 -0
  444. package/dist/src/onboard/steps/memorykeys.js +43 -0
  445. package/dist/src/onboard/steps/memorykeys.js.map +1 -0
  446. package/dist/src/onboard/steps/preflight.js +172 -0
  447. package/dist/src/onboard/steps/preflight.js.map +1 -0
  448. package/dist/src/onboard/steps/proof.js +812 -0
  449. package/dist/src/onboard/steps/proof.js.map +1 -0
  450. package/dist/src/onboard/steps/ssh.js +300 -0
  451. package/dist/src/onboard/steps/ssh.js.map +1 -0
  452. package/dist/src/onboard/steps/topology.js +291 -0
  453. package/dist/src/onboard/steps/topology.js.map +1 -0
  454. package/dist/src/onboard/steps/transport.js +519 -0
  455. package/dist/src/onboard/steps/transport.js.map +1 -0
  456. package/dist/src/onboard/steps/tuiroute.js +102 -0
  457. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  458. package/dist/src/onboard/steps/verifyup.js +378 -0
  459. package/dist/src/onboard/steps/verifyup.js.map +1 -0
  460. package/dist/src/onboard/steps/vmdeploy.js +368 -0
  461. package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
  462. package/dist/src/onboard/toml.js +53 -0
  463. package/dist/src/onboard/toml.js.map +1 -0
  464. package/dist/src/onboard/toolnames.js +30 -0
  465. package/dist/src/onboard/toolnames.js.map +1 -0
  466. package/dist/src/opencli.js +508 -0
  467. package/dist/src/opencli.js.map +1 -0
  468. package/dist/src/openpr.js +457 -0
  469. package/dist/src/openpr.js.map +1 -0
  470. package/dist/src/openviking/client.js +821 -0
  471. package/dist/src/openviking/client.js.map +1 -0
  472. package/dist/src/openviking/context.js +218 -0
  473. package/dist/src/openviking/context.js.map +1 -0
  474. package/dist/src/openviking/embedder.js +457 -0
  475. package/dist/src/openviking/embedder.js.map +1 -0
  476. package/dist/src/openviking/gapsink.js +38 -0
  477. package/dist/src/openviking/gapsink.js.map +1 -0
  478. package/dist/src/openviking/health.js +69 -0
  479. package/dist/src/openviking/health.js.map +1 -0
  480. package/dist/src/openviking/index.js +24 -0
  481. package/dist/src/openviking/index.js.map +1 -0
  482. package/dist/src/openviking/lessonfind.js +54 -0
  483. package/dist/src/openviking/lessonfind.js.map +1 -0
  484. package/dist/src/openviking/probe.js +89 -0
  485. package/dist/src/openviking/probe.js.map +1 -0
  486. package/dist/src/openviking/proc.js +95 -0
  487. package/dist/src/openviking/proc.js.map +1 -0
  488. package/dist/src/openviking/provisionreport.js +144 -0
  489. package/dist/src/openviking/provisionreport.js.map +1 -0
  490. package/dist/src/openviking/reembed.js +317 -0
  491. package/dist/src/openviking/reembed.js.map +1 -0
  492. package/dist/src/openviking/relayout.js +153 -0
  493. package/dist/src/openviking/relayout.js.map +1 -0
  494. package/dist/src/openviking/sidecar-main.js +39 -0
  495. package/dist/src/openviking/sidecar-main.js.map +1 -0
  496. package/dist/src/openviking/sidecar.js +79 -0
  497. package/dist/src/openviking/sidecar.js.map +1 -0
  498. package/dist/src/openviking/store.js +1758 -0
  499. package/dist/src/openviking/store.js.map +1 -0
  500. package/dist/src/openviking/supervisor.js +422 -0
  501. package/dist/src/openviking/supervisor.js.map +1 -0
  502. package/dist/src/openviking/types.js +5 -0
  503. package/dist/src/openviking/types.js.map +1 -0
  504. package/dist/src/operator-recovery.js +172 -0
  505. package/dist/src/operator-recovery.js.map +1 -0
  506. package/dist/src/operatormessage.js +44 -0
  507. package/dist/src/operatormessage.js.map +1 -0
  508. package/dist/src/operatorwrite.js +23 -0
  509. package/dist/src/operatorwrite.js.map +1 -0
  510. package/dist/src/outcomes.js +1364 -0
  511. package/dist/src/outcomes.js.map +1 -0
  512. package/dist/src/piauthcopy.js +5 -0
  513. package/dist/src/piauthcopy.js.map +1 -0
  514. package/dist/src/picli.js +45 -0
  515. package/dist/src/picli.js.map +1 -0
  516. package/dist/src/picwd.js +25 -0
  517. package/dist/src/picwd.js.map +1 -0
  518. package/dist/src/pilogin.js +35 -0
  519. package/dist/src/pilogin.js.map +1 -0
  520. package/dist/src/pingpong.js +1028 -0
  521. package/dist/src/pingpong.js.map +1 -0
  522. package/dist/src/posting-journal.js +442 -0
  523. package/dist/src/posting-journal.js.map +1 -0
  524. package/dist/src/posting-outbox.js +666 -0
  525. package/dist/src/posting-outbox.js.map +1 -0
  526. package/dist/src/posting-receipt.js +216 -0
  527. package/dist/src/posting-receipt.js.map +1 -0
  528. package/dist/src/posting.js +348 -0
  529. package/dist/src/posting.js.map +1 -0
  530. package/dist/src/postops.js +637 -0
  531. package/dist/src/postops.js.map +1 -0
  532. package/dist/src/pr-activity.js +11 -0
  533. package/dist/src/pr-activity.js.map +1 -0
  534. package/dist/src/pr-author.js +5 -0
  535. package/dist/src/pr-author.js.map +1 -0
  536. package/dist/src/pr-conversation.js +355 -0
  537. package/dist/src/pr-conversation.js.map +1 -0
  538. package/dist/src/pr-session-lifecycle.js +542 -0
  539. package/dist/src/pr-session-lifecycle.js.map +1 -0
  540. package/dist/src/preflight.js +341 -0
  541. package/dist/src/preflight.js.map +1 -0
  542. package/dist/src/priorresolve.js +275 -0
  543. package/dist/src/priorresolve.js.map +1 -0
  544. package/dist/src/qcrbin.js +180 -0
  545. package/dist/src/qcrbin.js.map +1 -0
  546. package/dist/src/recheck.js +1399 -0
  547. package/dist/src/recheck.js.map +1 -0
  548. package/dist/src/reconcile.js +840 -0
  549. package/dist/src/reconcile.js.map +1 -0
  550. package/dist/src/recordedfindings.js +15 -0
  551. package/dist/src/recordedfindings.js.map +1 -0
  552. package/dist/src/regression.js +546 -0
  553. package/dist/src/regression.js.map +1 -0
  554. package/dist/src/replay.js +598 -0
  555. package/dist/src/replay.js.map +1 -0
  556. package/dist/src/requiredpasses.js +113 -0
  557. package/dist/src/requiredpasses.js.map +1 -0
  558. package/dist/src/resumecheck.js +168 -0
  559. package/dist/src/resumecheck.js.map +1 -0
  560. package/dist/src/reviewcli.js +671 -0
  561. package/dist/src/reviewcli.js.map +1 -0
  562. package/dist/src/reviewcontext.js +22 -0
  563. package/dist/src/reviewcontext.js.map +1 -0
  564. package/dist/src/reviewdetach.js +574 -0
  565. package/dist/src/reviewdetach.js.map +1 -0
  566. package/dist/src/reviewhistory.js +310 -0
  567. package/dist/src/reviewhistory.js.map +1 -0
  568. package/dist/src/reviewhome.js +26 -0
  569. package/dist/src/reviewhome.js.map +1 -0
  570. package/dist/src/reviewmemory.js +110 -0
  571. package/dist/src/reviewmemory.js.map +1 -0
  572. package/dist/src/reviewnow.js +45 -0
  573. package/dist/src/reviewnow.js.map +1 -0
  574. package/dist/src/reviewrendercli.js +104 -0
  575. package/dist/src/reviewrendercli.js.map +1 -0
  576. package/dist/src/round-work.js +20 -0
  577. package/dist/src/round-work.js.map +1 -0
  578. package/dist/src/runhost.js +72 -0
  579. package/dist/src/runhost.js.map +1 -0
  580. package/dist/src/scanauth.js +25 -0
  581. package/dist/src/scanauth.js.map +1 -0
  582. package/dist/src/scanbox.js +846 -0
  583. package/dist/src/scanbox.js.map +1 -0
  584. package/dist/src/scancli.js +1074 -0
  585. package/dist/src/scancli.js.map +1 -0
  586. package/dist/src/scandriverlog.js +30 -0
  587. package/dist/src/scandriverlog.js.map +1 -0
  588. package/dist/src/session-bridge.js +75 -0
  589. package/dist/src/session-bridge.js.map +1 -0
  590. package/dist/src/session-command-payload.js +29 -0
  591. package/dist/src/session-command-payload.js.map +1 -0
  592. package/dist/src/session-command-supervisor.js +939 -0
  593. package/dist/src/session-command-supervisor.js.map +1 -0
  594. package/dist/src/sessionjsonl.js +85 -0
  595. package/dist/src/sessionjsonl.js.map +1 -0
  596. package/dist/src/sessions.js +541 -0
  597. package/dist/src/sessions.js.map +1 -0
  598. package/dist/src/setup/adapters.js +330 -0
  599. package/dist/src/setup/adapters.js.map +1 -0
  600. package/dist/src/setup/baselines.js +64 -0
  601. package/dist/src/setup/baselines.js.map +1 -0
  602. package/dist/src/setup/caserunner.js +554 -0
  603. package/dist/src/setup/caserunner.js.map +1 -0
  604. package/dist/src/setup/configcommit.js +358 -0
  605. package/dist/src/setup/configcommit.js.map +1 -0
  606. package/dist/src/setup/engine.js +672 -0
  607. package/dist/src/setup/engine.js.map +1 -0
  608. package/dist/src/setup/facts.js +593 -0
  609. package/dist/src/setup/facts.js.map +1 -0
  610. package/dist/src/setup/findings.js +948 -0
  611. package/dist/src/setup/findings.js.map +1 -0
  612. package/dist/src/setup/history.js +338 -0
  613. package/dist/src/setup/history.js.map +1 -0
  614. package/dist/src/setup/interview.js +185 -0
  615. package/dist/src/setup/interview.js.map +1 -0
  616. package/dist/src/setup/ledger.js +207 -0
  617. package/dist/src/setup/ledger.js.map +1 -0
  618. package/dist/src/setup/pins.js +43 -0
  619. package/dist/src/setup/pins.js.map +1 -0
  620. package/dist/src/setup/portrait.js +432 -0
  621. package/dist/src/setup/portrait.js.map +1 -0
  622. package/dist/src/setup/presentation.js +9 -0
  623. package/dist/src/setup/presentation.js.map +1 -0
  624. package/dist/src/setup/profile.js +417 -0
  625. package/dist/src/setup/profile.js.map +1 -0
  626. package/dist/src/setup/replay.js +240 -0
  627. package/dist/src/setup/replay.js.map +1 -0
  628. package/dist/src/setup/replaybuild.js +311 -0
  629. package/dist/src/setup/replaybuild.js.map +1 -0
  630. package/dist/src/setup/replayrun.js +806 -0
  631. package/dist/src/setup/replayrun.js.map +1 -0
  632. package/dist/src/setup/replaysource.js +169 -0
  633. package/dist/src/setup/replaysource.js.map +1 -0
  634. package/dist/src/setup/retune.js +333 -0
  635. package/dist/src/setup/retune.js.map +1 -0
  636. package/dist/src/setup/retunelog.js +51 -0
  637. package/dist/src/setup/retunelog.js.map +1 -0
  638. package/dist/src/setup/scan.js +725 -0
  639. package/dist/src/setup/scan.js.map +1 -0
  640. package/dist/src/setup/scanbuild.js +133 -0
  641. package/dist/src/setup/scanbuild.js.map +1 -0
  642. package/dist/src/setup/scanfacts.js +96 -0
  643. package/dist/src/setup/scanfacts.js.map +1 -0
  644. package/dist/src/setup/scanlease.js +251 -0
  645. package/dist/src/setup/scanlease.js.map +1 -0
  646. package/dist/src/setup/scanreport.js +289 -0
  647. package/dist/src/setup/scanreport.js.map +1 -0
  648. package/dist/src/setup/scanrun.js +1733 -0
  649. package/dist/src/setup/scanrun.js.map +1 -0
  650. package/dist/src/setup/scantree.js +257 -0
  651. package/dist/src/setup/scantree.js.map +1 -0
  652. package/dist/src/setup/stage2.js +617 -0
  653. package/dist/src/setup/stage2.js.map +1 -0
  654. package/dist/src/setup/stage2request.js +259 -0
  655. package/dist/src/setup/stage2request.js.map +1 -0
  656. package/dist/src/setup/survey.js +307 -0
  657. package/dist/src/setup/survey.js.map +1 -0
  658. package/dist/src/setup/surveyrun.js +549 -0
  659. package/dist/src/setup/surveyrun.js.map +1 -0
  660. package/dist/src/setup/textsniff.js +89 -0
  661. package/dist/src/setup/textsniff.js.map +1 -0
  662. package/dist/src/setup/trackersink.js +395 -0
  663. package/dist/src/setup/trackersink.js.map +1 -0
  664. package/dist/src/setup/triage.js +636 -0
  665. package/dist/src/setup/triage.js.map +1 -0
  666. package/dist/src/setup/tuning.js +585 -0
  667. package/dist/src/setup/tuning.js.map +1 -0
  668. package/dist/src/setup/unitcut.js +437 -0
  669. package/dist/src/setup/unitcut.js.map +1 -0
  670. package/dist/src/singleflight.js +72 -0
  671. package/dist/src/singleflight.js.map +1 -0
  672. package/dist/src/slack/announce.js +725 -0
  673. package/dist/src/slack/announce.js.map +1 -0
  674. package/dist/src/slack/api.js +227 -0
  675. package/dist/src/slack/api.js.map +1 -0
  676. package/dist/src/slack/messages.js +158 -0
  677. package/dist/src/slack/messages.js.map +1 -0
  678. package/dist/src/slack/origin.js +550 -0
  679. package/dist/src/slack/origin.js.map +1 -0
  680. package/dist/src/slack/policy.js +225 -0
  681. package/dist/src/slack/policy.js.map +1 -0
  682. package/dist/src/slack/reply.js +153 -0
  683. package/dist/src/slack/reply.js.map +1 -0
  684. package/dist/src/slack/router.js +189 -0
  685. package/dist/src/slack/router.js.map +1 -0
  686. package/dist/src/slack/socket.js +323 -0
  687. package/dist/src/slack/socket.js.map +1 -0
  688. package/dist/src/slack/types.js +11 -0
  689. package/dist/src/slack/types.js.map +1 -0
  690. package/dist/src/sqlitehint.js +30 -0
  691. package/dist/src/sqlitehint.js.map +1 -0
  692. package/dist/src/ssh-observation.js +25 -0
  693. package/dist/src/ssh-observation.js.map +1 -0
  694. package/dist/src/state.js +5869 -0
  695. package/dist/src/state.js.map +1 -0
  696. package/dist/src/statemachine.js +223 -0
  697. package/dist/src/statemachine.js.map +1 -0
  698. package/dist/src/stubs.js +52 -0
  699. package/dist/src/stubs.js.map +1 -0
  700. package/dist/src/takeover.js +506 -0
  701. package/dist/src/takeover.js.map +1 -0
  702. package/dist/src/takeoverpi.js +262 -0
  703. package/dist/src/takeoverpi.js.map +1 -0
  704. package/dist/src/terminal-failure.js +26 -0
  705. package/dist/src/terminal-failure.js.map +1 -0
  706. package/dist/src/thread-resolution-refusal.js +4 -0
  707. package/dist/src/thread-resolution-refusal.js.map +1 -0
  708. package/dist/src/tmux.js +126 -0
  709. package/dist/src/tmux.js.map +1 -0
  710. package/dist/src/trackerid.js +43 -0
  711. package/dist/src/trackerid.js.map +1 -0
  712. package/dist/src/transcriptindex.js +512 -0
  713. package/dist/src/transcriptindex.js.map +1 -0
  714. package/dist/src/tui/actions.js +404 -0
  715. package/dist/src/tui/actions.js.map +1 -0
  716. package/dist/src/tui/alerts.js +523 -0
  717. package/dist/src/tui/alerts.js.map +1 -0
  718. package/dist/src/tui/app.js +1884 -0
  719. package/dist/src/tui/app.js.map +1 -0
  720. package/dist/src/tui/archiveops.js +164 -0
  721. package/dist/src/tui/archiveops.js.map +1 -0
  722. package/dist/src/tui/autofile.js +191 -0
  723. package/dist/src/tui/autofile.js.map +1 -0
  724. package/dist/src/tui/board.js +1219 -0
  725. package/dist/src/tui/board.js.map +1 -0
  726. package/dist/src/tui/candidates.js +699 -0
  727. package/dist/src/tui/candidates.js.map +1 -0
  728. package/dist/src/tui/cellwidth.js +151 -0
  729. package/dist/src/tui/cellwidth.js.map +1 -0
  730. package/dist/src/tui/changelog-store.js +404 -0
  731. package/dist/src/tui/changelog-store.js.map +1 -0
  732. package/dist/src/tui/chord.js +26 -0
  733. package/dist/src/tui/chord.js.map +1 -0
  734. package/dist/src/tui/clipboard.js +81 -0
  735. package/dist/src/tui/clipboard.js.map +1 -0
  736. package/dist/src/tui/cmdbar.js +256 -0
  737. package/dist/src/tui/cmdbar.js.map +1 -0
  738. package/dist/src/tui/companion.js +1339 -0
  739. package/dist/src/tui/companion.js.map +1 -0
  740. package/dist/src/tui/companionplan.js +200 -0
  741. package/dist/src/tui/companionplan.js.map +1 -0
  742. package/dist/src/tui/companiontab.js +61 -0
  743. package/dist/src/tui/companiontab.js.map +1 -0
  744. package/dist/src/tui/confighome.js +105 -0
  745. package/dist/src/tui/confighome.js.map +1 -0
  746. package/dist/src/tui/connect.js +50 -0
  747. package/dist/src/tui/connect.js.map +1 -0
  748. package/dist/src/tui/deploy-ceremony.js +75 -0
  749. package/dist/src/tui/deploy-ceremony.js.map +1 -0
  750. package/dist/src/tui/deploy-drain.js +93 -0
  751. package/dist/src/tui/deploy-drain.js.map +1 -0
  752. package/dist/src/tui/deploy-process.js +234 -0
  753. package/dist/src/tui/deploy-process.js.map +1 -0
  754. package/dist/src/tui/deploytab.js +1263 -0
  755. package/dist/src/tui/deploytab.js.map +1 -0
  756. package/dist/src/tui/design.js +111 -0
  757. package/dist/src/tui/design.js.map +1 -0
  758. package/dist/src/tui/digesttab.js +148 -0
  759. package/dist/src/tui/digesttab.js.map +1 -0
  760. package/dist/src/tui/emptystate.js +32 -0
  761. package/dist/src/tui/emptystate.js.map +1 -0
  762. package/dist/src/tui/evidence.js +790 -0
  763. package/dist/src/tui/evidence.js.map +1 -0
  764. package/dist/src/tui/feed.js +415 -0
  765. package/dist/src/tui/feed.js.map +1 -0
  766. package/dist/src/tui/feedbacktab.js +907 -0
  767. package/dist/src/tui/feedbacktab.js.map +1 -0
  768. package/dist/src/tui/filters.js +75 -0
  769. package/dist/src/tui/filters.js.map +1 -0
  770. package/dist/src/tui/ghread.js +328 -0
  771. package/dist/src/tui/ghread.js.map +1 -0
  772. package/dist/src/tui/historypull.js +61 -0
  773. package/dist/src/tui/historypull.js.map +1 -0
  774. package/dist/src/tui/inboxpull.js +197 -0
  775. package/dist/src/tui/inboxpull.js.map +1 -0
  776. package/dist/src/tui/index.js +1511 -0
  777. package/dist/src/tui/index.js.map +1 -0
  778. package/dist/src/tui/issuesink.js +206 -0
  779. package/dist/src/tui/issuesink.js.map +1 -0
  780. package/dist/src/tui/jsonfile.js +32 -0
  781. package/dist/src/tui/jsonfile.js.map +1 -0
  782. package/dist/src/tui/keys.js +97 -0
  783. package/dist/src/tui/keys.js.map +1 -0
  784. package/dist/src/tui/layout.js +70 -0
  785. package/dist/src/tui/layout.js.map +1 -0
  786. package/dist/src/tui/learning.js +469 -0
  787. package/dist/src/tui/learning.js.map +1 -0
  788. package/dist/src/tui/lessonswitch.js +103 -0
  789. package/dist/src/tui/lessonswitch.js.map +1 -0
  790. package/dist/src/tui/livecontrol.js +167 -0
  791. package/dist/src/tui/livecontrol.js.map +1 -0
  792. package/dist/src/tui/memforward.js +347 -0
  793. package/dist/src/tui/memforward.js.map +1 -0
  794. package/dist/src/tui/memincident.js +287 -0
  795. package/dist/src/tui/memincident.js.map +1 -0
  796. package/dist/src/tui/memory.js +485 -0
  797. package/dist/src/tui/memory.js.map +1 -0
  798. package/dist/src/tui/memtab.js +513 -0
  799. package/dist/src/tui/memtab.js.map +1 -0
  800. package/dist/src/tui/metricstab.js +979 -0
  801. package/dist/src/tui/metricstab.js.map +1 -0
  802. package/dist/src/tui/model.js +129 -0
  803. package/dist/src/tui/model.js.map +1 -0
  804. package/dist/src/tui/mouse.js +83 -0
  805. package/dist/src/tui/mouse.js.map +1 -0
  806. package/dist/src/tui/msg.js +196 -0
  807. package/dist/src/tui/msg.js.map +1 -0
  808. package/dist/src/tui/nativecompanion.js +321 -0
  809. package/dist/src/tui/nativecompanion.js.map +1 -0
  810. package/dist/src/tui/openurl.js +17 -0
  811. package/dist/src/tui/openurl.js.map +1 -0
  812. package/dist/src/tui/paint.js +501 -0
  813. package/dist/src/tui/paint.js.map +1 -0
  814. package/dist/src/tui/panes.js +74 -0
  815. package/dist/src/tui/panes.js.map +1 -0
  816. package/dist/src/tui/pipane.js +166 -0
  817. package/dist/src/tui/pipane.js.map +1 -0
  818. package/dist/src/tui/poll-hooks.js +11 -0
  819. package/dist/src/tui/poll-hooks.js.map +1 -0
  820. package/dist/src/tui/poll.js +113 -0
  821. package/dist/src/tui/poll.js.map +1 -0
  822. package/dist/src/tui/proposals.js +715 -0
  823. package/dist/src/tui/proposals.js.map +1 -0
  824. package/dist/src/tui/prpause.js +139 -0
  825. package/dist/src/tui/prpause.js.map +1 -0
  826. package/dist/src/tui/reload.js +351 -0
  827. package/dist/src/tui/reload.js.map +1 -0
  828. package/dist/src/tui/remote.js +318 -0
  829. package/dist/src/tui/remote.js.map +1 -0
  830. package/dist/src/tui/rollover.js +767 -0
  831. package/dist/src/tui/rollover.js.map +1 -0
  832. package/dist/src/tui/route.js +143 -0
  833. package/dist/src/tui/route.js.map +1 -0
  834. package/dist/src/tui/scantab.js +1053 -0
  835. package/dist/src/tui/scantab.js.map +1 -0
  836. package/dist/src/tui/setupentry.js +81 -0
  837. package/dist/src/tui/setupentry.js.map +1 -0
  838. package/dist/src/tui/setupstate.js +29 -0
  839. package/dist/src/tui/setupstate.js.map +1 -0
  840. package/dist/src/tui/sidebar.js +602 -0
  841. package/dist/src/tui/sidebar.js.map +1 -0
  842. package/dist/src/tui/sshtriage.js +490 -0
  843. package/dist/src/tui/sshtriage.js.map +1 -0
  844. package/dist/src/tui/startup.js +172 -0
  845. package/dist/src/tui/startup.js.map +1 -0
  846. package/dist/src/tui/tabs.js +41 -0
  847. package/dist/src/tui/tabs.js.map +1 -0
  848. package/dist/src/tui/termmirror.js +217 -0
  849. package/dist/src/tui/termmirror.js.map +1 -0
  850. package/dist/src/tui/theme.js +96 -0
  851. package/dist/src/tui/theme.js.map +1 -0
  852. package/dist/src/tui/ticketstab.js +224 -0
  853. package/dist/src/tui/ticketstab.js.map +1 -0
  854. package/dist/src/tui/tmuxsocket.js +101 -0
  855. package/dist/src/tui/tmuxsocket.js.map +1 -0
  856. package/dist/src/tui/trackertickets.js +77 -0
  857. package/dist/src/tui/trackertickets.js.map +1 -0
  858. package/dist/src/tui/unwedge.js +235 -0
  859. package/dist/src/tui/unwedge.js.map +1 -0
  860. package/dist/src/tui/viewer.js +479 -0
  861. package/dist/src/tui/viewer.js.map +1 -0
  862. package/dist/src/tui/viewerinput.js +125 -0
  863. package/dist/src/tui/viewerinput.js.map +1 -0
  864. package/dist/src/updatenotice.js +143 -0
  865. package/dist/src/updatenotice.js.map +1 -0
  866. package/dist/src/usage.js +1007 -0
  867. package/dist/src/usage.js.map +1 -0
  868. package/dist/src/verdicts.js +3860 -0
  869. package/dist/src/verdicts.js.map +1 -0
  870. package/dist/src/version.js +58 -0
  871. package/dist/src/version.js.map +1 -0
  872. package/dist/src/wakes.js +411 -0
  873. package/dist/src/wakes.js.map +1 -0
  874. package/dist/src/webhook.js +736 -0
  875. package/dist/src/webhook.js.map +1 -0
  876. package/dist/src/webhookheal.js +238 -0
  877. package/dist/src/webhookheal.js.map +1 -0
  878. package/dist/src/wire.js +1823 -0
  879. package/dist/src/wire.js.map +1 -0
  880. package/dist/src/worker-session-host.js +531 -0
  881. package/dist/src/worker-session-host.js.map +1 -0
  882. package/dist/src/workerbreaker.js +112 -0
  883. package/dist/src/workerbreaker.js.map +1 -0
  884. package/dist/src/workercontrol.js +49 -0
  885. package/dist/src/workercontrol.js.map +1 -0
  886. package/dist/src/workerlease.js +351 -0
  887. package/dist/src/workerlease.js.map +1 -0
  888. package/dist/src/workerliveness.js +84 -0
  889. package/dist/src/workerliveness.js.map +1 -0
  890. package/dist/src/workerlog.js +65 -0
  891. package/dist/src/workerlog.js.map +1 -0
  892. package/dist/src/workerruntime.js +10 -0
  893. package/dist/src/workerruntime.js.map +1 -0
  894. package/dist/src/workersupervisor.js +1206 -0
  895. package/dist/src/workersupervisor.js.map +1 -0
  896. package/dist/src/workerusage.js +23 -0
  897. package/dist/src/workerusage.js.map +1 -0
  898. package/dist/src/workerworld.js +338 -0
  899. package/dist/src/workerworld.js.map +1 -0
  900. package/dist/src/worktrees.js +984 -0
  901. package/dist/src/worktrees.js.map +1 -0
  902. package/docker/Dockerfile +284 -0
  903. package/docker/Dockerfile.dockerignore +35 -0
  904. package/docker/README.md +221 -0
  905. package/docker/companion/Dockerfile +139 -0
  906. package/docker/companion/agents/evidence.md +34 -0
  907. package/docker/companion/agents/explore.md +26 -0
  908. package/docker/companion/agents/pass-survey.md +49 -0
  909. package/docker/companion/agents/study.md +26 -0
  910. package/docker/companion/entrypoint.sh +362 -0
  911. package/docker/companion/memory-health.mjs +269 -0
  912. package/docker/entrypoint.sh +799 -0
  913. package/docker/memory/Dockerfile +191 -0
  914. package/docker/memory/Dockerfile.dockerignore +20 -0
  915. package/docker/memory/README.md +60 -0
  916. package/docker/memory/entrypoint.sh +163 -0
  917. package/docker/setid/qcr-setid.c +109 -0
  918. package/docker/tunnel/Dockerfile.cloudflared +51 -0
  919. package/docker/tunnel/Dockerfile.ngrok +40 -0
  920. package/docker/tunnel/sidecar.mjs +411 -0
  921. package/docker/vm/tmux.conf +26 -0
  922. package/docker-compose.yml +348 -0
  923. package/extensions/cli/bash-guard-ambient.ts +71 -0
  924. package/extensions/cli/bash-guard.ts +12 -0
  925. package/extensions/cli/companion-guard.ts +8 -0
  926. package/extensions/cli/debug-round.ts +9 -0
  927. package/extensions/cli/inject-bridge.ts +8 -0
  928. package/extensions/cli/ledger-pin.ts +13 -0
  929. package/extensions/cli/lens.ts +22 -0
  930. package/extensions/cli/login-route.ts +10 -0
  931. package/extensions/cli/muted-theme.ts +9 -0
  932. package/extensions/cli/onboarding.ts +8 -0
  933. package/extensions/cli/setup.ts +8 -0
  934. package/extensions/cli/tui-view.ts +8 -0
  935. package/extensions/companion-guard.ts +411 -0
  936. package/extensions/debug-round.ts +281 -0
  937. package/extensions/inject-bridge.ts +630 -0
  938. package/extensions/inline-bash.ts +102 -0
  939. package/extensions/login-route.ts +220 -0
  940. package/extensions/muted-theme.json +68 -0
  941. package/extensions/muted-theme.ts +23 -0
  942. package/extensions/onboarding.ts +262 -0
  943. package/extensions/setup-replay.ts +336 -0
  944. package/extensions/setup-scan.ts +1236 -0
  945. package/extensions/setup-triage.ts +190 -0
  946. package/extensions/setup.ts +1428 -0
  947. package/extensions/tui-view.ts +489 -0
  948. package/extensions/vendor/openviking-pi/LICENSE +201 -0
  949. package/extensions/vendor/openviking-pi/NOTICE +68 -0
  950. package/extensions/vendor/openviking-pi/README.md +550 -0
  951. package/extensions/vendor/openviking-pi/client.ts +588 -0
  952. package/extensions/vendor/openviking-pi/config.json +28 -0
  953. package/extensions/vendor/openviking-pi/config.ts +271 -0
  954. package/extensions/vendor/openviking-pi/index.ts +523 -0
  955. package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
  956. package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
  957. package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
  958. package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
  959. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
  960. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
  961. package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
  962. package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
  963. package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
  964. package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
  965. package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
  966. package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
  967. package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
  968. package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
  969. package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
  970. package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
  971. package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
  972. package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
  973. package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
  974. package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
  975. package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
  976. package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
  977. package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
  978. package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
  979. package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
  980. package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
  981. package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
  982. package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
  983. package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
  984. package/extensions/vendor/openviking-pi/recall.ts +149 -0
  985. package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
  986. package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
  987. package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
  988. package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
  989. package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
  990. package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
  991. package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
  992. package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
  993. package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
  994. package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
  995. package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
  996. package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
  997. package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
  998. package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
  999. package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
  1000. package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
  1001. package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  1002. package/extensions/vendor/openviking-pi/sync.ts +163 -0
  1003. package/extensions/vendor/openviking-pi/takeover.ts +36 -0
  1004. package/extensions/vendor/openviking-pi/tools.ts +479 -0
  1005. package/extensions/vendor/openviking-pi/upstream.json +48 -0
  1006. package/package.json +138 -4
  1007. package/profiles/default/README.md +21 -0
  1008. package/profiles/default/addenda/.gitkeep +0 -0
  1009. package/profiles/default/profile.toml +17 -0
  1010. package/profiles/example-django/README.md +21 -0
  1011. package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
  1012. package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
  1013. package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
  1014. package/profiles/example-django/addenda/40-orm-usage.md +19 -0
  1015. package/profiles/example-django/addenda/50-declared-shape.md +10 -0
  1016. package/profiles/example-django/addenda/60-config-placement.md +14 -0
  1017. package/profiles/example-django/heuristics.toml +24 -0
  1018. package/profiles/example-django/profile.toml +9 -0
  1019. package/profiles/fast/README.md +24 -0
  1020. package/profiles/fast/profile.toml +59 -0
  1021. package/profiles/minimal/README.md +17 -0
  1022. package/profiles/minimal/addenda/.gitkeep +0 -0
  1023. package/profiles/minimal/profile.toml +8 -0
  1024. package/profiles/snappy/README.md +24 -0
  1025. package/profiles/snappy/addenda/10-race-as-question.md +6 -0
  1026. package/profiles/snappy/profile.toml +46 -0
  1027. package/prompts/companion-day-open.md +46 -0
  1028. package/prompts/companion-first-run.md +104 -0
  1029. package/prompts/companion.md +821 -0
  1030. package/prompts/conversation-system.md +117 -0
  1031. package/prompts/eod-sweep.md +120 -0
  1032. package/prompts/onboarding.md +113 -0
  1033. package/prompts/pass-common.md +52 -0
  1034. package/prompts/pass-quality-common.md +22 -0
  1035. package/prompts/round-first.md +874 -0
  1036. package/prompts/round-follow-up.md +991 -0
  1037. package/prompts/round-re-review.md +889 -0
  1038. package/prompts/round-recheck.md +196 -0
  1039. package/prompts/round-staleness.md +856 -0
  1040. package/schemas/review-result.v1.json +391 -0
  1041. package/schemas/scan-result.v1.json +421 -0
  1042. package/schemas/version.v1.json +57 -0
  1043. package/scripts/approve-pr.sh +16 -0
  1044. package/scripts/build-stamp.mjs +67 -0
  1045. package/scripts/commit-verdict.sh +155 -0
  1046. package/scripts/companion-e2e.sh +250 -0
  1047. package/scripts/companion-memory-forward.mjs +1966 -0
  1048. package/scripts/config-fingerprint.sh +28 -0
  1049. package/scripts/deploy-manifest.mjs +141 -0
  1050. package/scripts/deploy.sh +1435 -0
  1051. package/scripts/directory-lock.mjs +193 -0
  1052. package/scripts/heartbeat-fresh.mjs +54 -0
  1053. package/scripts/install-locator.sh +328 -0
  1054. package/scripts/install-wrapper.sh +76 -0
  1055. package/scripts/memory-search-check.mjs +98 -0
  1056. package/scripts/mint-token.sh +233 -0
  1057. package/scripts/openviking-embedder.py +392 -0
  1058. package/scripts/openviking-ov-conf.mjs +161 -0
  1059. package/scripts/openviking-provision.mjs +1122 -0
  1060. package/scripts/openviking-reembed.mjs +1154 -0
  1061. package/scripts/post-review.sh +16 -0
  1062. package/scripts/pr-checks.sh +256 -0
  1063. package/scripts/pr-context.py +174 -0
  1064. package/scripts/pr-context.sh +63 -0
  1065. package/scripts/pr-self.sh +144 -0
  1066. package/scripts/pr-threads.sh +145 -0
  1067. package/scripts/qcr +15 -0
  1068. package/scripts/qcr-host.sh +769 -0
  1069. package/scripts/render-review.sh +24 -0
  1070. package/scripts/reply-thread.sh +16 -0
  1071. package/scripts/required-passes.py +112 -0
  1072. package/scripts/smoke-companion.sh +211 -0
  1073. package/scripts/tui-deploy-runner.mjs +61 -0
  1074. package/scripts/verify-dist.mjs +201 -0
  1075. package/scripts/vm-ghcr-login.sh +188 -0
  1076. package/seeds/agents/pass-baseline.md +61 -0
  1077. package/seeds/agents/pass-contracts.md +30 -0
  1078. package/seeds/agents/pass-data-safety.md +54 -0
  1079. package/seeds/agents/pass-delivery.md +37 -0
  1080. package/seeds/agents/pass-prod-risk.md +24 -0
  1081. package/seeds/agents/pass-security.md +31 -0
  1082. package/seeds/agents/pass-statelessness.md +27 -0
  1083. package/seeds/agents/pass-ui.md +23 -0
  1084. package/seeds/agents/pass-validate.md +50 -0
  1085. package/seeds/prompts/conversation-system.md +117 -0
  1086. package/seeds/prompts/eod-sweep.md +120 -0
  1087. package/seeds/prompts/pass-common.md +52 -0
  1088. package/skills/companion/adjudicate/SKILL.md +247 -0
  1089. package/skills/companion/evidence/SKILL.md +150 -0
  1090. package/skills/companion/land/SKILL.md +200 -0
  1091. package/skills/companion/replay/SKILL.md +138 -0
  1092. package/skills/companion/setup/SKILL.md +167 -0
  1093. package/skills/companion/survey/SKILL.md +160 -0
  1094. package/skills/selfimprove/README.md +34 -0
  1095. package/skills/selfimprove/SKILL.md +127 -0
  1096. package/skills/selfimprove/agents/openai.yaml +2 -0
  1097. package/skills/selfimprove/commands/selfimprove.md +6 -0
  1098. package/skills/selfimprove/references/fix-loop.md +49 -0
  1099. package/skills/selfimprove/references/ledger.md +20 -0
  1100. package/skills/selfimprove/references/setup.md +40 -0
  1101. package/src/anomalies.ts +566 -0
  1102. package/src/answerworker.ts +251 -0
  1103. package/src/appstamp.ts +65 -0
  1104. package/src/archive/admission.ts +3 -0
  1105. package/src/archive/archivedb.ts +548 -0
  1106. package/src/archive/archiver.ts +1963 -0
  1107. package/src/archive/archivewrite.ts +279 -0
  1108. package/src/archive/bundle.ts +333 -0
  1109. package/src/archive/dbmove.ts +911 -0
  1110. package/src/archive/duplicatecounts.ts +98 -0
  1111. package/src/archive/eligibility.ts +181 -0
  1112. package/src/archive/extract.ts +342 -0
  1113. package/src/archive/fsutil.ts +415 -0
  1114. package/src/archive/gzfile.ts +370 -0
  1115. package/src/archive/kinds.ts +21 -0
  1116. package/src/archive/livecheckpoint.ts +217 -0
  1117. package/src/archive/liveretention.ts +180 -0
  1118. package/src/archive/manifest.ts +519 -0
  1119. package/src/archive/overseer.ts +336 -0
  1120. package/src/archive/piglobal.ts +174 -0
  1121. package/src/archive/plan.ts +612 -0
  1122. package/src/archive/prarchive.ts +984 -0
  1123. package/src/archive/prfiles.ts +406 -0
  1124. package/src/archive/report.ts +252 -0
  1125. package/src/archive/restore.ts +330 -0
  1126. package/src/archive/rows.ts +187 -0
  1127. package/src/archive/seal.ts +630 -0
  1128. package/src/archive/status.ts +719 -0
  1129. package/src/archive/storepass.ts +581 -0
  1130. package/src/backfill.ts +362 -0
  1131. package/src/bench/cost.ts +113 -0
  1132. package/src/bench/exec.ts +90 -0
  1133. package/src/bench/values.ts +13 -0
  1134. package/src/botcontrol.ts +176 -0
  1135. package/src/botworker.ts +953 -0
  1136. package/src/capture.ts +321 -0
  1137. package/src/cases.ts +1789 -0
  1138. package/src/channelfs.ts +26 -0
  1139. package/src/checklist.ts +506 -0
  1140. package/src/ciretry.ts +142 -0
  1141. package/src/cli.ts +2306 -0
  1142. package/src/cliops.ts +385 -0
  1143. package/src/clonedir.ts +15 -0
  1144. package/src/commandeffects.ts +57 -0
  1145. package/src/commands.ts +1704 -0
  1146. package/src/companioninbox.ts +239 -0
  1147. package/src/compare.ts +957 -0
  1148. package/src/config.ts +2070 -0
  1149. package/src/configcheck.ts +133 -0
  1150. package/src/coreext.ts +47 -0
  1151. package/src/daemon.ts +1327 -0
  1152. package/src/debuground.ts +927 -0
  1153. package/src/debugroundpi.ts +272 -0
  1154. package/src/diagnostics.ts +14 -0
  1155. package/src/digest.ts +183 -0
  1156. package/src/dispatch.ts +2251 -0
  1157. package/src/engine/answerguard.ts +27 -0
  1158. package/src/engine/botsdk.ts +336 -0
  1159. package/src/engine/caserunner-sdk.ts +423 -0
  1160. package/src/engine/debugpi.ts +247 -0
  1161. package/src/eod.ts +669 -0
  1162. package/src/eodpinwatch.ts +556 -0
  1163. package/src/escalation.ts +1253 -0
  1164. package/src/eventloop.ts +1045 -0
  1165. package/src/execboundary.ts +645 -0
  1166. package/src/feedback.ts +1162 -0
  1167. package/src/findingscheck.ts +298 -0
  1168. package/src/fsdenied.ts +24 -0
  1169. package/src/ghread.ts +242 -0
  1170. package/src/github.ts +1539 -0
  1171. package/src/githubtoken.ts +435 -0
  1172. package/src/homes.ts +175 -0
  1173. package/src/hostroute.ts +53 -0
  1174. package/src/improvements.ts +787 -0
  1175. package/src/install.ts +1103 -0
  1176. package/src/installkind.ts +50 -0
  1177. package/src/ipc/capability.ts +333 -0
  1178. package/src/ipc/client.ts +269 -0
  1179. package/src/ipc/control.ts +90 -0
  1180. package/src/ipc/ghreadshim.ts +94 -0
  1181. package/src/ipc/launcher.ts +65 -0
  1182. package/src/ipc/permits.ts +362 -0
  1183. package/src/ipc/postshim.ts +164 -0
  1184. package/src/ipc/protocol.ts +342 -0
  1185. package/src/ipc/server.ts +371 -0
  1186. package/src/ipc/workerspec.ts +218 -0
  1187. package/src/learner/learner.ts +1655 -0
  1188. package/src/learner/miner.ts +1290 -0
  1189. package/src/learner/packet.ts +340 -0
  1190. package/src/learner/receipt.ts +168 -0
  1191. package/src/learner/status.ts +107 -0
  1192. package/src/ledgercheck.ts +157 -0
  1193. package/src/lessonledger.ts +703 -0
  1194. package/src/lessonselect.ts +888 -0
  1195. package/src/lessonshadow.ts +810 -0
  1196. package/src/levers.ts +393 -0
  1197. package/src/live/projection.ts +491 -0
  1198. package/src/live/publish.ts +92 -0
  1199. package/src/live/read.ts +86 -0
  1200. package/src/livecli.ts +336 -0
  1201. package/src/loginhint.ts +61 -0
  1202. package/src/loginrefresh/atomic.ts +76 -0
  1203. package/src/loginrefresh/lock.ts +104 -0
  1204. package/src/loginrefresh/main.ts +59 -0
  1205. package/src/loginrefresh/pirefresh.ts +60 -0
  1206. package/src/loginrefresh/refresher.ts +326 -0
  1207. package/src/loginrefresh/supervisor.ts +273 -0
  1208. package/src/loginrefresh/types.ts +35 -0
  1209. package/src/memory/context.ts +385 -0
  1210. package/src/memory/datafence.ts +9 -0
  1211. package/src/memory/delivery.ts +171 -0
  1212. package/src/memory/eventtags.ts +80 -0
  1213. package/src/memory/health.ts +953 -0
  1214. package/src/memory/import.ts +664 -0
  1215. package/src/memory/layout.ts +261 -0
  1216. package/src/memory/legacy-file.ts +104 -0
  1217. package/src/memory/lessons.ts +447 -0
  1218. package/src/memory/ovsession.ts +601 -0
  1219. package/src/memory/relayout.ts +335 -0
  1220. package/src/memory/roundactivity.ts +209 -0
  1221. package/src/memory/selection.ts +989 -0
  1222. package/src/memory/shadowstatus.ts +125 -0
  1223. package/src/memory/store.ts +275 -0
  1224. package/src/memory/textidentity.ts +8 -0
  1225. package/src/metrics.ts +1056 -0
  1226. package/src/missingconfig.ts +7 -0
  1227. package/src/npmverbs.ts +266 -0
  1228. package/src/onboard/choices.ts +255 -0
  1229. package/src/onboard/engine.ts +537 -0
  1230. package/src/onboard/launch.ts +103 -0
  1231. package/src/onboard/pipin.ts +31 -0
  1232. package/src/onboard/stepforward.ts +61 -0
  1233. package/src/onboard/steps/appslug.ts +268 -0
  1234. package/src/onboard/steps/bringup.ts +482 -0
  1235. package/src/onboard/steps/compose.ts +490 -0
  1236. package/src/onboard/steps/configdelivery.ts +58 -0
  1237. package/src/onboard/steps/configfill.ts +1440 -0
  1238. package/src/onboard/steps/deploysh.ts +105 -0
  1239. package/src/onboard/steps/embeddings.ts +550 -0
  1240. package/src/onboard/steps/escalation.ts +640 -0
  1241. package/src/onboard/steps/githubaccount.ts +716 -0
  1242. package/src/onboard/steps/githubapp.ts +1498 -0
  1243. package/src/onboard/steps/golive.ts +579 -0
  1244. package/src/onboard/steps/index.ts +82 -0
  1245. package/src/onboard/steps/ingress.ts +974 -0
  1246. package/src/onboard/steps/inventory.ts +526 -0
  1247. package/src/onboard/steps/llmauth.ts +1180 -0
  1248. package/src/onboard/steps/memory.ts +1347 -0
  1249. package/src/onboard/steps/memorykeys.ts +85 -0
  1250. package/src/onboard/steps/preflight.ts +191 -0
  1251. package/src/onboard/steps/proof.ts +1042 -0
  1252. package/src/onboard/steps/ssh.ts +377 -0
  1253. package/src/onboard/steps/topology.ts +338 -0
  1254. package/src/onboard/steps/transport.ts +680 -0
  1255. package/src/onboard/steps/tuiroute.ts +119 -0
  1256. package/src/onboard/steps/verifyup.ts +467 -0
  1257. package/src/onboard/steps/vmdeploy.ts +442 -0
  1258. package/src/onboard/toml.ts +50 -0
  1259. package/src/onboard/toolnames.ts +34 -0
  1260. package/src/opencli.ts +591 -0
  1261. package/src/openpr.ts +568 -0
  1262. package/src/openviking/client.ts +1131 -0
  1263. package/src/openviking/context.ts +263 -0
  1264. package/src/openviking/embedder.ts +516 -0
  1265. package/src/openviking/gapsink.ts +63 -0
  1266. package/src/openviking/health.ts +83 -0
  1267. package/src/openviking/index.ts +119 -0
  1268. package/src/openviking/lessonfind.ts +92 -0
  1269. package/src/openviking/probe.ts +129 -0
  1270. package/src/openviking/proc.ts +110 -0
  1271. package/src/openviking/provisionreport.ts +160 -0
  1272. package/src/openviking/reembed.ts +412 -0
  1273. package/src/openviking/relayout.ts +177 -0
  1274. package/src/openviking/sidecar-main.ts +38 -0
  1275. package/src/openviking/sidecar.ts +106 -0
  1276. package/src/openviking/store.ts +2025 -0
  1277. package/src/openviking/supervisor.ts +474 -0
  1278. package/src/openviking/types.ts +174 -0
  1279. package/src/operator-recovery.ts +237 -0
  1280. package/src/operatormessage.ts +52 -0
  1281. package/src/operatorwrite.ts +25 -0
  1282. package/src/outcomes.ts +1677 -0
  1283. package/src/piauthcopy.ts +12 -0
  1284. package/src/picli.ts +49 -0
  1285. package/src/picwd.ts +25 -0
  1286. package/src/pilogin.ts +37 -0
  1287. package/src/pingpong.ts +1339 -0
  1288. package/src/posting-journal.ts +554 -0
  1289. package/src/posting-outbox.ts +810 -0
  1290. package/src/posting-receipt.ts +341 -0
  1291. package/src/posting.ts +603 -0
  1292. package/src/postops.ts +832 -0
  1293. package/src/pr-activity.ts +20 -0
  1294. package/src/pr-author.ts +6 -0
  1295. package/src/pr-conversation.ts +432 -0
  1296. package/src/pr-session-lifecycle.ts +631 -0
  1297. package/src/preflight.ts +391 -0
  1298. package/src/priorresolve.ts +343 -0
  1299. package/src/qcrbin.ts +186 -0
  1300. package/src/recheck.ts +1776 -0
  1301. package/src/reconcile.ts +1056 -0
  1302. package/src/recordedfindings.ts +27 -0
  1303. package/src/regression.ts +833 -0
  1304. package/src/replay.ts +816 -0
  1305. package/src/requiredpasses.ts +148 -0
  1306. package/src/resumecheck.ts +246 -0
  1307. package/src/reviewcli.ts +787 -0
  1308. package/src/reviewcontext.ts +35 -0
  1309. package/src/reviewdetach.ts +734 -0
  1310. package/src/reviewhistory.ts +441 -0
  1311. package/src/reviewhome.ts +36 -0
  1312. package/src/reviewmemory.ts +133 -0
  1313. package/src/reviewnow.ts +76 -0
  1314. package/src/reviewrendercli.ts +139 -0
  1315. package/src/round-work.ts +42 -0
  1316. package/src/runhost.ts +108 -0
  1317. package/src/scanauth.ts +36 -0
  1318. package/src/scanbox.ts +1081 -0
  1319. package/src/scancli.ts +1296 -0
  1320. package/src/scandriverlog.ts +32 -0
  1321. package/src/session-bridge.ts +139 -0
  1322. package/src/session-command-payload.ts +35 -0
  1323. package/src/session-command-supervisor.ts +1142 -0
  1324. package/src/sessionjsonl.ts +88 -0
  1325. package/src/sessions.ts +659 -0
  1326. package/src/setup/adapters.ts +335 -0
  1327. package/src/setup/baselines.ts +110 -0
  1328. package/src/setup/caserunner.ts +611 -0
  1329. package/src/setup/configcommit.ts +443 -0
  1330. package/src/setup/engine.ts +1001 -0
  1331. package/src/setup/facts.ts +627 -0
  1332. package/src/setup/findings.ts +1264 -0
  1333. package/src/setup/history.ts +443 -0
  1334. package/src/setup/interview.ts +243 -0
  1335. package/src/setup/ledger.ts +324 -0
  1336. package/src/setup/pins.ts +66 -0
  1337. package/src/setup/portrait.ts +635 -0
  1338. package/src/setup/presentation.ts +18 -0
  1339. package/src/setup/profile.ts +508 -0
  1340. package/src/setup/replay.ts +370 -0
  1341. package/src/setup/replaybuild.ts +392 -0
  1342. package/src/setup/replayrun.ts +1096 -0
  1343. package/src/setup/replaysource.ts +198 -0
  1344. package/src/setup/retune.ts +415 -0
  1345. package/src/setup/retunelog.ts +95 -0
  1346. package/src/setup/scan.ts +1033 -0
  1347. package/src/setup/scanbuild.ts +168 -0
  1348. package/src/setup/scanfacts.ts +107 -0
  1349. package/src/setup/scanlease.ts +297 -0
  1350. package/src/setup/scanreport.ts +373 -0
  1351. package/src/setup/scanrun.ts +2268 -0
  1352. package/src/setup/scantree.ts +259 -0
  1353. package/src/setup/stage2.ts +926 -0
  1354. package/src/setup/stage2request.ts +347 -0
  1355. package/src/setup/survey.ts +458 -0
  1356. package/src/setup/surveyrun.ts +758 -0
  1357. package/src/setup/textsniff.ts +95 -0
  1358. package/src/setup/trackersink.ts +433 -0
  1359. package/src/setup/triage.ts +768 -0
  1360. package/src/setup/tuning.ts +766 -0
  1361. package/src/setup/unitcut.ts +523 -0
  1362. package/src/singleflight.ts +86 -0
  1363. package/src/slack/announce.ts +911 -0
  1364. package/src/slack/api.ts +303 -0
  1365. package/src/slack/messages.ts +288 -0
  1366. package/src/slack/origin.ts +599 -0
  1367. package/src/slack/policy.ts +354 -0
  1368. package/src/slack/reply.ts +201 -0
  1369. package/src/slack/router.ts +285 -0
  1370. package/src/slack/socket.ts +384 -0
  1371. package/src/slack/types.ts +108 -0
  1372. package/src/sqlitehint.ts +32 -0
  1373. package/src/ssh-observation.ts +30 -0
  1374. package/src/state.ts +8261 -0
  1375. package/src/statemachine.ts +372 -0
  1376. package/src/stubs.ts +68 -0
  1377. package/src/takeover.ts +634 -0
  1378. package/src/takeoverpi.ts +306 -0
  1379. package/src/terminal-failure.ts +41 -0
  1380. package/src/thread-resolution-refusal.ts +2 -0
  1381. package/src/tmux.ts +153 -0
  1382. package/src/trackerid.ts +41 -0
  1383. package/src/transcriptindex.ts +665 -0
  1384. package/src/tui/actions.ts +484 -0
  1385. package/src/tui/alerts.ts +666 -0
  1386. package/src/tui/app.ts +2026 -0
  1387. package/src/tui/archiveops.ts +192 -0
  1388. package/src/tui/autofile.ts +233 -0
  1389. package/src/tui/board.ts +1529 -0
  1390. package/src/tui/candidates.ts +889 -0
  1391. package/src/tui/cellwidth.ts +148 -0
  1392. package/src/tui/changelog-store.ts +503 -0
  1393. package/src/tui/chord.ts +25 -0
  1394. package/src/tui/clipboard.ts +92 -0
  1395. package/src/tui/cmdbar.ts +299 -0
  1396. package/src/tui/companion.ts +1495 -0
  1397. package/src/tui/companionplan.ts +247 -0
  1398. package/src/tui/companiontab.ts +83 -0
  1399. package/src/tui/confighome.ts +155 -0
  1400. package/src/tui/connect.ts +74 -0
  1401. package/src/tui/deploy-ceremony.ts +104 -0
  1402. package/src/tui/deploy-drain.ts +129 -0
  1403. package/src/tui/deploy-process.ts +265 -0
  1404. package/src/tui/deploytab.ts +1523 -0
  1405. package/src/tui/design.ts +125 -0
  1406. package/src/tui/digesttab.ts +214 -0
  1407. package/src/tui/emptystate.ts +39 -0
  1408. package/src/tui/evidence.ts +999 -0
  1409. package/src/tui/feed.ts +604 -0
  1410. package/src/tui/feedbacktab.ts +1077 -0
  1411. package/src/tui/filters.ts +94 -0
  1412. package/src/tui/ghread.ts +388 -0
  1413. package/src/tui/historypull.ts +81 -0
  1414. package/src/tui/inboxpull.ts +236 -0
  1415. package/src/tui/index.ts +1656 -0
  1416. package/src/tui/issuesink.ts +238 -0
  1417. package/src/tui/jsonfile.ts +42 -0
  1418. package/src/tui/keys.ts +109 -0
  1419. package/src/tui/layout.ts +108 -0
  1420. package/src/tui/learning.ts +627 -0
  1421. package/src/tui/lessonswitch.ts +118 -0
  1422. package/src/tui/livecontrol.ts +228 -0
  1423. package/src/tui/memforward.ts +492 -0
  1424. package/src/tui/memincident.ts +363 -0
  1425. package/src/tui/memory.ts +620 -0
  1426. package/src/tui/memtab.ts +606 -0
  1427. package/src/tui/metricstab.ts +1177 -0
  1428. package/src/tui/model.ts +165 -0
  1429. package/src/tui/mouse.ts +98 -0
  1430. package/src/tui/msg.ts +268 -0
  1431. package/src/tui/nativecompanion.ts +417 -0
  1432. package/src/tui/openurl.ts +21 -0
  1433. package/src/tui/paint.ts +511 -0
  1434. package/src/tui/panes.ts +98 -0
  1435. package/src/tui/pipane.ts +219 -0
  1436. package/src/tui/poll-hooks.ts +12 -0
  1437. package/src/tui/poll.ts +147 -0
  1438. package/src/tui/proposals.ts +838 -0
  1439. package/src/tui/prpause.ts +159 -0
  1440. package/src/tui/reload.ts +436 -0
  1441. package/src/tui/remote.ts +421 -0
  1442. package/src/tui/rollover.ts +969 -0
  1443. package/src/tui/route.ts +173 -0
  1444. package/src/tui/scantab.ts +1290 -0
  1445. package/src/tui/setupentry.ts +93 -0
  1446. package/src/tui/setupstate.ts +52 -0
  1447. package/src/tui/sidebar.ts +749 -0
  1448. package/src/tui/sshtriage.ts +631 -0
  1449. package/src/tui/startup.ts +231 -0
  1450. package/src/tui/tabs.ts +45 -0
  1451. package/src/tui/termmirror.ts +224 -0
  1452. package/src/tui/theme.ts +96 -0
  1453. package/src/tui/ticketstab.ts +310 -0
  1454. package/src/tui/tmuxsocket.ts +129 -0
  1455. package/src/tui/trackertickets.ts +84 -0
  1456. package/src/tui/unwedge.ts +274 -0
  1457. package/src/tui/viewer.ts +575 -0
  1458. package/src/tui/viewerinput.ts +173 -0
  1459. package/src/updatenotice.ts +178 -0
  1460. package/src/usage.ts +1154 -0
  1461. package/src/verdicts.ts +4707 -0
  1462. package/src/version.ts +80 -0
  1463. package/src/wakes.ts +476 -0
  1464. package/src/webhook.ts +951 -0
  1465. package/src/webhookheal.ts +276 -0
  1466. package/src/wire.ts +2182 -0
  1467. package/src/worker-session-host.ts +602 -0
  1468. package/src/workerbreaker.ts +156 -0
  1469. package/src/workercontrol.ts +104 -0
  1470. package/src/workerlease.ts +441 -0
  1471. package/src/workerliveness.ts +103 -0
  1472. package/src/workerlog.ts +79 -0
  1473. package/src/workerruntime.ts +30 -0
  1474. package/src/workersupervisor.ts +1538 -0
  1475. package/src/workerusage.ts +42 -0
  1476. package/src/workerworld.ts +433 -0
  1477. package/src/worktrees.ts +1138 -0
  1478. package/tests/fixtures/regression-cases.json +795 -0
  1479. package/tests/fixtures/seed-manifest.json +19 -0
package/src/cli.ts ADDED
@@ -0,0 +1,2306 @@
1
+ // qcr — the operator CLI. Read-only against state/ (status.json + state.db)
2
+ // except where a subcommand says otherwise. Nothing here attaches to a tmux
3
+ // session: a PR is read with `live` / `open pr`, entered with `takeover` /
4
+ // `debug-round` (whose own lines say how to attach to their pi).
5
+ import {
6
+ readFileSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ realpathSync,
11
+ renameSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { randomUUID } from "node:crypto";
17
+ import { takeHandoffToken } from "@quickchatai/pi-quickreview/src/ghcred.js";
18
+ import { loadConfig, sandboxRoot, type Config, type TuiConfig } from "./config.js";
19
+ import { daemonLoadLine } from "./eventloop.js";
20
+ import {
21
+ ManifestUnreadable,
22
+ parseManifestText,
23
+ summarizeManifest,
24
+ type ManifestSummary,
25
+ } from "./archive/manifest.js";
26
+ import { archiveStatusLine, fmtBytes } from "./archive/status.js";
27
+ import { memoryStatusLine } from "./memory/health.js";
28
+ import {
29
+ cancelCauseLabel,
30
+ parseExecutedPasses,
31
+ roundUsage,
32
+ StateStore,
33
+ tallyCancellations,
34
+ type CancellationTally,
35
+ type StatusSnapshot,
36
+ } from "./state.js";
37
+ import {
38
+ cancellationSummaryLine,
39
+ computeMetrics,
40
+ computeTimeline,
41
+ readOps,
42
+ renderMetrics,
43
+ renderTimeline,
44
+ } from "./metrics.js";
45
+ import { CommandDrainSender, drainPayload } from "./tui/deploy-drain.js";
46
+ import {
47
+ locateConfig,
48
+ locateKeysFile,
49
+ locateSecrets,
50
+ migrateConfig,
51
+ migrateProfiles,
52
+ migrateReport,
53
+ profileMigrateReport,
54
+ profileSearchDirs,
55
+ migrateSecrets,
56
+ operatorStateDir,
57
+ repointComposeSecrets,
58
+ composeRepointReport,
59
+ homeProfileLookup,
60
+ secretsMigrateReport,
61
+ stateHome,
62
+ strandedCheckoutConfig,
63
+ strandedCheckoutProfiles,
64
+ strandedLegacySecrets,
65
+ type InstallEnv,
66
+ } from "./install.js";
67
+ import { sanitizeText } from "./tui/paint.js";
68
+ import { operatorCommandWriteScript } from "./tui/unwedge.js";
69
+ import { shellQuote, transportOf } from "./tui/remote.js";
70
+ import { MutationGate } from "@quickchatai/pi-quickreview/src/gate.js";
71
+ import { botAccountLogin, isPersonMode } from "@quickchatai/pi-quickreview/src/identity.js";
72
+ import type { GitHubAdapter } from "./github.js";
73
+ import type { OutcomeCapture } from "./outcomes.js";
74
+ import type { Learner, MineReport } from "./learner/learner.js";
75
+ import type { CurationStatus } from "./learner/status.js";
76
+ import { remoteSandboxRoot, resolveLayout } from "./tui/layout.js";
77
+
78
+ export { drainPayload } from "./tui/deploy-drain.js";
79
+
80
+ /** Where the operator verbs keep the state database (src/homes.ts
81
+ * `operatorStateDir`): the state home for a package-manager install, the
82
+ * checkout's or the image's `state/` otherwise. */
83
+ function stateDir(): string {
84
+ return operatorStateDir(sandboxRoot());
85
+ }
86
+
87
+ function openStore(): StateStore {
88
+ return new StateStore(join(stateDir(), "state.db"));
89
+ }
90
+
91
+ export const OUTCOMES_USAGE =
92
+ "usage: qcr outcomes [--days <n>] [--capture] [--read-github] [--json]";
93
+
94
+ /** `qcr outcomes` flags. An unknown token is refused, so a
95
+ * mistyped `--capture` cannot quietly print a stale table. */
96
+ export function parseOutcomesArgs(rest: readonly string[]): {
97
+ days: number;
98
+ capture: boolean;
99
+ readGithub: boolean;
100
+ } {
101
+ let days = 30;
102
+ let capture = false;
103
+ let readGithub = false;
104
+ for (let i = 0; i < rest.length; i++) {
105
+ const a = rest[i];
106
+ if (a === "--capture") capture = true;
107
+ else if (a === "--read-github") readGithub = true;
108
+ else if (a === "--days") {
109
+ const n = Number(rest[++i]);
110
+ if (!Number.isInteger(n) || n <= 0) throw new Error(OUTCOMES_USAGE);
111
+ days = n;
112
+ } else throw new Error(OUTCOMES_USAGE);
113
+ }
114
+ if (readGithub && !capture) throw new Error(`${OUTCOMES_USAGE} (--read-github needs --capture)`);
115
+ return { days, capture, readGithub };
116
+ }
117
+
118
+ /**
119
+ * The gate behind every read-only CLI command that opens the adapter
120
+ * (`qcr outcomes --read-github`, `why --json`'s enrichment, `backfill`,
121
+ * `history digest`, `regression`): it can only RECORD. Dry-run, so every
122
+ * ordinary kind is written to `dryrunDir` and never executed; and an executor
123
+ * that refuses, so even the one class the gate runs in dry-run
124
+ * (`APP_SELF_CONFIG_KINDS`) cannot reach GitHub. The reads the command makes
125
+ * go direct, as every adapter read does; nothing can write.
126
+ */
127
+ export function recordOnlyGate(dryrunDir: string, command = "qcr outcomes"): MutationGate {
128
+ const gate = new MutationGate(true, dryrunDir, async () => {
129
+ throw new Error(`${command} performs no mutations`);
130
+ });
131
+ // The gate's loud line (app-self-config attempts) on stdout.
132
+ gate.setLog((msg, extra) => console.log(`[quickreviewd] ${msg}`, extra ?? ""));
133
+ return gate;
134
+ }
135
+
136
+ /**
137
+ * The `--capture` half of `qcr outcomes`: every PR with a posted finding
138
+ * recorded since `since`, oldest number first — with `read`, ONE read of its
139
+ * inline comments first for the link facts (a failed read is reported and the
140
+ * PR is captured from what the state database holds) — then captured.
141
+ * Returns the PRs captured.
142
+ */
143
+ export async function captureOutcomesWindow(opts: {
144
+ store: StateStore;
145
+ outcomes: Pick<OutcomeCapture, "observeBotComments" | "capturePr">;
146
+ since: string;
147
+ read?: Pick<GitHubAdapter, "listReviewCommentsFull">;
148
+ write?: (line: string) => void;
149
+ }): Promise<number[]> {
150
+ const write = opts.write ?? ((line: string) => process.stderr.write(`${line}\n`));
151
+ const prs = (
152
+ opts.store.db
153
+ .prepare(
154
+ "SELECT DISTINCT pr FROM round_findings WHERE disposition = 'posted' AND recorded_at >= ? ORDER BY pr",
155
+ )
156
+ .all(opts.since) as Array<{ pr: number }>
157
+ ).map((r) => r.pr);
158
+ for (const pr of prs) {
159
+ if (opts.read) {
160
+ try {
161
+ opts.outcomes.observeBotComments(pr, await opts.read.listReviewCommentsFull(pr));
162
+ } catch (err) {
163
+ write(`outcomes #${pr}: could not read its comments: ${String(err)}`);
164
+ }
165
+ }
166
+ const r = await opts.outcomes.capturePr(pr);
167
+ write(
168
+ `outcomes #${pr}: ${r.posted} posted, ${r.linked} comment(s) linked, ${r.unmatched} unmatched, ${r.changed} fate(s) changed`,
169
+ );
170
+ }
171
+ return prs;
172
+ }
173
+
174
+ /**
175
+ * `qcr outcomes`: what became of the findings posted in the last
176
+ * `--days` (default 30) — the fate table per lens, how many posted findings
177
+ * are linked to their comment, and the whole-review signals per review.
178
+ * `--capture` first re-captures every PR with a posted finding in the window
179
+ * from state.db and the base clone's git (the daemon does this on its own for
180
+ * open and just-closed PRs; this reaches the older ones). `--read-github` adds
181
+ * ONE read per such PR — its inline comments, for the link facts — through a
182
+ * gate that can only record (`recordOnlyGate`). Nothing is ever written to
183
+ * GitHub.
184
+ */
185
+ export async function cmdOutcomes(rest: string[], json: boolean): Promise<number> {
186
+ const { days, capture, readGithub } = parseOutcomesArgs(rest);
187
+ const cfg = loadConfig();
188
+ const { OutcomeCapture, renderOutcomeSummary } = await import("./outcomes.js");
189
+ const { WorktreeManager } = await import("./worktrees.js");
190
+ const { githubCloneDir } = await import("./config.js");
191
+ const root = sandboxRoot();
192
+ const worktrees = new WorktreeManager({
193
+ root,
194
+ repo: cfg.github.repo,
195
+ cloneDir: githubCloneDir(cfg.github),
196
+ });
197
+ const store = openStore();
198
+ try {
199
+ const outcomes = new OutcomeCapture({
200
+ store,
201
+ botLogin: cfg.github.bot_login,
202
+ personMode: isPersonMode(cfg.github),
203
+ ...(existsSync(worktrees.basePath) ? { git: worktrees } : {}),
204
+ });
205
+ const since = new Date(Date.now() - days * 86_400_000).toISOString();
206
+ if (capture) {
207
+ let read: GitHubAdapter | undefined;
208
+ if (readGithub) {
209
+ const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
210
+ const { GitHubAdapter } = await import("./github.js");
211
+ read = new GitHubAdapter(
212
+ cfg,
213
+ githubCredential(cfg.github),
214
+ recordOnlyGate(join(stateDir(), "dryrun")),
215
+ );
216
+ }
217
+ await captureOutcomesWindow({ store, outcomes, since, ...(read ? { read } : {}) });
218
+ }
219
+ const summary = outcomes.summary(since);
220
+ console.log(json ? JSON.stringify(summary) : renderOutcomeSummary(summary));
221
+ return 0;
222
+ } finally {
223
+ store.close();
224
+ }
225
+ }
226
+
227
+ export const LESSONS_USAGE =
228
+ "usage: qcr lessons [mine] [--json] | qcr lessons shadow-report [--days <n>] [--json] | " +
229
+ "qcr lessons off|on | qcr lessons suppress|unsuppress <experiences/<file>.md | viking://… URI>";
230
+
231
+ /** The subcommands of `qcr lessons` beyond the table: the shadow report, and the
232
+ * operator's kill switch (written into the root-sealed commands/operator/). */
233
+ export type LessonsSubcommand =
234
+ | { kind: "shadow-report"; days: number }
235
+ | { kind: "switch"; command: "lessons-off" | "lessons-on" }
236
+ | { kind: "switch"; command: "suppress-lesson" | "unsuppress-lesson"; target: string };
237
+
238
+ /** `qcr lessons shadow-report|off|on|suppress|unsuppress …`, or null when
239
+ * `rest` is the candidates table (`[mine]`). An unknown flag or a missing target is
240
+ * refused. */
241
+ export function parseLessonsSubcommand(rest: readonly string[]): LessonsSubcommand | null {
242
+ const [head, ...tail] = rest;
243
+ switch (head) {
244
+ case "shadow-report": {
245
+ let days = 14;
246
+ for (let i = 0; i < tail.length; i++) {
247
+ if (tail[i] === "--days") {
248
+ const n = Number(tail[i + 1]);
249
+ if (!Number.isInteger(n) || n < 1 || n > 365) throw new Error(LESSONS_USAGE);
250
+ days = n;
251
+ i += 1;
252
+ } else throw new Error(LESSONS_USAGE);
253
+ }
254
+ return { kind: "shadow-report", days };
255
+ }
256
+ case "off":
257
+ case "on":
258
+ if (tail.length > 0) throw new Error(LESSONS_USAGE);
259
+ return { kind: "switch", command: head === "off" ? "lessons-off" : "lessons-on" };
260
+ case "suppress":
261
+ case "unsuppress":
262
+ if (tail.length !== 1 || !tail[0]) throw new Error(LESSONS_USAGE);
263
+ return {
264
+ kind: "switch",
265
+ command: head === "suppress" ? "suppress-lesson" : "unsuppress-lesson",
266
+ target: tail[0],
267
+ };
268
+ default:
269
+ return null;
270
+ }
271
+ }
272
+
273
+ /** `qcr lessons` arguments: `mine` or nothing. An unknown token
274
+ * is refused, so a mistyped `mine` cannot quietly print yesterday's table. */
275
+ export function parseLessonsArgs(rest: readonly string[]): { mine: boolean } {
276
+ if (rest.length === 0) return { mine: false };
277
+ if (rest.length === 1 && rest[0] === "mine") return { mine: true };
278
+ throw new Error(LESSONS_USAGE);
279
+ }
280
+
281
+ /** What `qcr lessons` prints: the mine's report (with `mine`), the undecided
282
+ * candidates per class, and the latest EOD curation. */
283
+ export function renderLessons(opts: {
284
+ report: MineReport | null;
285
+ clusters: ReturnType<Learner["clusters"]>;
286
+ status: CurationStatus;
287
+ }): string {
288
+ const out: string[] = [];
289
+ const r = opts.report;
290
+ if (r) {
291
+ const by = Object.entries(r.added_by_source)
292
+ .filter(([, v]) => (v ?? 0) > 0)
293
+ .map(([k, v]) => `${k} ${v}`)
294
+ .join(", ");
295
+ out.push(`mined: ${r.added} new candidate${r.added === 1 ? "" : "s"}${by ? ` (${by})` : ""}`);
296
+ if (r.recall)
297
+ out.push(
298
+ `post-merge recall: ${r.recall.scanned} merged PR(s) read, ${r.recall.unknown} unknown`,
299
+ );
300
+ else out.push("post-merge recall: not read (no base clone)");
301
+ if (r.digest)
302
+ out.push(
303
+ `digest backfill: ${r.digest.added} added from ${r.digest.files} digest(s) (${r.digest.absent} absent, ${r.digest.unreadable} unreadable)`,
304
+ );
305
+ const f = r.fates;
306
+ if (f.revised + f.withdrawn + f.reinstated > 0)
307
+ out.push(
308
+ `fate candidates re-read: ${f.revised} revised, ${f.withdrawn} withdrawn, ${f.reinstated} reinstated`,
309
+ );
310
+ if ((r.relinked ?? 0) > 0)
311
+ out.push(`feedback candidates re-linked to their finding: ${r.relinked}`);
312
+ for (const e of r.errors) out.push(`source error: ${oneLine(e)}`);
313
+ out.push("");
314
+ }
315
+ out.push(
316
+ opts.clusters.length === 0
317
+ ? "No undecided lesson candidates."
318
+ : "Undecided lesson candidates by class (offered / waiting for a packet / events in the recurrence window):",
319
+ );
320
+ for (const c of opts.clusters)
321
+ out.push(
322
+ ` ${c.class.padEnd(32)} ${c.polarity.padEnd(10)} ${String(c.open).padStart(4)} ${String(c.queued).padStart(4)} ${String(c.recurrence).padStart(5)}`,
323
+ );
324
+ const s = opts.status;
325
+ out.push(
326
+ "",
327
+ s.day === null
328
+ ? "EOD curation: no outcomes packet yet."
329
+ : `EOD curation ${s.day}: ${s.state.replace("_", " ")} — ${s.offered} offered, ${s.adopted} adopted, ${s.declined} declined, ${s.verifying} verifying, ${s.not_persisted} not persisted, ${s.expired_today} expired${
330
+ (s.unresolved ?? []).length > 0
331
+ ? `\nunresolved: ${(s.unresolved ?? []).map((u) => `${u.day} ${u.state.replace("_", " ")} (${u.count})`).join("; ")}`
332
+ : ""
333
+ }`,
334
+ s.balance
335
+ ? `lessons in force: look-for:dont-raise ${s.balance.look_for}:${s.balance.dont_raise}${s.balance.ratio === null ? "" : ` (${s.balance.ratio.toFixed(2)})`} as of ${s.balance.as_of}`
336
+ : "lessons in force: the daemon reads memory for the balance — see `qcr status`",
337
+ );
338
+ return out.join("\n");
339
+ }
340
+
341
+ /**
342
+ * `qcr lessons [mine]`: the learner's candidates per class and
343
+ * the latest EOD curation, from the state database. `mine` first runs the
344
+ * miner on the operator's word — the same deterministic mine the daemon runs
345
+ * before every EOD sweep (state.db, and read-only git in the base clone when
346
+ * there is one). It reads no memory and writes none.
347
+ */
348
+ export async function cmdLessons(rest: string[], json: boolean): Promise<number> {
349
+ const sub = parseLessonsSubcommand(rest);
350
+ if (sub?.kind === "shadow-report") return cmdShadowReport(sub.days, json);
351
+ if (sub?.kind === "switch") return cmdLessonSwitch(sub);
352
+ const { mine } = parseLessonsArgs(rest);
353
+ const cfg = loadConfig();
354
+ const { Learner, learnerKnobsOf } = await import("./learner/learner.js");
355
+ const { WorktreeManager } = await import("./worktrees.js");
356
+ const { githubCloneDir } = await import("./config.js");
357
+ const root = sandboxRoot();
358
+ const worktrees = new WorktreeManager({
359
+ root,
360
+ repo: cfg.github.repo,
361
+ cloneDir: githubCloneDir(cfg.github),
362
+ });
363
+ const store = openStore();
364
+ try {
365
+ const learner = new Learner({
366
+ store,
367
+ root,
368
+ eodTime: cfg.eod.time,
369
+ timeZone: cfg.eod.timezone,
370
+ // The bot ACCOUNT ("" in person mode, where the person's words are a human's).
371
+ botLogin: botAccountLogin(cfg.github),
372
+ baseBranch: cfg.github.base_branch,
373
+ config: learnerKnobsOf(cfg.memory),
374
+ ...(existsSync(worktrees.basePath) ? { git: worktrees } : {}),
375
+ });
376
+ const report = mine ? await learner.mine() : null;
377
+ const clusters = learner.clusters();
378
+ const status = learner.status();
379
+ console.log(
380
+ json
381
+ ? JSON.stringify({ report, clusters, status })
382
+ : renderLessons({ report, clusters, status }),
383
+ );
384
+ return report && report.errors.length > 0 ? 1 : 0;
385
+ } finally {
386
+ store.close();
387
+ }
388
+ }
389
+
390
+ /**
391
+ * `qcr lessons shadow-report [--days N]`: the lesson shadow
392
+ * ledger joined with what humans did — in total, per lesson and per lens, the
393
+ * findings a delivered dont-raise lesson would have touched that humans then
394
+ * FIXED (harm), refuted or set aside (help), or not yet, each split by match
395
+ * tier (`class`, the lesson's class matched too — the would-demote tier,
396
+ * what the validator's license reaches; `lens`, lens and path alone —
397
+ * evidence only); and the look-for lessons whose class later showed a
398
+ * human-raised miss. state.db only (opening it migrates a ledger without
399
+ * match tiers once).
400
+ */
401
+ async function cmdShadowReport(days: number, json: boolean): Promise<number> {
402
+ const cfg = loadConfig();
403
+ const { LessonLedger } = await import("./lessonledger.js");
404
+ const { renderShadowReport, shadowReport } = await import("./lessonshadow.js");
405
+ const store = openStore();
406
+ try {
407
+ const report = shadowReport(new LessonLedger(store), { days, mode: cfg.memory.delivery });
408
+ console.log(json ? JSON.stringify(report) : renderShadowReport(report));
409
+ return 0;
410
+ } finally {
411
+ store.close();
412
+ }
413
+ }
414
+
415
+ /**
416
+ * `qcr lessons off|on|suppress|unsuppress`: the operator's kill
417
+ * switch, written as root into the sealed commands/operator/ (the drain's
418
+ * channel) and confirmed by the daemon's own result file.
419
+ */
420
+ async function cmdLessonSwitch(
421
+ sub: Extract<LessonsSubcommand, { kind: "switch" }>,
422
+ ): Promise<number> {
423
+ const cfg = loadConfig();
424
+ const { runnerFor } = await import("./tui/remote.js");
425
+ const { LessonSwitchSender, lessonUriOf } = await import("./tui/lessonswitch.js");
426
+ const cmd: import("./tui/lessonswitch.js").LessonSwitchCommand =
427
+ "target" in sub
428
+ ? { command: sub.command, uri: lessonUriOf(sub.target, cfg.openviking.user) }
429
+ : { command: sub.command };
430
+ await resolveLayout(cfg.tui, runnerFor(cfg.tui));
431
+ console.log(
432
+ `${cmd.command}${"uri" in cmd ? ` ${cmd.uri}` : ""} → ${cfg.tui.ssh_host}:${remoteSandboxRoot(cfg.tui)}/commands/operator/`,
433
+ );
434
+ const res = await new LessonSwitchSender(cfg.tui, runnerFor(cfg.tui)).send(cmd);
435
+ if (!res.ok) {
436
+ console.log(`✖ refused: ${res.reason ?? "no reason given"}`);
437
+ if (res.issuer === "session")
438
+ console.log(
439
+ " the daemon found commands/operator/ UNSEALED (writable by its own uid), so it served the file as a session's — check the directory is root-owned 0755 in the container",
440
+ );
441
+ return 1;
442
+ }
443
+ console.log(
444
+ `✓ ${res.changed === false ? "already so" : "done"} — lessons ${res.lessons_off ? "OFF (kill switch)" : "on"}; ${res.suppressed ?? "?"} suppressed`,
445
+ );
446
+ return 0;
447
+ }
448
+
449
+ /** Foreign free text (a PR title, a session's pause reason) headed for the
450
+ * operator's terminal: one line, no C0/C1 control bytes — the board's
451
+ * sanitizeText does the same, so an ESC/CSI in a title cannot drive the
452
+ * terminal. */
453
+ function oneLine(s: string): string {
454
+ return s.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
455
+ }
456
+
457
+ /** A session-written ledger (the archived copy) headed for the operator's
458
+ * terminal: its lines kept, its control bytes gone — the board's LEDGER
459
+ * section reads it the same way. */
460
+ export function archivedLedgerText(raw: string): string {
461
+ return sanitizeText(raw);
462
+ }
463
+
464
+ export function renderStatus(snapshot: StatusSnapshot): string {
465
+ const lines: string[] = [];
466
+ const mode = snapshot.dry_run ? "DRY-RUN" : "LIVE";
467
+ lines.push(`quickreview — ${mode} — snapshot ${snapshot.written_at}`);
468
+ if (snapshot.run_host?.warning) lines.push(`WARNING: ${oneLine(snapshot.run_host.warning)}`);
469
+ if (snapshot.digest_headline) lines.push(`digest: ${oneLine(snapshot.digest_headline)}`);
470
+ lines.push(
471
+ `rounds running: ${snapshot.running_rounds} total spend: $${snapshot.total_spend_usd.toFixed(2)}`,
472
+ );
473
+ // Whether reviewer memory works, every time — an absent block
474
+ // (older daemon) and an unmeasured one say so, never "healthy".
475
+ lines.push(oneLine(memoryStatusLine(snapshot.memory, new Date(snapshot.written_at))));
476
+ // The daemon's own event loop and the container's CPU
477
+ // throttling, every time — an absent block (older daemon) and an unmeasured
478
+ // one say so, never "ok".
479
+ lines.push(oneLine(daemonLoadLine(snapshot.daemon)));
480
+ // The closed-PR archiver, every time — an absent block (older
481
+ // daemon) says so, and a backlog nobody planned yet is UNKNOWN, never 0.
482
+ lines.push(oneLine(archiveStatusLine(snapshot.archive)));
483
+ if (snapshot.draining) {
484
+ lines.push(
485
+ `DRAINING${snapshot.draining_since ? ` since ${snapshot.draining_since}` : ""} — ${drainHolderLine(snapshot)} — ` +
486
+ `webhook deliveries dropped: ${snapshot.drained_deliveries ?? 0}; ` +
487
+ `deploy when rounds running hits 0 (${drainLiftLine(snapshot)})`,
488
+ );
489
+ }
490
+ lines.push("");
491
+ if (snapshot.prs.length === 0) {
492
+ lines.push("no tracked PRs");
493
+ } else {
494
+ lines.push("PR state stale head title");
495
+ for (const pr of snapshot.prs) {
496
+ lines.push(
497
+ `#${String(pr.number).padEnd(5)} ${pr.state.padEnd(17)} ${(pr.stale_recheck ? "yes" : "-").padEnd(6)} ${pr.head_sha.slice(0, 10).padEnd(11)} ${oneLine(pr.title).slice(0, 60)}`,
498
+ );
499
+ }
500
+ // Every held PR says who holds it and when the hold lapses.
501
+ const held = snapshot.prs.filter((pr) => pr.paused);
502
+ if (held.length > 0) {
503
+ lines.push("");
504
+ for (const pr of held) {
505
+ const hold = pr.pause;
506
+ const who = hold
507
+ ? hold.issuer === "operator"
508
+ ? "paused by the operator until resumed"
509
+ : `paused by a session${hold.expires_at ? `, lapses ${hold.expires_at}` : ""}`
510
+ : "paused (holder unknown — older daemon)";
511
+ // The reason is a SESSION's free text headed for a terminal.
512
+ const reason = hold?.reason ? oneLine(hold.reason) : undefined;
513
+ lines.push(`#${pr.number} ${who}${reason ? ` — ${reason}` : ""}`);
514
+ }
515
+ }
516
+ }
517
+ return lines.join("\n");
518
+ }
519
+
520
+ function drainHolderLine(snapshot: StatusSnapshot): string {
521
+ if (snapshot.draining_by === undefined) return "holder unknown (older daemon)";
522
+ if (snapshot.draining_by === "operator") {
523
+ if (snapshot.draining_hold === true) return "HELD by the operator (hold: no boot lifts it)";
524
+ return snapshot.draining_until
525
+ ? `held by the operator, lapses ${snapshot.draining_until}`
526
+ : "held by the operator until lifted";
527
+ }
528
+ return `held by a SESSION, lapses ${snapshot.draining_until ?? "(no expiry recorded)"}`;
529
+ }
530
+
531
+ /** What lifts the drain that stands. A snapshot with no hold marker comes from
532
+ * a daemon that does not report holds: whether its boot lifts it is unknown. */
533
+ function drainLiftLine(snapshot: StatusSnapshot): string {
534
+ if (snapshot.draining_hold === true || snapshot.draining_reason)
535
+ return "only qcr drain off lifts it — no boot does";
536
+ if (snapshot.draining_hold === undefined)
537
+ return "hold not reported (older daemon); qcr drain off lifts it, a healthy boot may";
538
+ return "auto-lifts on the next boot's first healthy reconcile; qcr drain off to lift now";
539
+ }
540
+
541
+ function cmdStatus(): void {
542
+ const path = join(stateDir(), "status.json");
543
+ if (!existsSync(path)) {
544
+ console.log("no status.json yet — is the daemon running?");
545
+ return;
546
+ }
547
+ console.log(renderStatus(JSON.parse(readFileSync(path, "utf8")) as StatusSnapshot));
548
+ }
549
+
550
+ function cmdSpend(): void {
551
+ const store = openStore();
552
+ try {
553
+ const total = store.totalSpend();
554
+ const today = new Date().toISOString().slice(0, 10);
555
+ // est_usd is the one spend field; expired rounds count (`roundUsage`).
556
+ const todayRounds = store.roundsOn(today);
557
+ const day = roundUsage(todayRounds);
558
+ const totals = store.db
559
+ .prepare(
560
+ "SELECT COALESCE(SUM(tokens_in), 0) AS tin, COALESCE(SUM(tokens_out), 0) AS tout FROM rounds",
561
+ )
562
+ .get() as { tin: number; tout: number };
563
+ console.log(
564
+ `total spend: $${total.toFixed(2)} (est) — ${totals.tin} in / ${totals.tout} out tokens`,
565
+ );
566
+ console.log(
567
+ `today (${today}): $${day.spend_usd.toFixed(2)} across ${todayRounds.length} rounds — ${day.tokens_in} in / ${day.tokens_out} out tokens`,
568
+ );
569
+ } finally {
570
+ store.close();
571
+ }
572
+ }
573
+
574
+ function cmdToday(): void {
575
+ const store = openStore();
576
+ try {
577
+ const today = new Date().toISOString().slice(0, 10);
578
+ const rounds = store.roundsOn(today);
579
+ console.log(`rounds on ${today}: ${rounds.length}`);
580
+ for (const r of rounds) {
581
+ const tokens =
582
+ r.tokens_in !== null || r.tokens_out !== null
583
+ ? ` tok=${r.tokens_in ?? 0}/${r.tokens_out ?? 0}`
584
+ : "";
585
+ // A cancelled round says why inline — its status alone does not.
586
+ const why = r.status === "cancelled" ? ` cause=${cancelCauseLabel(r.cancel_cause)}` : "";
587
+ console.log(
588
+ `#${r.pr} round ${r.round_no} (${r.round_type}) [${r.status}]${why} verdict=${r.verdict ?? "—"} spend=$${(r.est_usd ?? 0).toFixed(2)}${tokens} lenses=${r.lenses}`,
589
+ );
590
+ }
591
+ console.log(todayCancellationLine(cancellationsOn(store, today)));
592
+ const events = store.eventsOn(today);
593
+ console.log(`events: ${events.length}`);
594
+ for (const e of events.slice(-40)) console.log(` ${e.ts} ${e.kind}${e.pr ? ` #${e.pr}` : ""}`);
595
+ } finally {
596
+ store.close();
597
+ }
598
+ }
599
+
600
+ // ---- machine-readable payloads (the TUI's detail-on-demand surface) -------
601
+
602
+ export interface WhyPayload {
603
+ pr: number;
604
+ known: boolean;
605
+ state?: string;
606
+ title?: string;
607
+ head_sha?: string;
608
+ author?: string;
609
+ trigger_ts?: string | null;
610
+ stale_recheck?: boolean;
611
+ updated_at?: string;
612
+ spend_usd: number;
613
+ rounds: Array<{
614
+ round_no: number;
615
+ round_type: string;
616
+ status: string;
617
+ verdict: string | null;
618
+ started_at: string;
619
+ deadline_at?: string;
620
+ finished_at: string | null;
621
+ spend_usd: number | null;
622
+ est_usd?: number | null;
623
+ tokens_in?: number | null;
624
+ tokens_out?: number | null;
625
+ lenses: string;
626
+ validate_tier?: string | null;
627
+ executed_passes?: unknown[] | null;
628
+ summary?: string | null;
629
+ cancel_cause?: string | null;
630
+ }>;
631
+ /** `open` is null: resolution is GitHub's state and the daemon does not
632
+ * mirror it — unknown, never the total dressed as a count. */
633
+ threads: { open: number | null; total: number };
634
+ /** The findings the validator doubted — kept, never posted, shown
635
+ * in the TUI detail pane. Absent from an older daemon (never read as none). */
636
+ doubted?: DoubtedFinding[];
637
+ /** The survivors the profile's `[posting]` policy withheld: kept, never
638
+ * posted. Same shape; `word` is the posting-policy word. Absent from an
639
+ * older daemon. */
640
+ withheld?: DoubtedFinding[];
641
+ ledger?: string;
642
+ archive?: string[];
643
+ /** The PR's archive manifest in brief — what the archiver
644
+ * holds for it and what left the live tree. Absent when there is no
645
+ * manifest (nothing archived, or an older daemon); `unreadable` with why
646
+ * when there is one that cannot be read — never read as "nothing". */
647
+ archive_manifest?: WhyArchiveManifest;
648
+ /** GitHub conversation surface — present only when the enrichment fetch
649
+ * succeeded (needs the app credential; degrades silently without it). */
650
+ description?: string;
651
+ labels?: string[];
652
+ comments?: Array<{ author: string; body: string; created_at: string }>;
653
+ }
654
+
655
+ /** `why --json`'s archive manifest summary. */
656
+ export type WhyArchiveManifest =
657
+ ({ state: "read" } & ManifestSummary) | { state: "unreadable"; error: string };
658
+
659
+ /** The PR's archive manifest in brief, read in this process (synchronous:
660
+ * `qcr why` is a CLI, never the daemon). Undefined when there is none. */
661
+ export function archiveManifestSummary(archiveDir: string): WhyArchiveManifest | undefined {
662
+ const path = join(archiveDir, "manifest.json");
663
+ let text: string;
664
+ try {
665
+ text = readFileSync(path, "utf8");
666
+ } catch (err) {
667
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined;
668
+ return { state: "unreadable", error: (err as Error).message };
669
+ }
670
+ try {
671
+ return { state: "read", ...summarizeManifest(parseManifestText(text, path)) };
672
+ } catch (err) {
673
+ if (err instanceof ManifestUnreadable) return { state: "unreadable", error: err.message };
674
+ throw err;
675
+ }
676
+ }
677
+
678
+ /** `qcr why`'s archive manifest line. */
679
+ export function archiveManifestLine(m: WhyArchiveManifest): string {
680
+ if (m.state === "unreadable") return `archive manifest: UNREADABLE (${oneLine(m.error)})`;
681
+ return (
682
+ `archive manifest: ${m.passes} pass(es), last ${m.last_pass_at ?? "unknown"}; ` +
683
+ `${m.transcripts} transcript(s), ${m.files} large file(s), ${m.bundles} bundle(s) of ${m.records} record(s) — ` +
684
+ `${fmtBytes(m.bytes)} → ${fmtBytes(m.gz_bytes)}; ${m.sources_removed} live source(s) removed` +
685
+ (m.duplicates_removed > 0 ? `, ${m.duplicates_removed} legacy duplicate(s) removed` : "") +
686
+ (m.restores > 0 ? `; ${m.restores} restore(s)` : "")
687
+ );
688
+ }
689
+
690
+ /** One doubted finding as `why --json` carries it. */
691
+ export interface DoubtedFinding {
692
+ round_no: number;
693
+ lens: string;
694
+ path: string | null;
695
+ line: number | null;
696
+ severity: string | null;
697
+ /** The validity veto word: refuted, unverifiable or bad-anchor. */
698
+ word: string | null;
699
+ reason: string | null;
700
+ }
701
+
702
+ /** Best-effort GitHub enrichment for `why --json` (description, labels, last
703
+ * comments). Read-only; any failure leaves the payload un-enriched.
704
+ * `secretsDir` is an injection seam for tests — pointing it at an empty dir
705
+ * makes the credential lookup fail fast instead of hitting the real network
706
+ * when the developer machine carries the app credential. */
707
+ export async function enrichWhy(payload: WhyPayload, secretsDir?: string): Promise<WhyPayload> {
708
+ try {
709
+ const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
710
+ const { GitHubAdapter } = await import("./github.js");
711
+ const cfg = loadConfig();
712
+ const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr why");
713
+ const adapter = new GitHubAdapter(
714
+ cfg,
715
+ githubCredential({ ...cfg.github, secrets_dir: secretsDir ?? cfg.github.secrets_dir }),
716
+ gate,
717
+ );
718
+ const convo = await adapter.prConversation(payload.pr, 3);
719
+ return { ...payload, description: convo.body, labels: convo.labels, comments: convo.comments };
720
+ } catch {
721
+ return payload;
722
+ }
723
+ }
724
+
725
+ /** A kept-not-posted finding (doubted or withheld) as `why --json` carries it. */
726
+ function keptFinding(f: DoubtedFinding): DoubtedFinding {
727
+ return {
728
+ round_no: f.round_no,
729
+ lens: f.lens,
730
+ path: f.path,
731
+ line: f.line,
732
+ severity: f.severity,
733
+ word: f.word,
734
+ reason: f.reason,
735
+ };
736
+ }
737
+
738
+ export function whyJson(store: StateStore, n: number, archiveDir: string): WhyPayload {
739
+ const pr = store.getPr(n);
740
+ const rounds = store.roundsFor(n);
741
+ const threadTotal = store.db
742
+ .prepare("SELECT COUNT(*) AS c FROM bot_comments WHERE pr = ?")
743
+ .get(n) as { c: number };
744
+ const payload: WhyPayload = {
745
+ pr: n,
746
+ known: pr !== undefined || rounds.length > 0,
747
+ // Every round's cost, expired ones included.
748
+ spend_usd: roundUsage(rounds).spend_usd,
749
+ rounds: rounds.map((r) => ({
750
+ round_no: r.round_no,
751
+ round_type: r.round_type,
752
+ status: r.status,
753
+ verdict: r.verdict ?? null,
754
+ started_at: r.started_at,
755
+ deadline_at: r.deadline_at,
756
+ finished_at: r.finished_at ?? null,
757
+ spend_usd: r.spend_usd ?? null,
758
+ est_usd: r.est_usd ?? null,
759
+ tokens_in: r.tokens_in ?? null,
760
+ tokens_out: r.tokens_out ?? null,
761
+ lenses: r.lenses,
762
+ validate_tier: r.validate_tier ?? null,
763
+ executed_passes: parseExecutedPasses(r.executed_passes),
764
+ summary: r.summary ?? null,
765
+ // Why a cancelled round was cancelled — null on every other
766
+ // status. The TUI's detail pane reads this beside the prose summary.
767
+ cancel_cause: r.cancel_cause ?? null,
768
+ })),
769
+ // The registry knows every bot thread; "open" needs GitHub truth the
770
+ // daemon doesn't mirror, so it is UNKNOWN (null) — never the total.
771
+ threads: { open: null, total: threadTotal.c },
772
+ doubted: store.doubtedFindings(n).map(keptFinding),
773
+ withheld: store.withheldFindings(n).map(keptFinding),
774
+ };
775
+ if (pr) {
776
+ payload.state = pr.state;
777
+ payload.title = pr.title;
778
+ payload.head_sha = pr.head_sha;
779
+ payload.author = pr.author;
780
+ payload.trigger_ts = pr.trigger_ts;
781
+ payload.stale_recheck = Boolean(pr.stale_recheck);
782
+ payload.updated_at = pr.updated_at;
783
+ }
784
+ if (existsSync(archiveDir)) {
785
+ payload.archive = readdirSync(archiveDir);
786
+ const ledger = join(archiveDir, "ledger.md");
787
+ if (existsSync(ledger)) payload.ledger = readFileSync(ledger, "utf8");
788
+ const manifest = archiveManifestSummary(archiveDir);
789
+ if (manifest) payload.archive_manifest = manifest;
790
+ }
791
+ return payload;
792
+ }
793
+
794
+ export function todayJson(
795
+ store: StateStore,
796
+ today: string,
797
+ ): {
798
+ date: string;
799
+ rounds: unknown[];
800
+ spend_usd: number;
801
+ tokens_in: number;
802
+ tokens_out: number;
803
+ events: unknown[];
804
+ cancellations: CancellationTally[];
805
+ } {
806
+ const rounds = store.roundsOn(today);
807
+ return {
808
+ date: today,
809
+ rounds,
810
+ ...roundUsage(rounds),
811
+ events: store.eventsOn(today),
812
+ cancellations: cancellationsOn(store, today),
813
+ };
814
+ }
815
+
816
+ /** Today's cancellations grouped by cause — the same tally the
817
+ * metrics payload carries, scoped to one timezone-agnostic ISO date so `qcr
818
+ * today` can print it beside the rounds it explains. */
819
+ export function cancellationsOn(store: StateStore, date: string): CancellationTally[] {
820
+ return tallyCancellations(store.roundsOn(date));
821
+ }
822
+
823
+ /** `qcr today`'s one-line cancellation summary. Never silent: "none" is an
824
+ * answer. */
825
+ export function todayCancellationLine(tallies: CancellationTally[]): string {
826
+ return cancellationSummaryLine("cancelled", tallies);
827
+ }
828
+
829
+ /** `qcr why`'s first line; the title is the author's text, so one line. */
830
+ export function whyHeaderLine(
831
+ n: number,
832
+ pr: { state: string; head_sha: string; stale_recheck: number; title: string },
833
+ ): string {
834
+ return `#${n} state=${pr.state} head=${pr.head_sha.slice(0, 10)} stale=${pr.stale_recheck} "${oneLine(pr.title)}"`;
835
+ }
836
+
837
+ function cmdWhy(n: number): void {
838
+ const archiveDir = join(sandboxRoot(), "archive", `pr-${n}`);
839
+ const store = openStore();
840
+ try {
841
+ const pr = store.getPr(n);
842
+ if (pr) console.log(whyHeaderLine(n, pr));
843
+ const rounds = store.roundsFor(n);
844
+ for (const r of rounds)
845
+ console.log(
846
+ `round ${r.round_no} (${r.round_type}) [${r.status}] verdict=${r.verdict ?? "—"} started=${r.started_at}`,
847
+ );
848
+ if (existsSync(archiveDir)) {
849
+ console.log(`archive: ${archiveDir}`);
850
+ for (const f of readdirSync(archiveDir)) console.log(` ${f}`);
851
+ const manifest = archiveManifestSummary(archiveDir);
852
+ if (manifest) console.log(archiveManifestLine(manifest));
853
+ const ledger = join(archiveDir, "ledger.md");
854
+ if (existsSync(ledger)) {
855
+ console.log("--- archived ledger ---");
856
+ console.log(archivedLedgerText(readFileSync(ledger, "utf8")));
857
+ }
858
+ } else if (!pr && rounds.length === 0) {
859
+ console.log(`nothing known about #${n}`);
860
+ }
861
+ } finally {
862
+ store.close();
863
+ }
864
+ }
865
+
866
+ /** `qcr metrics [--pr <n>] [--json]` — the aggregation engine over the live
867
+ * sqlite (read-only, container-safe: same store/config opening as `why`). */
868
+ async function cmdMetrics(pr: number | null, json: boolean): Promise<void> {
869
+ const store = openStore();
870
+ try {
871
+ if (pr !== null) {
872
+ const payload = computeTimeline(store, pr);
873
+ console.log(json ? JSON.stringify(payload) : renderTimeline(payload));
874
+ return;
875
+ }
876
+ // Day buckets follow the ops timezone. `qcr metrics` must stay
877
+ // usable beside a bare state.db, so an unreadable config degrades to UTC
878
+ // rather than refusing.
879
+ let timeZone = "UTC";
880
+ try {
881
+ timeZone = loadConfig().ops.timezone;
882
+ } catch {
883
+ /* config genuinely unavailable — UTC day buckets */
884
+ }
885
+ const payload = computeMetrics(store, { timeZone });
886
+ // The operational half is file-derived: status.json (heartbeat/dry-run/
887
+ // concurrency) + the BUILD_SHA marker deploy.sh writes into the sandbox
888
+ // root. parseBuildSha is deploytab's — the marker format has one parser.
889
+ const { parseBuildSha } = await import("./tui/deploytab.js");
890
+ payload.ops = readOps(
891
+ {
892
+ statusPath: join(stateDir(), "status.json"),
893
+ buildShaPath: join(sandboxRoot(), "BUILD_SHA"),
894
+ },
895
+ { readFile: (p) => readFileSync(p, "utf8"), parseBuildSha },
896
+ payload.ops,
897
+ );
898
+ console.log(json ? JSON.stringify(payload) : renderMetrics(payload));
899
+ } finally {
900
+ store.close();
901
+ }
902
+ }
903
+
904
+ const BACKFILL_USAGE = "usage: qcr backfill [--dry-run] [--limit <n>] [--json]";
905
+
906
+ /** `qcr backfill`'s arguments (`--json` is taken off before). An unknown or
907
+ * repeated token is refused: a mistyped `--dry-run` must not run the import. */
908
+ export function parseBackfillArgs(rest: readonly string[]): {
909
+ dryRun: boolean;
910
+ limit: number | undefined;
911
+ } {
912
+ let dryRun = false;
913
+ let limit: number | undefined;
914
+ for (let i = 0; i < rest.length; i += 1) {
915
+ const a = rest[i];
916
+ if (a === "--dry-run" && !dryRun) dryRun = true;
917
+ else if (a === "--limit" && limit === undefined && rest[i + 1] !== undefined) {
918
+ limit = Number(rest[i + 1]);
919
+ i += 1;
920
+ if (!Number.isInteger(limit) || limit < 0) throw new Error(BACKFILL_USAGE);
921
+ } else throw new Error(BACKFILL_USAGE);
922
+ }
923
+ return { dryRun, limit };
924
+ }
925
+
926
+ /** `qcr backfill [--dry-run] [--limit N]` — the one-shot pre-daemon history
927
+ * import. Read-only against GitHub: it opens the adapter purely for
928
+ * its search/GraphQL reads and never reaches a mutation surface. */
929
+ export async function cmdBackfill(
930
+ dryRun: boolean,
931
+ limit: number | undefined,
932
+ json: boolean,
933
+ ): Promise<void> {
934
+ const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
935
+ const { GitHubAdapter } = await import("./github.js");
936
+ const { runBackfill } = await import("./backfill.js");
937
+ const cfg = loadConfig();
938
+ // The import reads "reviews by the bot's login" as the bot's history. Under
939
+ // the person's own credential that login is the person's, and their own
940
+ // reviews are not the bot's: there is nothing this importer can tell apart.
941
+ if (isPersonMode(cfg.github))
942
+ throw new Error(
943
+ 'qcr backfill imports a GitHub App\'s review history; under [github] auth = "user" the login is ' +
944
+ "your own, so your reviews and the bot's cannot be told apart by it — nothing to import",
945
+ );
946
+ // A gate that can only ever record: the importer performs no mutations, and
947
+ // this keeps that structural rather than prompted.
948
+ const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr backfill");
949
+ const adapter = new GitHubAdapter(cfg, githubCredential(cfg.github), gate);
950
+ const store = openStore();
951
+ try {
952
+ const stats = await runBackfill(store, adapter, cfg, {
953
+ dryRun,
954
+ ...(limit !== undefined ? { limit } : {}),
955
+ log: (line) => process.stderr.write(`${line}\n`),
956
+ });
957
+ if (json) console.log(JSON.stringify(stats));
958
+ } finally {
959
+ store.close();
960
+ }
961
+ }
962
+
963
+ /** `--months N` / `--limit N`, or undefined; a malformed value is an error. */
964
+ function intFlag(rest: string[], flag: string, usage: string): number | undefined {
965
+ const i = rest.indexOf(flag);
966
+ if (i < 0) return undefined;
967
+ const n = Number(rest[i + 1]);
968
+ if (!Number.isInteger(n) || n < 0) throw new Error(usage);
969
+ return n;
970
+ }
971
+
972
+ const HISTORY_USAGE =
973
+ "usage: qcr history <digest [--months <n>] [--limit <n>] | pull [--months <n>]>";
974
+
975
+ /**
976
+ * `qcr history` — the review history digest setup reads.
977
+ *
978
+ * `digest` runs WHERE THE APP CREDENTIAL IS (in the VM container) and prints
979
+ * the digest to stdout, writing nothing: read-only against GitHub, built with
980
+ * the same record-only gate `qcr backfill` builds, so no mutation is reachable
981
+ * from it even by mistake. `pull` runs on the PC: it runs `digest` over the
982
+ * TUI's route and writes the result into THIS install's state home — the only
983
+ * place the raw comments ever land.
984
+ */
985
+ export async function cmdHistory(rest: string[]): Promise<number> {
986
+ const sub = rest[0];
987
+ const months = intFlag(rest, "--months", HISTORY_USAGE);
988
+ if (sub === "digest") {
989
+ const limit = intFlag(rest, "--limit", HISTORY_USAGE);
990
+ const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
991
+ const { GitHubAdapter } = await import("./github.js");
992
+ const { collectReviewHistory } = await import("./reviewhistory.js");
993
+ const cfg = loadConfig();
994
+ const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr history");
995
+ const adapter = new GitHubAdapter(cfg, githubCredential(cfg.github), gate);
996
+ const store = openStore();
997
+ try {
998
+ const digest = await collectReviewHistory(adapter, store, cfg, {
999
+ ...(months !== undefined ? { months } : {}),
1000
+ ...(limit !== undefined ? { limit } : {}),
1001
+ log: (line) => process.stderr.write(`${line}\n`),
1002
+ });
1003
+ process.stdout.write(`${JSON.stringify(digest)}\n`);
1004
+ return 0;
1005
+ } finally {
1006
+ store.close();
1007
+ }
1008
+ }
1009
+ if (sub === "pull") {
1010
+ const { runnerFor } = await import("./tui/remote.js");
1011
+ const { pullReviewHistory } = await import("./tui/historypull.js");
1012
+ const { stateHome } = await import("./install.js");
1013
+ const cfg = loadConfig();
1014
+ const r = await pullReviewHistory({
1015
+ tui: cfg.tui,
1016
+ ssh: runnerFor(cfg.tui),
1017
+ stateHome: stateHome(),
1018
+ repo: cfg.github.repo,
1019
+ ...(months !== undefined ? { months } : {}),
1020
+ log: (line) => console.log(line),
1021
+ });
1022
+ if (!r.ok) {
1023
+ console.log(`✖ ${r.reason}`);
1024
+ return 1;
1025
+ }
1026
+ return 0;
1027
+ }
1028
+ throw new Error(HISTORY_USAGE);
1029
+ }
1030
+
1031
+ /**
1032
+ * `qcr regression` — run the human-review case set against the
1033
+ * reviewer and write the investigation.
1034
+ *
1035
+ * The gate is built HERE, exactly as `cmdBackfill` builds its own: a dry-run
1036
+ * gate whose executor throws. That is not a claim copied out of `config.toml`
1037
+ * — `runRegression` refuses to start unless the object it is handed reports
1038
+ * dry-run, and this is the only gate in the process. `src/replay.ts` holds no
1039
+ * adapter and no mutation surface at all, so there is nothing for a gate to
1040
+ * front even if one were live.
1041
+ */
1042
+ export async function cmdRegression(rest: string[], json: boolean): Promise<number> {
1043
+ const { nodeReplayIo, parseRegressionArgs, readCaseSet, runRegressionCli, ScratchReplay } =
1044
+ await import("./replay.js");
1045
+ const root = sandboxRoot();
1046
+ // The replay plans rounds with `buildPassPlan`, so it must see the
1047
+ // profile's overlays exactly as the daemon does (wire.ts buildParts, the
1048
+ // same function) — otherwise a lens the profile switched off would be
1049
+ // replayed.
1050
+ const { applyProfileOverlays } = await import("@quickchatai/pi-quickreview/src/profiles.js");
1051
+ const cfg = applyProfileOverlays(root, loadConfig(), homeProfileLookup());
1052
+ const args = parseRegressionArgs(rest, json);
1053
+ const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr regression");
1054
+ const scratchRoot = join(stateDir(), "regression");
1055
+ return runRegressionCli(args, {
1056
+ loadCaseSet: (p) => readCaseSet(p ?? join(root, "tests", "fixtures", "regression-cases.json")),
1057
+ gate,
1058
+ makeReplay: async (g) => {
1059
+ const io = await nodeReplayIo({ cfg, sandboxRoot: root, scratchRoot });
1060
+ return new ScratchReplay({
1061
+ gate: g,
1062
+ cfg,
1063
+ sandboxRoot: root,
1064
+ scratchRoot,
1065
+ io,
1066
+ ...(args.clonePath !== undefined ? { baseClone: args.clonePath } : {}),
1067
+ log: (l) => console.log(l),
1068
+ }).replay;
1069
+ },
1070
+ reportPath: (ranAt) => join(root, "reports", `regression-${ranAt.replace(/[:.]/g, "-")}.md`),
1071
+ writeReport: (p, body) => {
1072
+ mkdirSync(join(p, ".."), { recursive: true });
1073
+ writeFileSync(p, body);
1074
+ },
1075
+ log: (line) => console.log(line),
1076
+ });
1077
+ }
1078
+
1079
+ // ---- deploy-window drain -----------------------------------------------------
1080
+ //
1081
+ // `qcr drain on|off` works as the TUI's unwedge does: ONE validated command
1082
+ // file written as root into the daemon's sealed commands/operator/ dir
1083
+ // (docker exec over ssh, payload on stdin, tmp+mv), then the daemon's own
1084
+ // result file read back. src/commands.ts re-validates against the same
1085
+ // whitelist and the same tier gate; nothing here touches GitHub or goes near
1086
+ // the MutationGate. `--local` writes the file into THIS machine's sandbox
1087
+ // commands/operator/ dir instead, as the current user (for running inside the
1088
+ // container or against a local dev daemon) — sealed only if that dir is
1089
+ // root-owned; otherwise the daemon bounds it like a session's drain.
1090
+
1091
+ /** The documented manual fallback: the exact one-liner that writes the same
1092
+ * command file by hand when the runner path fails — over ssh, or straight
1093
+ * into local docker when `[tui] target = "local"`, the transport the runner
1094
+ * path itself used. */
1095
+ export function drainManualCommand(tui: TuiConfig, on: boolean, now: Date, hold = false): string {
1096
+ // Same root-owned operator directory the runner path writes: a
1097
+ // fallback that wrote into the session dir would be a bounded session drain.
1098
+ const docker = `${tui.sudo_docker ? "sudo " : ""}docker exec -i -u 0 ${tui.container}`;
1099
+ // A fresh name each time the fallback is printed, so the `off` after an
1100
+ // `on` (or the next deploy's `on`) never lands on a result file that already
1101
+ // holds an answer. The daemon's handled-ledger is keyed by file VERSION, so
1102
+ // a re-used name (an operator editing an old line) still executes — the
1103
+ // fresh name is what keeps its result readable.
1104
+ const name = `${now.toISOString().replace(/[:.]/g, "-")}-manual-drain.json`;
1105
+ // drainPayload's own object, compacted onto the one line.
1106
+ const payload = `printf '%s' '${JSON.stringify(JSON.parse(drainPayload(on, hold)))}' | `;
1107
+ // The script as the container's single `sh -c` argument.
1108
+ const script = shellQuote(operatorCommandWriteScript(tui, name));
1109
+ if (transportOf(tui) === "local") return `${payload}${docker} sh -c ${script}`;
1110
+ // Over ssh, also backslash, dollar, double quote and backtick for the LOCAL
1111
+ // double-quoted ssh argument, which would otherwise expand them on the
1112
+ // operator's machine.
1113
+ const remote = script.replace(/[\\$"`]/g, "\\$&");
1114
+ return `${payload}ssh ${tui.ssh_host} "${docker} sh -c ${remote}"`;
1115
+ }
1116
+
1117
+ export interface DrainRunDeps {
1118
+ cfg: Config;
1119
+ ssh: import("./tui/remote.js").SshRunner;
1120
+ log: (line: string) => void;
1121
+ now?: () => Date;
1122
+ sleep?: (ms: number) => Promise<void>;
1123
+ }
1124
+
1125
+ /** Write the drain command through the configured transport and wait for the
1126
+ * daemon's verdict. Returns a process exit code; every failure path prints
1127
+ * the manual fallback so the operator is never stuck behind this wrapper. */
1128
+ export async function runDrain(on: boolean, deps: DrainRunDeps, hold = false): Promise<number> {
1129
+ const tui = deps.cfg.tui;
1130
+ const where = transportOf(tui) === "local" ? `local docker ${tui.container}` : tui.ssh_host;
1131
+ // Say the directory the command will really be written to: the root follows the container's layout
1132
+ // label (the cutover recreates the container under a running operator), and an unresolved root is the
1133
+ // legacy one.
1134
+ await resolveLayout(tui, deps.ssh);
1135
+ deps.log(
1136
+ `drain ${on ? (hold ? "on --hold" : "on") : "off"} → ${where}:${remoteSandboxRoot(tui)}/commands/`,
1137
+ );
1138
+ const sender = new CommandDrainSender(tui, deps.ssh, deps.now, deps.sleep);
1139
+ const result = await sender.set(on, hold);
1140
+ if (result.ok && on && hold && result.hold !== true) {
1141
+ // The daemon took a drain but did not say it is a hold: a boot may lift it.
1142
+ deps.log(
1143
+ "✖ the drain is ON but the daemon did not confirm the HOLD — the next healthy boot may lift it; qcr drain off, then check the daemon's version",
1144
+ );
1145
+ return 1;
1146
+ }
1147
+ if (result.ok) {
1148
+ if (on) {
1149
+ deps.log(
1150
+ `✓ draining — ${result.running_rounds ?? 0} round(s) still finishing; deploy when \`qcr status\` shows rounds running: 0`,
1151
+ );
1152
+ if (result.issuer === "session") {
1153
+ // The daemon did not see the operator seal on commands/operator/ (the
1154
+ // directory was writable by its own uid), so it bounded this drain
1155
+ // like a session's. Saying so beats a drain that lifts mid-deploy.
1156
+ deps.log(
1157
+ ` ⚠ NOT an operator drain: the daemon found commands/operator/ unsealed and bounded it — lapses ${result.expires_at ?? "(unknown)"}`,
1158
+ );
1159
+ deps.log(
1160
+ " check the directory is root-owned and 0755 in the container before deploying",
1161
+ );
1162
+ }
1163
+ deps.log(
1164
+ hold
1165
+ ? " HOLD: no boot lifts it — only `qcr drain off` does"
1166
+ : " (auto-lifts on the new boot's first healthy reconcile — re-drain before each step of a multi-deploy window)",
1167
+ );
1168
+ } else {
1169
+ deps.log(
1170
+ `✓ drain lifted — ${result.dropped_deliveries ?? 0} webhook delivery(ies) were dropped; the reconcile poll recovers them`,
1171
+ );
1172
+ }
1173
+ return 0;
1174
+ }
1175
+ deps.log(`✖ drain refused: ${result.reason ?? "no reason given"}`);
1176
+ deps.log("manual fallback — run:");
1177
+ deps.log(` ${drainManualCommand(tui, on, (deps.now ?? (() => new Date()))(), hold)}`);
1178
+ return 1;
1179
+ }
1180
+
1181
+ /** `--local`: same command file, written straight into this machine's
1182
+ * sandbox commands/operator/ dir as the current user (in-container use; no
1183
+ * ssh). Fire-and-forget — the
1184
+ * local watcher's result lands in commands/results/. */
1185
+ function runDrainLocal(on: boolean, hold: boolean): number {
1186
+ const dir = join(sandboxRoot(), "commands", "operator");
1187
+ mkdirSync(dir, { recursive: true });
1188
+ const name = `${new Date().toISOString().replace(/[:.]/g, "-")}-cli-drain-${randomUUID()}.json`;
1189
+ writeFileSync(join(dir, `.${name}.tmp`), drainPayload(on, hold));
1190
+ renameSync(join(dir, `.${name}.tmp`), join(dir, name));
1191
+ console.log(
1192
+ `drain ${on ? (hold ? "on --hold" : "on") : "off"} command written to ${join(dir, name)} — result lands in commands/results/`,
1193
+ );
1194
+ console.log(
1195
+ "note: written as THIS user, so unless commands/operator/ is root-owned the daemon sees no operator seal and bounds the drain like a session's",
1196
+ );
1197
+ return 0;
1198
+ }
1199
+
1200
+ const DRAIN_USAGE = "usage: qcr drain <on [--hold]|off> [--local]";
1201
+
1202
+ /** `qcr drain`'s arguments: exactly one of `on`/`off`, optionally `--local`,
1203
+ * and `--hold` with `on` only (the operator's no-auto-lift drain).
1204
+ * Anything else is refused before a transport is picked or a command file
1205
+ * written — a mistyped `--local` must not send the drain over ssh instead. */
1206
+ export function parseDrainArgs(rest: readonly string[]): {
1207
+ on: boolean;
1208
+ local: boolean;
1209
+ hold: boolean;
1210
+ } {
1211
+ let sub: "on" | "off" | undefined;
1212
+ let local = false;
1213
+ let hold = false;
1214
+ for (const a of rest) {
1215
+ if ((a === "on" || a === "off") && sub === undefined) sub = a;
1216
+ else if (a === "--local" && !local) local = true;
1217
+ else if (a === "--hold" && !hold) hold = true;
1218
+ else throw new Error(DRAIN_USAGE);
1219
+ }
1220
+ if (sub === undefined) throw new Error(DRAIN_USAGE);
1221
+ if (hold && sub !== "on") throw new Error(DRAIN_USAGE);
1222
+ return { on: sub === "on", local, hold };
1223
+ }
1224
+
1225
+ async function cmdDrain(rest: string[]): Promise<number> {
1226
+ const { on, local, hold } = parseDrainArgs(rest);
1227
+ if (local) return runDrainLocal(on, hold);
1228
+ // Honour [tui] target — on the laptop topology the same command
1229
+ // file write runs against local docker instead of ssh.
1230
+ const { runnerFor } = await import("./tui/remote.js");
1231
+ const cfg = loadConfig();
1232
+ return runDrain(on, { cfg, ssh: runnerFor(cfg.tui), log: (l) => console.log(l) }, hold);
1233
+ }
1234
+
1235
+ // ---- the operator's per-PR pause ---------------------------------------------------
1236
+ //
1237
+ // `qcr pause-pr <n>` / `qcr resume-pr <n>`: the operator's `pause-pr` /
1238
+ // `resume-pr`, written as root into the sealed commands/operator/ (the drain's
1239
+ // channel) and confirmed by the daemon's own result file
1240
+ // (src/tui/prpause.ts). From that channel a pause carries no expiry: it stands
1241
+ // until `qcr resume-pr`, and no session lifts it. The cutover wizard's stage 9
1242
+ // pauses every tracked PR but one with it before the drain is lifted.
1243
+
1244
+ export const PR_LEVER_USAGE =
1245
+ "usage: qcr pause-pr <pr> [--reason <text>] | qcr resume-pr <pr> | qcr review-now <pr>";
1246
+
1247
+ /** `qcr pause-pr|resume-pr`'s arguments: one PR number, and for a pause an
1248
+ * optional `--reason <text>` (the reason the board and `qcr status` show). */
1249
+ export function parsePrLeverArgs(
1250
+ verb: "pause-pr" | "resume-pr" | "review-now",
1251
+ rest: readonly string[],
1252
+ ): import("./tui/prpause.js").PrLeverCommand {
1253
+ let pr: number | undefined;
1254
+ let reason: string | undefined;
1255
+ for (let i = 0; i < rest.length; i++) {
1256
+ const a = rest[i]!;
1257
+ if (a === "--reason" && verb === "pause-pr" && reason === undefined) {
1258
+ const v = rest[++i];
1259
+ if (v === undefined || v.trim() === "") throw new Error(PR_LEVER_USAGE);
1260
+ reason = v;
1261
+ } else if (/^\d+$/.test(a) && Number(a) > 0 && pr === undefined) pr = Number(a);
1262
+ else throw new Error(PR_LEVER_USAGE);
1263
+ }
1264
+ if (pr === undefined) throw new Error(PR_LEVER_USAGE);
1265
+ if (verb === "resume-pr") return { command: "resume-pr", pr };
1266
+ if (verb === "review-now") return { command: "review-now", pr };
1267
+ return { command: "pause-pr", pr, reason: reason ?? "paused by the operator (qcr pause-pr)" };
1268
+ }
1269
+
1270
+ async function cmdPrLever(
1271
+ verb: "pause-pr" | "resume-pr" | "review-now",
1272
+ rest: string[],
1273
+ ): Promise<number> {
1274
+ const cmd = parsePrLeverArgs(verb, rest);
1275
+ const cfg = loadConfig();
1276
+ const { runnerFor } = await import("./tui/remote.js");
1277
+ const { PrLeverSender, renderPrLever } = await import("./tui/prpause.js");
1278
+ const ssh = runnerFor(cfg.tui);
1279
+ await resolveLayout(cfg.tui, ssh);
1280
+ const where =
1281
+ transportOf(cfg.tui) === "local" ? `local docker ${cfg.tui.container}` : cfg.tui.ssh_host;
1282
+ console.log(`${verb} #${cmd.pr} → ${where}:${remoteSandboxRoot(cfg.tui)}/commands/operator/`);
1283
+ const { lines, code } = renderPrLever(cmd, await new PrLeverSender(cfg.tui, ssh).send(cmd));
1284
+ for (const l of lines) console.log(l);
1285
+ return code;
1286
+ }
1287
+
1288
+ // ---- takeover -----------------------------------------------------
1289
+ //
1290
+ // `qcr takeover <pr>`, `qcr debug-round <pr>` and `qcr release <pr>` are three
1291
+ // of the operator's control commands (src/commands.ts CONTROL_COMMANDS), sent down the
1292
+ // same root-sealed operator route the TUI's input line uses (src/tui/livecontrol.ts).
1293
+ // The daemon grants the lease and starts interactive pi on the PR's real session in tmux
1294
+ // `takeover-pr-<n>` (src/takeover.ts), or arms the PR's next round to run in tmux
1295
+ // `debug-pr-<n>` (src/debuground.ts); this verb only asks, and says how to attach.
1296
+
1297
+ export const TAKEOVER_USAGE = "usage: qcr <takeover|debug-round|release> <pr-number>";
1298
+
1299
+ /** `qcr takeover|release`'s arguments: exactly one PR number. */
1300
+ export function parseTakeoverArgs(rest: readonly string[]): number {
1301
+ if (rest.length !== 1 || !/^\d+$/.test(rest[0]!) || Number(rest[0]) <= 0)
1302
+ throw new Error(TAKEOVER_USAGE);
1303
+ return Number(rest[0]);
1304
+ }
1305
+
1306
+ /** The container name the attach hints print when no configured one is passed. */
1307
+ const DEFAULT_ATTACH_CONTAINER = "quickreview-sandbox";
1308
+
1309
+ /** The lines that follow a granted debug round: the lease is armed for the
1310
+ * PR's next round, which a worker would otherwise take. */
1311
+ export function debugRoundAttachLines(pr: number, container = DEFAULT_ATTACH_CONTAINER): string[] {
1312
+ const session = `debug-pr-${pr}`;
1313
+ return [
1314
+ `the PR's next round runs in interactive pi: tmux attach -t ${session}`,
1315
+ `(on the VM: docker exec -it ${container} tmux attach -t ${session})`,
1316
+ `it posts like any round, through the gate with the install's dry_run. \`qcr release ${pr}\` ends it; it ends with the verdict commit, and the round after goes back to a worker.`,
1317
+ ];
1318
+ }
1319
+
1320
+ /** The lines that follow a granted takeover: how to reach the pi. */
1321
+ export function takeoverAttachLines(pr: number, container = DEFAULT_ATTACH_CONTAINER): string[] {
1322
+ const session = `takeover-pr-${pr}`;
1323
+ return [
1324
+ `attach: tmux attach -t ${session}`,
1325
+ `(on the VM: docker exec -it ${container} tmux attach -t ${session})`,
1326
+ `the TUI's PR pane shows it too. \`qcr release ${pr}\` ends it; it also ends after 30 minutes idle.`,
1327
+ ];
1328
+ }
1329
+
1330
+ async function cmdTakeover(
1331
+ kind: "takeover" | "release" | "debug-round",
1332
+ rest: string[],
1333
+ ): Promise<number> {
1334
+ const pr = parseTakeoverArgs(rest);
1335
+ const cfg = loadConfig();
1336
+ const { runnerFor } = await import("./tui/remote.js");
1337
+ const { LiveControlSender } = await import("./tui/livecontrol.js");
1338
+ const outcome = await new LiveControlSender(cfg.tui, runnerFor(cfg.tui)).send(
1339
+ { kind, pr },
1340
+ (state, label) => {
1341
+ if (state !== "pending") console.log(label);
1342
+ },
1343
+ );
1344
+ const ok = outcome.state === "handled" || outcome.state === "queued";
1345
+ if (ok && kind === "takeover")
1346
+ for (const line of takeoverAttachLines(pr, cfg.tui.container)) console.log(line);
1347
+ if (ok && kind === "debug-round")
1348
+ for (const line of debugRoundAttachLines(pr, cfg.tui.container)) console.log(line);
1349
+ return ok ? 0 : 1;
1350
+ }
1351
+
1352
+ // ---- the operator's message to a PR ------------------------------------
1353
+
1354
+ const MESSAGE_USAGE = "usage: qcr message <pr> [--may-post] <text...>";
1355
+
1356
+ /** `qcr message`'s arguments: the PR number, then the words of the message, with
1357
+ * `--may-post` anywhere before them. `may_post` is set ONLY by that flag: a message
1358
+ * posts nothing unless the operator says so here (or presses Alt-p in the viewer). */
1359
+ export function parseMessageArgs(rest: readonly string[]): {
1360
+ pr: number;
1361
+ text: string;
1362
+ mayPost: boolean;
1363
+ } {
1364
+ let mayPost = false;
1365
+ const words: string[] = [];
1366
+ for (const a of rest) {
1367
+ if (a === "--may-post" && !mayPost && words.length <= 1) mayPost = true;
1368
+ else words.push(a);
1369
+ }
1370
+ const [prArg, ...text] = words;
1371
+ const pr = /^#?\d+$/.test(prArg ?? "") ? Number((prArg as string).replace("#", "")) : NaN;
1372
+ const body = text.join(" ").trim();
1373
+ if (!Number.isSafeInteger(pr) || pr <= 0 || body === "") throw new Error(MESSAGE_USAGE);
1374
+ return { pr, text: body, mayPost };
1375
+ }
1376
+
1377
+ /** `qcr message`: send the operator's message to a PR's session through the same
1378
+ * operator channel the viewer's input line uses, and say how the daemon took it. */
1379
+ export async function cmdMessage(
1380
+ rest: string[],
1381
+ deps: {
1382
+ sender?: Pick<import("./tui/livecontrol.js").ControlSender, "send">;
1383
+ log?: (line: string) => void;
1384
+ } = {},
1385
+ ): Promise<number> {
1386
+ const { pr, text, mayPost } = parseMessageArgs(rest);
1387
+ const log = deps.log ?? ((l: string) => console.log(l));
1388
+ let sender = deps.sender;
1389
+ if (!sender) {
1390
+ const { runnerFor } = await import("./tui/remote.js");
1391
+ const { LiveControlSender } = await import("./tui/livecontrol.js");
1392
+ const cfg = loadConfig();
1393
+ sender = new LiveControlSender(cfg.tui, runnerFor(cfg.tui));
1394
+ }
1395
+ const out = await sender.send(
1396
+ { kind: "message", pr, text, ...(mayPost ? { may_post: true } : {}) },
1397
+ (_state, label) => log(label),
1398
+ );
1399
+ return out.state === "queued" || out.state === "handled" ? 0 : 1;
1400
+ }
1401
+
1402
+ /**
1403
+ * `qcr install <where|migrate>`. `where` prints what the install
1404
+ * locator resolves; `migrate` moves the checkout's old config.toml into the
1405
+ * install's config home, and every install profile out of the
1406
+ * checkout's `profiles/` — idempotent, and it never overwrites a differing
1407
+ * config or profile. Deliberately NOT loadConfig(): a config that does not
1408
+ * parse must still be movable. Both verbs cover the secrets too —
1409
+ * `where` names the secrets dir and the companion key file the locator picks
1410
+ * (without the configured `[github] secrets_dir`, which only a parsed config
1411
+ * knows), and `migrate` moves `~/.config/quickreview-bot/` into
1412
+ * `<config home>/secrets/` — and repoints a compose `.env` whose
1413
+ * `QCR_SECRETS_DIR` still names it. Nothing else reads those old
1414
+ * places, so `where` lists whatever is still STRANDED there, and
1415
+ * `migrate` is the one command that opens them.
1416
+ */
1417
+ export function cmdInstall(
1418
+ rest: string[],
1419
+ deps: {
1420
+ root?: string;
1421
+ env?: InstallEnv & { QCR_OPENVIKING_KEYS?: string };
1422
+ log?: (line: string) => void;
1423
+ } = {},
1424
+ ): number {
1425
+ if (rest.length !== 1) throw new Error("usage: qcr install <where|migrate>");
1426
+ const root = deps.root ?? sandboxRoot();
1427
+ const env = deps.env ?? process.env;
1428
+ const log = deps.log ?? ((line: string) => console.log(line));
1429
+ switch (rest[0]) {
1430
+ case "where": {
1431
+ const located = locateConfig({ env });
1432
+ log(
1433
+ `config ${located.path} (${located.source}${existsSync(located.path) ? "" : ", missing"})`,
1434
+ );
1435
+ log(`config home ${located.home}`);
1436
+ log(`state home ${stateHome(env)}`);
1437
+ log(`state db ${join(operatorStateDir(root, env), "state.db")} (the operator verbs)`);
1438
+ log(
1439
+ `profiles ${profileSearchDirs({ root, env }).join(" → ")} (the checkout: generic examples only)`,
1440
+ );
1441
+ const secrets = locateSecrets({ env });
1442
+ const keys = locateKeysFile({ env });
1443
+ log(
1444
+ `secrets ${secrets.path} (${secrets.source}; a set [github] secrets_dir overrides it)`,
1445
+ );
1446
+ log(`memory keys ${keys.path} (${keys.source})`);
1447
+ // Nothing reads the old places, so an install that
1448
+ // has not migrated is told what it is stranding, and the way out.
1449
+ const stranded = [
1450
+ strandedCheckoutConfig({ root, env }),
1451
+ ...strandedCheckoutProfiles({ root, env }),
1452
+ strandedLegacySecrets(env),
1453
+ ].filter((p): p is string => p !== null);
1454
+ for (const path of stranded)
1455
+ log(
1456
+ `stranded ${path} — no longer read; \`qcr install migrate\` moves it into the homes`,
1457
+ );
1458
+ return 0;
1459
+ }
1460
+ case "migrate": {
1461
+ // The config, every install profile still in the checkout,
1462
+ // then the secrets — independent moves, so one refusal never
1463
+ // holds another back, and the exit code fails if any of them refused.
1464
+ const config = migrateReport(migrateConfig({ root, env }));
1465
+ const profiles = profileMigrateReport(migrateProfiles({ root, env }));
1466
+ const secrets = secretsMigrateReport(migrateSecrets({ env }));
1467
+ const compose = composeRepointReport(repointComposeSecrets({ root, env }));
1468
+ for (const line of [...config.lines, ...profiles.lines, ...secrets.lines, ...compose])
1469
+ log(line);
1470
+ return Math.max(config.code, profiles.code, secrets.code);
1471
+ }
1472
+ default:
1473
+ throw new Error("usage: qcr install <where|migrate>");
1474
+ }
1475
+ }
1476
+
1477
+ function cmdEvents(tail: number, json: boolean): void {
1478
+ const store = openStore();
1479
+ try {
1480
+ const events = store.recentEvents(tail);
1481
+ if (json) {
1482
+ console.log(JSON.stringify({ events }));
1483
+ return;
1484
+ }
1485
+ for (const e of events) console.log(`${e.ts} ${e.kind}${e.pr ? ` #${e.pr}` : ""}`);
1486
+ } finally {
1487
+ store.close();
1488
+ }
1489
+ }
1490
+
1491
+ /** What starting the companion needs from CompanionLifecycle. */
1492
+ interface CompanionStarter {
1493
+ ensureUp(log?: (line: string) => void): Promise<string>;
1494
+ spawnPi(): Promise<boolean>;
1495
+ }
1496
+
1497
+ /** Start the companion the one way `up` and `rebuild` both do: the token and
1498
+ * config mirror refreshed before the container starts, so it never mounts a
1499
+ * stale mirror or an expired token — then the container, then pi. */
1500
+ async function startCompanion(
1501
+ lc: CompanionStarter,
1502
+ refreshInputs: () => Promise<void>,
1503
+ log?: (line: string) => void,
1504
+ ): Promise<{ outcome: string; born: boolean }> {
1505
+ await refreshInputs();
1506
+ const outcome = await lc.ensureUp(log);
1507
+ return { outcome, born: await lc.spawnPi() };
1508
+ }
1509
+
1510
+ /** `qcr companion rebuild`: replace the container and image, then start the
1511
+ * new one exactly as `up` does. The pihome volume is untouched. To rebuild
1512
+ * the scan sandbox's image without touching the companion: `qcr scan
1513
+ * rebuild`. */
1514
+ export async function rebuildCompanion(
1515
+ lc: CompanionStarter & {
1516
+ checkBuildable(): void;
1517
+ removeContainer(): Promise<unknown>;
1518
+ buildImage(log: (line: string) => void): Promise<unknown>;
1519
+ },
1520
+ name: string,
1521
+ refreshInputs: () => Promise<void>,
1522
+ log: (line: string) => void,
1523
+ ): Promise<void> {
1524
+ // Refused before the running container goes: a rebuild that cannot build
1525
+ // must leave the companion as it was.
1526
+ lc.checkBuildable();
1527
+ await lc.removeContainer();
1528
+ await lc.buildImage(log);
1529
+ const { outcome, born } = await startCompanion(lc, refreshInputs, log);
1530
+ log(
1531
+ `companion ${name}: image rebuilt, ${outcome}; pi session ${born ? "spawned" : "already running"}`,
1532
+ );
1533
+ }
1534
+
1535
+ /** `qcr companion up|down|status|rebuild|plan` — thin over CompanionLifecycle. */
1536
+ export async function cmdCompanion(sub: string | undefined): Promise<void> {
1537
+ if (sub === "plan") {
1538
+ // For the host wrapper: the docker run it makes for the containerised TUI's
1539
+ // companion. Needs no config and runs no docker.
1540
+ const { companionPlan, renderPlan } = await import("./tui/companionplan.js");
1541
+ process.stdout.write(renderPlan(companionPlan(process.env)));
1542
+ return;
1543
+ }
1544
+ const { CompanionLifecycle, companionStateWord, realDockerRunner } =
1545
+ await import("./tui/companion.js");
1546
+ const cfg = loadConfig().companion;
1547
+ const lc = new CompanionLifecycle(cfg, realDockerRunner);
1548
+ // What every start publishes before pi spawns: the read-only GitHub token
1549
+ // and the config mirror. Both are one-shot — nothing refreshes them after
1550
+ // this command exits (the TUI does, while it runs).
1551
+ const refreshInputs = async (): Promise<void> => {
1552
+ const { CompanionGithubToken, tokenOutcomeLine } = await import("./tui/ghread.js");
1553
+ const { companionGithubDir } = await import("./install.js");
1554
+ const full = loadConfig();
1555
+ const gh = await new CompanionGithubToken({
1556
+ dir: companionGithubDir(),
1557
+ repo: full.github.repo,
1558
+ secretsDir: full.github.secrets_dir,
1559
+ auth: full.github.auth ?? "app",
1560
+ }).refresh();
1561
+ console.log(`companion ${cfg.container_name}: ${tokenOutcomeLine(gh)}`);
1562
+ const mirror = lc.syncConfigMirror();
1563
+ console.log(
1564
+ `companion ${cfg.container_name}: config mirror ${typeof mirror === "object" ? `NOT refreshed — ${mirror.failed}` : mirror}`,
1565
+ );
1566
+ };
1567
+ switch (sub) {
1568
+ case "up": {
1569
+ const { outcome, born } = await startCompanion(lc, refreshInputs);
1570
+ console.log(
1571
+ `companion ${cfg.container_name}: ${outcome}; pi session ${born ? "spawned" : "already running"}`,
1572
+ );
1573
+ console.log(`attach: docker ${lc.attachArgv().join(" ")}`);
1574
+ return;
1575
+ }
1576
+ case "down":
1577
+ await lc.gracefulStop();
1578
+ console.log(`companion ${cfg.container_name}: stopped (volumes and conversation kept)`);
1579
+ return;
1580
+ case "status": {
1581
+ const st = await lc.status();
1582
+ const pi = st.running ? await lc.piRunning() : false;
1583
+ console.log(
1584
+ `companion ${cfg.container_name}: ${companionStateWord(st)}` +
1585
+ `${st.running ? ` · pi session ${pi ? "live" : "down"}` : ""}`,
1586
+ );
1587
+ // Unknown is not an answer: a script reading the exit code must not
1588
+ // take "docker could not be asked" for "no container".
1589
+ if (st.unknown !== undefined) process.exitCode = 1;
1590
+ return;
1591
+ }
1592
+ case "rebuild": {
1593
+ // The image is otherwise built ONLY when it is missing, so an
1594
+ // entrypoint or Dockerfile change never reaches a machine that already
1595
+ // has one — it just keeps starting yesterday's image. This is the
1596
+ // explicit "apply what I changed" path. The pihome volume is untouched,
1597
+ // so OAuth and the conversation survive; only the container and image
1598
+ // are replaced.
1599
+ await rebuildCompanion(lc, cfg.container_name, refreshInputs, (line) => console.log(line));
1600
+ return;
1601
+ }
1602
+ default:
1603
+ throw new Error("usage: qcr companion <up|down|status|rebuild|plan>");
1604
+ }
1605
+ }
1606
+
1607
+ /**
1608
+ * `qcr memory reembed [--user <name>[,<name>…]]` — the operator's
1609
+ * half of the embedder migration, run from the PC that holds the keys.
1610
+ *
1611
+ * The daemon re-embeds ITS OWN namespace (the reviewer's) by itself once the
1612
+ * server is ready; it holds no other key and never will (the companion
1613
+ * boundary), so the companion's namespace — and any other provisioned user's — is
1614
+ * rebuilt from here, over the memory forward, with the keys in the operator's
1615
+ * keys file. Default: every provisioned user EXCEPT the daemon's own. The
1616
+ * reindex is an idempotent upsert, so re-running it (or naming the reviewer
1617
+ * too) is safe, only slow. Nothing but the request leaves this machine: no
1618
+ * key ever reaches an argv or the output.
1619
+ */
1620
+ export async function cmdMemory(rest: string[]): Promise<number> {
1621
+ const sub = rest[0];
1622
+ if (sub === "relayout") return cmdMemoryRelayout(rest.slice(1));
1623
+ if (sub !== "reembed") throw new Error(MEMORY_USAGE);
1624
+ const named = parseReembedArgs(rest.slice(1));
1625
+ const cfg = loadConfig();
1626
+ const own = cfg.openviking.user;
1627
+ const users = named ?? cfg.openviking.provision_users.filter((u) => u !== own);
1628
+ if (users.length === 0)
1629
+ throw new Error(
1630
+ "usage: qcr memory reembed [--user <name>[,<name>…]] — no namespace to re-embed",
1631
+ );
1632
+ const [
1633
+ { loadReembedModule, reembedNamespaces },
1634
+ { USER_KEY_ENV, keysFileReader, tuiMemoryBaseUrl },
1635
+ { keysFilePath },
1636
+ ] = await Promise.all([
1637
+ import("./openviking/reembed.js"),
1638
+ import("./tui/memory.js"),
1639
+ import("./tui/companion.js"),
1640
+ ]);
1641
+ const keysFile = keysFilePath(process.env);
1642
+ const readKey = keysFileReader(keysFile);
1643
+ const base = tuiMemoryBaseUrl(process.env);
1644
+ console.log(
1645
+ `re-embedding ${users.join(", ")} through ${base} with the keys in ${keysFile} (this can take minutes per namespace)`,
1646
+ );
1647
+ const { ok, results } = await reembedNamespaces({
1648
+ module: await loadReembedModule(sandboxRoot()),
1649
+ base,
1650
+ users,
1651
+ keyFor: (user) => namespaceKey(user, USER_KEY_ENV, readKey).key,
1652
+ log: (line) => console.log(line),
1653
+ });
1654
+ for (const r of results) console.log(` ${r.user}: ${r.status} — ${r.detail}`);
1655
+ if (ok) {
1656
+ // The journal lives with the store: in the daemon container, or in the
1657
+ // memory sidecar (whose app root is /opt/qcr) under `endpoint = "remote"`.
1658
+ const { tuiMemoryContainer } = await import("./tui/memforward.js");
1659
+ const memoryContainer = tuiMemoryContainer(cfg.tui);
1660
+ // The daemon container's app root follows its `qcr.layout` label.
1661
+ const { runnerFor } = await import("./tui/remote.js");
1662
+ await resolveLayout(cfg.tui, runnerFor(cfg.tui));
1663
+ const script = reembedScriptPath(
1664
+ memoryContainer === cfg.tui.container,
1665
+ remoteSandboxRoot(cfg.tui),
1666
+ );
1667
+ console.log(
1668
+ "done. The daemon's journal still lists these namespaces as awaiting you; receipt them in the container so the heartbeat stops naming them:\n" +
1669
+ users
1670
+ .map(
1671
+ (u) =>
1672
+ ` docker exec ${memoryContainer} node ${script} settle --home ${cfg.openviking.home} --user ${u}`,
1673
+ )
1674
+ .join("\n"),
1675
+ );
1676
+ }
1677
+ return ok ? 0 : 1;
1678
+ }
1679
+
1680
+ /** Where the re-embed script lives in the container the journal is in: the
1681
+ * daemon container's own app root, or the memory sidecar's /opt/qcr. */
1682
+ export function reembedScriptPath(inDaemonContainer: boolean, daemonRoot: string): string {
1683
+ return `${inDaemonContainer ? daemonRoot : "/opt/qcr"}/scripts/openviking-reembed.mjs`;
1684
+ }
1685
+
1686
+ const MEMORY_USAGE =
1687
+ "usage: qcr memory reembed [--user <name>[,<name>…]] | qcr memory relayout [--plan|--apply] [--user <name>]";
1688
+
1689
+ /** A plain memory namespace name — the shape `memory relayout` holds its
1690
+ * `--user` (and its default) to. `memory reembed` keeps its names lenient. */
1691
+ const NAMESPACE_RE = /^[a-z][a-z0-9_-]*$/;
1692
+
1693
+ /**
1694
+ * The flags of `qcr memory reembed` (after the verb): the namespaces `--user`
1695
+ * names, or null when it is absent (the caller's default set). An unknown
1696
+ * token or a repeated or empty `--user` is refused rather than ignored — a
1697
+ * mistyped flag must not quietly re-embed the default namespaces instead.
1698
+ */
1699
+ export function parseReembedArgs(rest: readonly string[]): string[] | null {
1700
+ let users: string[] | null = null;
1701
+ for (let i = 0; i < rest.length; i += 1) {
1702
+ if (rest[i] !== "--user" || users !== null) throw new Error(MEMORY_USAGE);
1703
+ users = String(rest[i + 1] ?? "")
1704
+ .split(/[,\s]+/)
1705
+ .filter(Boolean);
1706
+ i += 1;
1707
+ if (users.length === 0) throw new Error(MEMORY_USAGE);
1708
+ }
1709
+ return users;
1710
+ }
1711
+
1712
+ /** The keys-file names that may hold `user`'s key — a known user's own
1713
+ * list, else `OPENVIKING_<USER>_KEY` — and the first one present. */
1714
+ function namespaceKey(
1715
+ user: string,
1716
+ known: Readonly<Record<string, readonly string[]>>,
1717
+ readKey: (name: string) => string | null,
1718
+ ): { names: readonly string[]; key: string | null } {
1719
+ const names = Object.hasOwn(known, user)
1720
+ ? known[user]!
1721
+ : [`OPENVIKING_${user.toUpperCase()}_KEY`];
1722
+ return { names, key: names.map((n) => readKey(n)).find((v) => v) ?? null };
1723
+ }
1724
+
1725
+ /**
1726
+ * The flags of `qcr memory relayout`, or the usage error. `--plan` is the
1727
+ * default and writes nothing; only an explicit `--apply` writes, and the two
1728
+ * together are refused. `--user` must name a plain namespace; without it the
1729
+ * default (the daemon's `[openviking] user`) is asked for, lazily, so a bad
1730
+ * flag is reported before any config is read. An unknown token is refused
1731
+ * rather than ignored — a mistyped `--apply` must not quietly plan instead.
1732
+ */
1733
+ export function parseRelayoutArgs(
1734
+ rest: readonly string[],
1735
+ defaultUser: () => string,
1736
+ ): { apply: boolean; user: string } {
1737
+ let apply = false;
1738
+ let plan = false;
1739
+ let user: string | null = null;
1740
+ for (let i = 0; i < rest.length; i += 1) {
1741
+ const arg = rest[i]!;
1742
+ if (arg === "--apply") apply = true;
1743
+ else if (arg === "--plan") plan = true;
1744
+ else if (arg === "--user" && user === null) {
1745
+ user = String(rest[i + 1] ?? "").trim();
1746
+ i += 1;
1747
+ } else throw new Error(MEMORY_USAGE);
1748
+ }
1749
+ if (apply && plan) throw new Error(MEMORY_USAGE);
1750
+ const name = user ?? defaultUser();
1751
+ if (!NAMESPACE_RE.test(name)) throw new Error(MEMORY_USAGE);
1752
+ return { apply, user: name };
1753
+ }
1754
+
1755
+ /**
1756
+ * `qcr memory relayout [--plan|--apply] [--user <name>]` — sort
1757
+ * one memory namespace (default: the daemon's, `[openviking] user`) into the
1758
+ * memory layout (src/memory/layout.ts). `--plan` (the default) prints where every entry goes;
1759
+ * `--apply` carries the plan out and reads the store again to show what is
1760
+ * left — idempotent, so a second `--apply` plans nothing. Run from the PC
1761
+ * that holds the keys, over the memory forward, like `reembed`; the daemon
1762
+ * never runs it. Nothing is deleted: a move leaves a retired stub, a
1763
+ * retirement is a tag.
1764
+ */
1765
+ export async function cmdMemoryRelayout(rest: string[]): Promise<number> {
1766
+ const { apply, user } = parseRelayoutArgs(rest, () => loadConfig().openviking.user);
1767
+ const [
1768
+ { USER_KEY_ENV, keysFileReader, tuiMemoryBaseUrl },
1769
+ { keysFilePath },
1770
+ { OpenVikingClient },
1771
+ { OpenVikingStore },
1772
+ { runRelayout },
1773
+ ] = await Promise.all([
1774
+ import("./tui/memory.js"),
1775
+ import("./tui/companion.js"),
1776
+ import("./openviking/client.js"),
1777
+ import("./openviking/store.js"),
1778
+ import("./openviking/relayout.js"),
1779
+ ]);
1780
+ const keysFile = keysFilePath(process.env);
1781
+ const readKey = keysFileReader(keysFile);
1782
+ const { names, key } = namespaceKey(user, USER_KEY_ENV, readKey);
1783
+ if (!key) {
1784
+ console.log(`no key for the ${user} namespace in ${keysFile} (${names.join(" or ")})`);
1785
+ return 1;
1786
+ }
1787
+ const base = tuiMemoryBaseUrl(process.env);
1788
+ const client = new OpenVikingClient({ baseUrl: base, apiKey: key, user, timeoutMs: 30_000 });
1789
+ // Every read is a fresh, full one: the plan must describe the store as it
1790
+ // is now, not a cached sweep.
1791
+ const store = new OpenVikingStore({ client, cacheTtlMs: 0, fullRefreshMs: 0 });
1792
+ console.log(`${apply ? "applying" : "planning"} the relayout of ${user} memory through ${base}`);
1793
+ const out = await runRelayout(store, { apply, log: (line) => console.log(line) });
1794
+ return out.ok ? 0 : 1;
1795
+ }
1796
+
1797
+ const SETUP_USAGE = "usage: qcr setup [validate [<profile>] | commit]";
1798
+
1799
+ /**
1800
+ * `qcr setup` — set up the reviewed repo with the companion.
1801
+ * qcr setup ask the companion to run its setup skill (an
1802
+ * inject into its column; open `qcr tui`)
1803
+ * qcr setup validate [profile] the daemon's own check of a repo profile as
1804
+ * it is on disk — for a direct edit
1805
+ * qcr setup commit carry out the profile commits setup asked
1806
+ * for (the TUI does this on its own timer),
1807
+ * retrying any whose git step failed
1808
+ */
1809
+ export async function cmdSetup(rest: string[]): Promise<number> {
1810
+ const sub = rest[0];
1811
+ if (sub === "validate") {
1812
+ const { checkProfile, describeCheck, readProfileTarget } = await import("./setup/profile.js");
1813
+ const home = { root: sandboxRoot(), env: process.env };
1814
+ let name = rest[1]?.trim() ?? "";
1815
+ if (name === "") {
1816
+ const t = readProfileTarget(home);
1817
+ if (t.state !== "ok") {
1818
+ console.log(`✖ ${t.error}`);
1819
+ return 1;
1820
+ }
1821
+ name = t.named;
1822
+ }
1823
+ if (name === "") {
1824
+ console.log("the live config names no repo profile — name one: qcr setup validate <profile>");
1825
+ return 1;
1826
+ }
1827
+ const check = checkProfile(home, name);
1828
+ console.log(describeCheck(name, check));
1829
+ return check.ok ? 0 : 1;
1830
+ }
1831
+ if (sub === "commit") {
1832
+ const { commitRequestedProfiles, commitLine } = await import("./tui/setupentry.js");
1833
+ // The operator's own run: a commit whose git step failed is tried again.
1834
+ const results = commitRequestedProfiles(process.env, { retryFailed: true });
1835
+ if (results.length === 0) console.log("no profile commits waiting");
1836
+ for (const r of results) console.log(commitLine(r));
1837
+ return results.some((r) => r.state === "failed" || r.state === "refused") ? 1 : 0;
1838
+ }
1839
+ if (sub !== undefined) throw new Error(SETUP_USAGE);
1840
+ const { requestSetup, commitRequestedProfiles, commitLine } = await import("./tui/setupentry.js");
1841
+ const { companionInjectDir } = await import("./tui/companion.js");
1842
+ const cfg = loadConfig();
1843
+ for (const r of commitRequestedProfiles()) console.log(commitLine(r));
1844
+ const file = requestSetup({ injectDir: companionInjectDir(cfg.companion) });
1845
+ if (!cfg.companion.enabled) {
1846
+ console.log(
1847
+ `the request is queued (${file}), but [companion] enabled = false — the companion never starts, so nothing ` +
1848
+ "picks it up until you turn it on. Setup runs in the companion.",
1849
+ );
1850
+ return 1;
1851
+ }
1852
+ console.log(
1853
+ `asked the companion to set up ${cfg.github.repo} (${file}).\n` +
1854
+ "Open `qcr tui` — the companion column picks it up (at its next start, if it is not running) and walks you through " +
1855
+ "the survey and the interview. Nothing reaches PR reviews until you deploy the profile it writes.",
1856
+ );
1857
+ return 0;
1858
+ }
1859
+
1860
+ /** `qcr onboard` — the guided setup wizard: pi at the pinned
1861
+ * version via npx (works from a bare clone; pi stays a non-dependency), the
1862
+ * onboarding prompt + extension + a restricted tool set, then the TUI handoff
1863
+ * when the checklist verifies complete. Resumable: run it again after a crash
1864
+ * and completed steps are re-verified, not repeated. */
1865
+ export async function cmdOnboard(): Promise<number> {
1866
+ const { piPinFromPackageJson, onboardingSessionDir, runOnboard } =
1867
+ await import("./onboard/launch.js");
1868
+ const { checklistComplete } = await import("./onboard/engine.js");
1869
+ const { spawn } = await import("node:child_process");
1870
+ const root = sandboxRoot();
1871
+ // Pin source: config.toml [pi] when it loads; package.json's "qcrPi" block on a
1872
+ // bare clone (config.toml is written later, BY onboarding). Same pinned
1873
+ // version by the rule-7 mirror discipline.
1874
+ let pin: { package: string; version: string } | null = null;
1875
+ try {
1876
+ const { package: pkg, version } = loadConfig().pi;
1877
+ pin = { package: pkg, version };
1878
+ } catch {
1879
+ pin = piPinFromPackageJson(root);
1880
+ }
1881
+ if (!pin)
1882
+ throw new Error(
1883
+ 'onboard: pi pin not found (need config.toml [pi] or the package.json "qcrPi" block)',
1884
+ );
1885
+ mkdirSync(onboardingSessionDir(root), { recursive: true });
1886
+ const run = (cmd: string, args: string[]): Promise<number> =>
1887
+ new Promise((resolvePromise) => {
1888
+ const child = spawn(cmd, args, {
1889
+ stdio: "inherit",
1890
+ cwd: root,
1891
+ env: { ...process.env, QCR_ROOT: root },
1892
+ });
1893
+ child.on("error", (err) => {
1894
+ console.error(`onboard: ${cmd} failed to start: ${err.message}`);
1895
+ resolvePromise(1);
1896
+ });
1897
+ child.on("close", (code) => resolvePromise(code ?? 1));
1898
+ });
1899
+ return runOnboard({
1900
+ pin,
1901
+ root,
1902
+ spawn: run,
1903
+ isComplete: () => checklistComplete(join(root, "state")),
1904
+ log: (line) => console.log(line),
1905
+ cliPath: join(root, "dist", "src", "cli.js"),
1906
+ });
1907
+ }
1908
+
1909
+ export const ARCHIVE_USAGE =
1910
+ "usage: qcr archive status [--plan] [--json] | qcr archive run | qcr archive restore <pr> | " +
1911
+ "qcr archive extract <pr> --to <dir>";
1912
+
1913
+ /** `qcr archive` arguments. An unknown token is refused, so a
1914
+ * mistyped `--plan` cannot quietly print the block alone. */
1915
+ export type ArchiveArgs =
1916
+ | { verb: "status"; plan: boolean }
1917
+ | { verb: "run" }
1918
+ | { verb: "restore"; pr: number }
1919
+ | { verb: "extract"; pr: number; to: string };
1920
+
1921
+ export function parseArchiveArgs(rest: readonly string[]): ArchiveArgs {
1922
+ const prOf = (v: string | undefined): number => {
1923
+ const n = Number(v);
1924
+ if (v === undefined || !/^[1-9]\d*$/.test(v) || !Number.isSafeInteger(n))
1925
+ throw new Error(ARCHIVE_USAGE);
1926
+ return n;
1927
+ };
1928
+ switch (rest[0]) {
1929
+ case "status": {
1930
+ let plan = false;
1931
+ for (const a of rest.slice(1)) {
1932
+ if (a === "--plan") plan = true;
1933
+ else throw new Error(ARCHIVE_USAGE);
1934
+ }
1935
+ return { verb: "status", plan };
1936
+ }
1937
+ case "run":
1938
+ if (rest.length !== 1) throw new Error(ARCHIVE_USAGE);
1939
+ return { verb: "run" };
1940
+ case "restore":
1941
+ if (rest.length !== 2) throw new Error(ARCHIVE_USAGE);
1942
+ return { verb: "restore", pr: prOf(rest[1]) };
1943
+ case "extract": {
1944
+ const pr = prOf(rest[1]);
1945
+ if (rest.length !== 4 || rest[2] !== "--to" || !rest[3] || rest[3].startsWith("-"))
1946
+ throw new Error(ARCHIVE_USAGE);
1947
+ return { verb: "extract", pr, to: rest[3] };
1948
+ }
1949
+ default:
1950
+ throw new Error(ARCHIVE_USAGE);
1951
+ }
1952
+ }
1953
+
1954
+ /**
1955
+ * `qcr archive …`:
1956
+ * - `status [--plan] [--json]` — read-only, in this process. The daemon's
1957
+ * status.json `archive` block, the archive ledger (`archive_prs`) — and
1958
+ * with `--plan`, a plan computed HERE, now (worker leases asked in this container;
1959
+ * the daemon's in-flight births are not visible from a CLI, and the daemon
1960
+ * re-checks them before anything moves). Moves nothing.
1961
+ * - `run`, `restore <pr>` — the operator's two verbs: ONE command file
1962
+ * written as root into the sealed `commands/operator/` (the kill switch's
1963
+ * channel), confirmed by the daemon's own result file. The daemon runs
1964
+ * them under the archiver's strict mutex.
1965
+ * - `extract <pr> --to <dir>` — read-only over the archive: the PR's
1966
+ * archive decompressed into a scratch dir, every byte sha-verified, never
1967
+ * into a live tree. Runs where the archive is (the container).
1968
+ */
1969
+ async function cmdArchive(rest: readonly string[], json: boolean): Promise<number> {
1970
+ const args = parseArchiveArgs(rest);
1971
+ const cfg = loadConfig();
1972
+ if (args.verb === "run" || args.verb === "restore") {
1973
+ const { runnerFor } = await import("./tui/remote.js");
1974
+ const { ArchiveOpSender, renderArchiveOp } = await import("./tui/archiveops.js");
1975
+ const cmd: import("./tui/archiveops.js").ArchiveOpCommand =
1976
+ args.verb === "run"
1977
+ ? { command: "archive-run" }
1978
+ : { command: "archive-restore", pr: args.pr };
1979
+ if (!json) {
1980
+ await resolveLayout(cfg.tui, runnerFor(cfg.tui));
1981
+ console.log(
1982
+ `${cmd.command}${"pr" in cmd ? ` ${cmd.pr}` : ""} → ${cfg.tui.ssh_host}:${remoteSandboxRoot(cfg.tui)}/commands/operator/`,
1983
+ );
1984
+ }
1985
+ const res = await new ArchiveOpSender(cfg.tui, runnerFor(cfg.tui)).send(cmd);
1986
+ if (json) console.log(JSON.stringify(res));
1987
+ else for (const line of renderArchiveOp(cmd, res)) console.log(line);
1988
+ return res.ok ? 0 : 1;
1989
+ }
1990
+ const root = sandboxRoot();
1991
+ const { archivePolicy } = await import("./archive/archiver.js");
1992
+ const policy = archivePolicy(cfg.archive);
1993
+ if (args.verb === "extract") {
1994
+ const { extractPr } = await import("./archive/extract.js");
1995
+ const res = await extractPr({
1996
+ root,
1997
+ sessionsRoot: join(root, cfg.pi.session_dir),
1998
+ pr: args.pr,
1999
+ to: args.to,
2000
+ bytesPerS: policy.maxBytesPerS,
2001
+ });
2002
+ if (json) console.log(JSON.stringify(res));
2003
+ else if (!res.ok) console.log(`✖ refused: ${res.reason}`);
2004
+ else {
2005
+ const written = res.files.filter((f) => f.outcome === "written").length;
2006
+ console.log(
2007
+ `✓ pr-${args.pr} extracted to ${res.dir}: ${res.files.length} file(s) verified by sha256 ` +
2008
+ `(${written} written, ${res.files.length - written} already there)` +
2009
+ (res.copied.length > 0 ? `; copied as they are: ${res.copied.join(", ")}` : ""),
2010
+ );
2011
+ for (const e of res.errors) console.log(`✖ ${e.path}: ${e.error}`);
2012
+ }
2013
+ return res.ok && res.errors.length === 0 ? 0 : 1;
2014
+ }
2015
+ const statusPath = join(stateDir(), "status.json");
2016
+ const { archiveLedgerSummary, archiveStatusJson, readArchiveBlockFile, renderArchiveReport } =
2017
+ await import("./archive/report.js");
2018
+ const { block, statusNote, archiveRead } = readArchiveBlockFile(statusPath);
2019
+ const store = openStore();
2020
+ try {
2021
+ const ledger = archiveLedgerSummary(store);
2022
+ let plan: import("./archive/plan.js").ArchivePlan | null = null;
2023
+ if (args.plan) {
2024
+ const { computePlan } = await import("./archive/plan.js");
2025
+ const { createWorkerLiveness } = await import("./workerliveness.js");
2026
+ const { runDirOf } = await import("./ipc/capability.js");
2027
+ const workers = createWorkerLiveness({ runDir: runDirOf(join(root, "state")) });
2028
+ const { piGlobalSessionsDir } = await import("./archive/piglobal.js");
2029
+ plan = await computePlan({
2030
+ store,
2031
+ root,
2032
+ sessionsRoot: join(root, cfg.pi.session_dir),
2033
+ now: () => new Date(),
2034
+ afterDays: cfg.archive.after_days,
2035
+ eventsAfterDays: cfg.archive.events_after_days,
2036
+ liveSessions: async () => workers.liveSessions(),
2037
+ // This process's home: run in the container, the pihome volume.
2038
+ piGlobalDir: piGlobalSessionsDir(),
2039
+ });
2040
+ }
2041
+ if (json)
2042
+ console.log(
2043
+ JSON.stringify(archiveStatusJson({ block, archiveRead, statusNote, ledger, policy, plan })),
2044
+ );
2045
+ else console.log(renderArchiveReport({ block, statusNote, ledger, policy, plan }));
2046
+ return 0;
2047
+ } finally {
2048
+ store.close();
2049
+ }
2050
+ }
2051
+
2052
+ /** Takes the global `--json` flag out of `argv`. `qcr message` carries free
2053
+ * text, so a `--json` among its words stays words and the flag is not read. */
2054
+ export function splitJsonFlag(argv: string[]): { json: boolean; argv: string[] } {
2055
+ if (argv[0] === "message") return { json: false, argv };
2056
+ return { json: argv.includes("--json"), argv: argv.filter((a) => a !== "--json") };
2057
+ }
2058
+
2059
+ export async function main(rawArgv: string[]): Promise<void> {
2060
+ const { json, argv } = splitJsonFlag(rawArgv);
2061
+ // A GitHub token handed over as a file (packages/pi-plugin/src/ghcred.ts) is read and
2062
+ // unlinked here, before any verb can start a pi or bash child. A file that
2063
+ // is named but cannot be taken is a one-line failure, never a stack trace.
2064
+ try {
2065
+ takeHandoffToken();
2066
+ } catch (err) {
2067
+ const error = `the GitHub token handoff failed: ${err instanceof Error ? err.message : String(err)}`;
2068
+ if (json) console.log(JSON.stringify({ error }));
2069
+ else console.error(`qcr: ${error}`);
2070
+ process.exitCode = 1;
2071
+ return;
2072
+ }
2073
+ const [cmd, ...rest] = argv;
2074
+ switch (cmd) {
2075
+ case "checklist": {
2076
+ const { cmdChecklist } = await import("./cliops.js");
2077
+ process.exitCode = await cmdChecklist();
2078
+ return;
2079
+ }
2080
+ case "compare": {
2081
+ const prs = rest.map(Number);
2082
+ if (prs.length === 0 || prs.some((n) => !Number.isInteger(n)))
2083
+ throw new Error("usage: qcr compare <pr-number...>");
2084
+ const { cmdCompare } = await import("./cliops.js");
2085
+ process.exitCode = await cmdCompare(prs);
2086
+ return;
2087
+ }
2088
+ case "metrics": {
2089
+ const prIdx = rest.indexOf("--pr");
2090
+ const pr = prIdx >= 0 ? Number(rest[prIdx + 1]) : null;
2091
+ if (pr !== null && !Number.isInteger(pr))
2092
+ throw new Error("usage: qcr metrics [--pr <pr-number>] [--json]");
2093
+ return cmdMetrics(pr, json);
2094
+ }
2095
+ case "backfill": {
2096
+ const { dryRun, limit } = parseBackfillArgs(rest);
2097
+ return cmdBackfill(dryRun, limit, json);
2098
+ }
2099
+ case "history": {
2100
+ process.exitCode = await cmdHistory(rest);
2101
+ return;
2102
+ }
2103
+ case "outcomes": {
2104
+ process.exitCode = await cmdOutcomes(rest, json);
2105
+ return;
2106
+ }
2107
+ case "lessons": {
2108
+ process.exitCode = await cmdLessons(rest, json);
2109
+ return;
2110
+ }
2111
+ case "regression": {
2112
+ process.exitCode = await cmdRegression(rest, json);
2113
+ return;
2114
+ }
2115
+ case "status":
2116
+ return cmdStatus();
2117
+ case "spend":
2118
+ return cmdSpend();
2119
+ case "today": {
2120
+ if (json) {
2121
+ const store = openStore();
2122
+ try {
2123
+ console.log(JSON.stringify(todayJson(store, new Date().toISOString().slice(0, 10))));
2124
+ } finally {
2125
+ store.close();
2126
+ }
2127
+ return;
2128
+ }
2129
+ return cmdToday();
2130
+ }
2131
+ case "why": {
2132
+ const n = Number(rest[0]);
2133
+ if (!Number.isInteger(n)) throw new Error("usage: qcr why <pr-number> [--json]");
2134
+ if (json) {
2135
+ const store = openStore();
2136
+ try {
2137
+ const payload = whyJson(store, n, join(sandboxRoot(), "archive", `pr-${n}`));
2138
+ console.log(JSON.stringify(await enrichWhy(payload)));
2139
+ } finally {
2140
+ store.close();
2141
+ }
2142
+ return;
2143
+ }
2144
+ return cmdWhy(n);
2145
+ }
2146
+ case "events": {
2147
+ const tailIdx = rest.indexOf("--tail");
2148
+ const tail = tailIdx >= 0 ? Number(rest[tailIdx + 1]) : 40;
2149
+ if (!Number.isInteger(tail) || tail <= 0)
2150
+ throw new Error("usage: qcr events [--tail <k>] [--json]");
2151
+ return cmdEvents(tail, json);
2152
+ }
2153
+ case "tui": {
2154
+ const { runTui } = await import("./tui/index.js");
2155
+ return runTui(rest);
2156
+ }
2157
+ case "companion":
2158
+ if (rest.length !== 1) throw new Error("usage: qcr companion <up|down|status|rebuild|plan>");
2159
+ return cmdCompanion(rest[0]);
2160
+ case "memory": {
2161
+ process.exitCode = await cmdMemory(rest);
2162
+ return;
2163
+ }
2164
+ case "onboard": {
2165
+ process.exitCode = await cmdOnboard();
2166
+ return;
2167
+ }
2168
+ case "setup": {
2169
+ process.exitCode = await cmdSetup(rest);
2170
+ return;
2171
+ }
2172
+ case "drain": {
2173
+ process.exitCode = await cmdDrain(rest);
2174
+ return;
2175
+ }
2176
+ case "pause-pr":
2177
+ case "resume-pr":
2178
+ case "review-now": {
2179
+ // review-now: the operator's "review this PR now" — queued as a
2180
+ // human-asked round through the sealed operator channel, no label.
2181
+ process.exitCode = await cmdPrLever(cmd, rest);
2182
+ return;
2183
+ }
2184
+ case "github-token": {
2185
+ // The own-account route's token refresher (src/githubtoken.ts): gh's
2186
+ // token → <secrets>/github_token, 0600. The crontab entry runs this.
2187
+ const { cmdGithubToken } = await import("./githubtoken.js");
2188
+ process.exitCode = await cmdGithubToken(rest, {
2189
+ secretsDir: () => {
2190
+ try {
2191
+ return loadConfig().github.secrets_dir;
2192
+ } catch {
2193
+ return locateSecrets({}).path;
2194
+ }
2195
+ },
2196
+ });
2197
+ return;
2198
+ }
2199
+ case "message": {
2200
+ process.exitCode = await cmdMessage(rest);
2201
+ return;
2202
+ }
2203
+ case "archive": {
2204
+ process.exitCode = await cmdArchive(rest, json);
2205
+ return;
2206
+ }
2207
+ case "live": {
2208
+ // A review's LiveEvent log as a transcript (src/livecli.ts). Usage
2209
+ // errors are its own exit code 2.
2210
+ const { runLive } = await import("./livecli.js");
2211
+ const { liveContainersFromEnv } = await import("./reviewdetach.js");
2212
+ const { stateHome } = await import("./install.js");
2213
+ process.exitCode = await runLive(rest, json, {
2214
+ home: join(stateHome(), "review", "detached"),
2215
+ liveContainers: () => liveContainersFromEnv(process.env),
2216
+ out: (l) => console.log(l),
2217
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
2218
+ alive: (pid) => {
2219
+ try {
2220
+ process.kill(pid, 0);
2221
+ return true;
2222
+ } catch {
2223
+ return false;
2224
+ }
2225
+ },
2226
+ });
2227
+ return;
2228
+ }
2229
+ case "scan": {
2230
+ // Scan stage 1 and 2, the companion's scan tools as a scriptable verb
2231
+ // (src/scancli.ts). Usage errors are its own exit code 2.
2232
+ const { cmdScan } = await import("./scancli.js");
2233
+ process.exitCode = await cmdScan(rest, json);
2234
+ return;
2235
+ }
2236
+ case "review": {
2237
+ // A daemonless review in the terminal (src/reviewcli.ts). Usage errors
2238
+ // are its own exit code 2.
2239
+ const { cmdReview } = await import("./reviewcli.js");
2240
+ const { openReviewMemoryPortAt } = await import("./reviewmemory.js");
2241
+ process.exitCode = await cmdReview(rest, json, { memory: openReviewMemoryPortAt });
2242
+ return;
2243
+ }
2244
+ case "open": {
2245
+ // A finished review session in pi's own UI (src/opencli.ts). Usage
2246
+ // errors are its own exit code 2.
2247
+ const { cmdOpen } = await import("./opencli.js");
2248
+ process.exitCode = await cmdOpen(rest, json);
2249
+ return;
2250
+ }
2251
+ case "version": {
2252
+ // The install's identity for a plugin: image version, JSON schema
2253
+ // versions, pi pin (src/version.ts). Needs no config and no state.
2254
+ const { cmdVersion } = await import("./version.js");
2255
+ return cmdVersion(json);
2256
+ }
2257
+ case "install": {
2258
+ process.exitCode = cmdInstall(rest);
2259
+ return;
2260
+ }
2261
+ case "config": {
2262
+ // Load a config home the way the daemon boots, print every error
2263
+ // (src/configcheck.ts); the VM updater runs it on the staged home.
2264
+ const { cmdConfig } = await import("./configcheck.js");
2265
+ process.exitCode = cmdConfig(rest, json, { defaultHome: () => locateConfig().home });
2266
+ return;
2267
+ }
2268
+ case "takeover":
2269
+ case "debug-round":
2270
+ case "release": {
2271
+ process.exitCode = await cmdTakeover(cmd, rest);
2272
+ return;
2273
+ }
2274
+ default:
2275
+ console.log(
2276
+ "usage: qcr <version|config check [--config-home <dir>]|onboard|setup [validate [<profile>]|commit]|status|spend|today|why <n>|metrics [--pr <n>]|events|takeover <n>|debug-round <n>|release <n>|tui [pr|overseer]|drain <on [--hold]|off>|pause-pr <pr> [--reason <text>]|resume-pr <pr>|review-now <pr>|github-token sync [--gh <path>] [--secrets-dir <dir>]|message <pr> [--may-post] <text...>|archive <status [--plan]|run|restore <pr>|extract <pr> --to <dir>>|install <where|migrate>|" +
2277
+ "companion <up|down|status|rebuild>|review [start|status|stop|branch|--uncommitted|--diff FILE|-|<PR number|URL>] [--repo OWNER/NAME] [--base REF] [--preview] [--key K] [--limit USD] [--json]|live [RUN_ID|DIR] [--lens ID] [--round N] [--follow] [--json]|open [RUN_ID|DIR|FILE.jsonl|scan[:RUN]|pr N [--round R]] [--lens ID] [--write] [--print]|scan <preview|start|status|stop|findings|matches|match|suggestions|stage2 propose|stage2 start|dismiss|units show|units propose|units adjust|units save|login|doctor|rebuild>|memory reembed [--user <name>]|memory relayout [--plan|--apply]|backfill [--dry-run] [--limit <n>]|history <digest|pull>|outcomes [--days <n>] [--capture [--read-github]]|lessons [mine|shadow-report [--days <n>]|off|on|suppress <id>|unsuppress <id>]|checklist|compare <pr...>|" +
2278
+ "regression [--set <path>] [--clone <path>] [--change <name>] [--limit <n>] [--out <path>]>",
2279
+ );
2280
+ // A bare `qcr` (or an explicit help) is a usage request; anything else
2281
+ // here is an unknown command, which must not look like success.
2282
+ if (cmd !== undefined && !["help", "--help", "-h"].includes(cmd)) process.exitCode = 1;
2283
+ loadConfig(); // surface config errors early even on bad usage
2284
+ }
2285
+ }
2286
+
2287
+ /** Whether `entry` (the process's argv[1]) is this very module, links
2288
+ * resolved: `node dist/src/cli.js`, the image's `qcr`. The npm bin
2289
+ * (bin/qcr.js, reached through a package manager's link whose name is never
2290
+ * cli.js) imports this module and calls `main` itself, and another script
2291
+ * that merely shares the name is not this one. */
2292
+ export function isEntryModule(
2293
+ entry: string | undefined,
2294
+ self: string = fileURLToPath(import.meta.url),
2295
+ ): boolean {
2296
+ if (!entry) return false;
2297
+ try {
2298
+ return realpathSync(entry) === realpathSync(self);
2299
+ } catch {
2300
+ return false;
2301
+ }
2302
+ }
2303
+
2304
+ if (isEntryModule(process.argv[1])) {
2305
+ void main(process.argv.slice(2));
2306
+ }