@quickchatai/quickreview 0.0.0-stage → 1.1.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 (1443) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +385 -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 +1067 -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/muted-theme.js +5 -0
  62. package/dist/extensions/cli/muted-theme.js.map +1 -0
  63. package/dist/extensions/cli/onboarding.js +5 -0
  64. package/dist/extensions/cli/onboarding.js.map +1 -0
  65. package/dist/extensions/cli/setup.js +5 -0
  66. package/dist/extensions/cli/setup.js.map +1 -0
  67. package/dist/extensions/cli/tui-view.js +5 -0
  68. package/dist/extensions/cli/tui-view.js.map +1 -0
  69. package/dist/extensions/companion-guard.js +361 -0
  70. package/dist/extensions/companion-guard.js.map +1 -0
  71. package/dist/extensions/debug-round.js +204 -0
  72. package/dist/extensions/debug-round.js.map +1 -0
  73. package/dist/extensions/inject-bridge.js +603 -0
  74. package/dist/extensions/inject-bridge.js.map +1 -0
  75. package/dist/extensions/muted-theme.js +12 -0
  76. package/dist/extensions/muted-theme.js.map +1 -0
  77. package/dist/extensions/onboarding.js +175 -0
  78. package/dist/extensions/onboarding.js.map +1 -0
  79. package/dist/extensions/setup-replay.js +269 -0
  80. package/dist/extensions/setup-replay.js.map +1 -0
  81. package/dist/extensions/setup-scan.js +998 -0
  82. package/dist/extensions/setup-scan.js.map +1 -0
  83. package/dist/extensions/setup-triage.js +175 -0
  84. package/dist/extensions/setup-triage.js.map +1 -0
  85. package/dist/extensions/setup.js +1160 -0
  86. package/dist/extensions/setup.js.map +1 -0
  87. package/dist/extensions/tui-view.js +389 -0
  88. package/dist/extensions/tui-view.js.map +1 -0
  89. package/dist/extensions/vendor/openviking-pi/client.js +376 -0
  90. package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
  91. package/dist/extensions/vendor/openviking-pi/config.js +189 -0
  92. package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
  93. package/dist/extensions/vendor/openviking-pi/index.js +421 -0
  94. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
  95. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
  96. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
  97. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
  98. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
  99. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
  100. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
  101. package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
  102. package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
  103. package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
  104. package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
  105. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
  106. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
  107. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
  108. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
  109. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
  110. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
  111. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
  112. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
  113. package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
  114. package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
  115. package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
  116. package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
  117. package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
  118. package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
  119. package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
  120. package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
  121. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
  122. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
  123. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
  124. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
  125. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
  126. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
  127. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
  128. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
  129. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
  130. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
  131. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
  132. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
  133. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
  134. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
  135. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
  136. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
  137. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
  138. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
  139. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  140. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
  141. package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
  142. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
  143. package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
  144. package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
  145. package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
  146. package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
  147. package/dist/src/anomalies.js +483 -0
  148. package/dist/src/anomalies.js.map +1 -0
  149. package/dist/src/answerworker.js +180 -0
  150. package/dist/src/answerworker.js.map +1 -0
  151. package/dist/src/appstamp.js +68 -0
  152. package/dist/src/appstamp.js.map +1 -0
  153. package/dist/src/archive/admission.js +3 -0
  154. package/dist/src/archive/admission.js.map +1 -0
  155. package/dist/src/archive/archivedb.js +474 -0
  156. package/dist/src/archive/archivedb.js.map +1 -0
  157. package/dist/src/archive/archiver.js +1770 -0
  158. package/dist/src/archive/archiver.js.map +1 -0
  159. package/dist/src/archive/archivewrite.js +198 -0
  160. package/dist/src/archive/archivewrite.js.map +1 -0
  161. package/dist/src/archive/bundle.js +266 -0
  162. package/dist/src/archive/bundle.js.map +1 -0
  163. package/dist/src/archive/dbmove.js +638 -0
  164. package/dist/src/archive/dbmove.js.map +1 -0
  165. package/dist/src/archive/duplicatecounts.js +80 -0
  166. package/dist/src/archive/duplicatecounts.js.map +1 -0
  167. package/dist/src/archive/eligibility.js +113 -0
  168. package/dist/src/archive/eligibility.js.map +1 -0
  169. package/dist/src/archive/extract.js +288 -0
  170. package/dist/src/archive/extract.js.map +1 -0
  171. package/dist/src/archive/fsutil.js +330 -0
  172. package/dist/src/archive/fsutil.js.map +1 -0
  173. package/dist/src/archive/gzfile.js +277 -0
  174. package/dist/src/archive/gzfile.js.map +1 -0
  175. package/dist/src/archive/kinds.js +22 -0
  176. package/dist/src/archive/kinds.js.map +1 -0
  177. package/dist/src/archive/livecheckpoint.js +178 -0
  178. package/dist/src/archive/livecheckpoint.js.map +1 -0
  179. package/dist/src/archive/liveretention.js +145 -0
  180. package/dist/src/archive/liveretention.js.map +1 -0
  181. package/dist/src/archive/manifest.js +284 -0
  182. package/dist/src/archive/manifest.js.map +1 -0
  183. package/dist/src/archive/overseer.js +283 -0
  184. package/dist/src/archive/overseer.js.map +1 -0
  185. package/dist/src/archive/piglobal.js +129 -0
  186. package/dist/src/archive/piglobal.js.map +1 -0
  187. package/dist/src/archive/plan.js +398 -0
  188. package/dist/src/archive/plan.js.map +1 -0
  189. package/dist/src/archive/prarchive.js +831 -0
  190. package/dist/src/archive/prarchive.js.map +1 -0
  191. package/dist/src/archive/prfiles.js +303 -0
  192. package/dist/src/archive/prfiles.js.map +1 -0
  193. package/dist/src/archive/report.js +169 -0
  194. package/dist/src/archive/report.js.map +1 -0
  195. package/dist/src/archive/restore.js +276 -0
  196. package/dist/src/archive/restore.js.map +1 -0
  197. package/dist/src/archive/rows.js +106 -0
  198. package/dist/src/archive/rows.js.map +1 -0
  199. package/dist/src/archive/seal.js +480 -0
  200. package/dist/src/archive/seal.js.map +1 -0
  201. package/dist/src/archive/status.js +528 -0
  202. package/dist/src/archive/status.js.map +1 -0
  203. package/dist/src/archive/storepass.js +409 -0
  204. package/dist/src/archive/storepass.js.map +1 -0
  205. package/dist/src/backfill.js +217 -0
  206. package/dist/src/backfill.js.map +1 -0
  207. package/dist/src/bench/cost.js +87 -0
  208. package/dist/src/bench/exec.js +76 -0
  209. package/dist/src/bench/values.js +14 -0
  210. package/dist/src/botcontrol.js +127 -0
  211. package/dist/src/botcontrol.js.map +1 -0
  212. package/dist/src/botworker.js +805 -0
  213. package/dist/src/botworker.js.map +1 -0
  214. package/dist/src/capture.js +234 -0
  215. package/dist/src/capture.js.map +1 -0
  216. package/dist/src/cases.js +1491 -0
  217. package/dist/src/cases.js.map +1 -0
  218. package/dist/src/channelfs.js +27 -0
  219. package/dist/src/channelfs.js.map +1 -0
  220. package/dist/src/checklist.js +312 -0
  221. package/dist/src/checklist.js.map +1 -0
  222. package/dist/src/ciretry.js +91 -0
  223. package/dist/src/ciretry.js.map +1 -0
  224. package/dist/src/cli.js +1916 -0
  225. package/dist/src/cli.js.map +1 -0
  226. package/dist/src/cliops.js +333 -0
  227. package/dist/src/cliops.js.map +1 -0
  228. package/dist/src/clonedir.js +15 -0
  229. package/dist/src/clonedir.js.map +1 -0
  230. package/dist/src/commandeffects.js +43 -0
  231. package/dist/src/commandeffects.js.map +1 -0
  232. package/dist/src/commands.js +1312 -0
  233. package/dist/src/commands.js.map +1 -0
  234. package/dist/src/companioninbox.js +203 -0
  235. package/dist/src/companioninbox.js.map +1 -0
  236. package/dist/src/compare.js +673 -0
  237. package/dist/src/compare.js.map +1 -0
  238. package/dist/src/config.js +1027 -0
  239. package/dist/src/config.js.map +1 -0
  240. package/dist/src/configcheck.js +111 -0
  241. package/dist/src/configcheck.js.map +1 -0
  242. package/dist/src/coreext.js +43 -0
  243. package/dist/src/coreext.js.map +1 -0
  244. package/dist/src/daemon.js +929 -0
  245. package/dist/src/daemon.js.map +1 -0
  246. package/dist/src/debuground.js +731 -0
  247. package/dist/src/debuground.js.map +1 -0
  248. package/dist/src/debugroundpi.js +219 -0
  249. package/dist/src/debugroundpi.js.map +1 -0
  250. package/dist/src/diagnostics.js +14 -0
  251. package/dist/src/diagnostics.js.map +1 -0
  252. package/dist/src/digest.js +139 -0
  253. package/dist/src/digest.js.map +1 -0
  254. package/dist/src/dispatch.js +1726 -0
  255. package/dist/src/dispatch.js.map +1 -0
  256. package/dist/src/engine/answerguard.js +18 -0
  257. package/dist/src/engine/answerguard.js.map +1 -0
  258. package/dist/src/engine/botsdk.js +262 -0
  259. package/dist/src/engine/botsdk.js.map +1 -0
  260. package/dist/src/engine/caserunner-sdk.js +332 -0
  261. package/dist/src/engine/caserunner-sdk.js.map +1 -0
  262. package/dist/src/engine/debugpi.js +194 -0
  263. package/dist/src/engine/debugpi.js.map +1 -0
  264. package/dist/src/eod.js +473 -0
  265. package/dist/src/eod.js.map +1 -0
  266. package/dist/src/eodpinwatch.js +391 -0
  267. package/dist/src/eodpinwatch.js.map +1 -0
  268. package/dist/src/escalation.js +1009 -0
  269. package/dist/src/escalation.js.map +1 -0
  270. package/dist/src/eventloop.js +779 -0
  271. package/dist/src/eventloop.js.map +1 -0
  272. package/dist/src/execboundary.js +430 -0
  273. package/dist/src/execboundary.js.map +1 -0
  274. package/dist/src/feedback.js +915 -0
  275. package/dist/src/feedback.js.map +1 -0
  276. package/dist/src/findingscheck.js +200 -0
  277. package/dist/src/findingscheck.js.map +1 -0
  278. package/dist/src/fsdenied.js +26 -0
  279. package/dist/src/fsdenied.js.map +1 -0
  280. package/dist/src/ghread.js +181 -0
  281. package/dist/src/ghread.js.map +1 -0
  282. package/dist/src/github.js +1063 -0
  283. package/dist/src/github.js.map +1 -0
  284. package/dist/src/homes.js +103 -0
  285. package/dist/src/homes.js.map +1 -0
  286. package/dist/src/hostroute.js +24 -0
  287. package/dist/src/hostroute.js.map +1 -0
  288. package/dist/src/improvements.js +625 -0
  289. package/dist/src/improvements.js.map +1 -0
  290. package/dist/src/install.js +946 -0
  291. package/dist/src/install.js.map +1 -0
  292. package/dist/src/installkind.js +40 -0
  293. package/dist/src/installkind.js.map +1 -0
  294. package/dist/src/ipc/capability.js +286 -0
  295. package/dist/src/ipc/capability.js.map +1 -0
  296. package/dist/src/ipc/client.js +226 -0
  297. package/dist/src/ipc/client.js.map +1 -0
  298. package/dist/src/ipc/control.js +70 -0
  299. package/dist/src/ipc/control.js.map +1 -0
  300. package/dist/src/ipc/ghreadshim.js +77 -0
  301. package/dist/src/ipc/ghreadshim.js.map +1 -0
  302. package/dist/src/ipc/launcher.js +65 -0
  303. package/dist/src/ipc/launcher.js.map +1 -0
  304. package/dist/src/ipc/permits.js +293 -0
  305. package/dist/src/ipc/permits.js.map +1 -0
  306. package/dist/src/ipc/postshim.js +137 -0
  307. package/dist/src/ipc/postshim.js.map +1 -0
  308. package/dist/src/ipc/protocol.js +239 -0
  309. package/dist/src/ipc/protocol.js.map +1 -0
  310. package/dist/src/ipc/server.js +312 -0
  311. package/dist/src/ipc/server.js.map +1 -0
  312. package/dist/src/ipc/workerspec.js +148 -0
  313. package/dist/src/ipc/workerspec.js.map +1 -0
  314. package/dist/src/learner/learner.js +1272 -0
  315. package/dist/src/learner/learner.js.map +1 -0
  316. package/dist/src/learner/miner.js +948 -0
  317. package/dist/src/learner/miner.js.map +1 -0
  318. package/dist/src/learner/packet.js +216 -0
  319. package/dist/src/learner/packet.js.map +1 -0
  320. package/dist/src/learner/receipt.js +135 -0
  321. package/dist/src/learner/receipt.js.map +1 -0
  322. package/dist/src/learner/status.js +38 -0
  323. package/dist/src/learner/status.js.map +1 -0
  324. package/dist/src/ledgercheck.js +120 -0
  325. package/dist/src/ledgercheck.js.map +1 -0
  326. package/dist/src/lessonledger.js +451 -0
  327. package/dist/src/lessonledger.js.map +1 -0
  328. package/dist/src/lessonselect.js +639 -0
  329. package/dist/src/lessonselect.js.map +1 -0
  330. package/dist/src/lessonshadow.js +540 -0
  331. package/dist/src/lessonshadow.js.map +1 -0
  332. package/dist/src/levers.js +302 -0
  333. package/dist/src/levers.js.map +1 -0
  334. package/dist/src/live/projection.js +450 -0
  335. package/dist/src/live/projection.js.map +1 -0
  336. package/dist/src/live/publish.js +83 -0
  337. package/dist/src/live/publish.js.map +1 -0
  338. package/dist/src/live/read.js +60 -0
  339. package/dist/src/live/read.js.map +1 -0
  340. package/dist/src/livecli.js +288 -0
  341. package/dist/src/livecli.js.map +1 -0
  342. package/dist/src/loginhint.js +42 -0
  343. package/dist/src/loginhint.js.map +1 -0
  344. package/dist/src/loginrefresh/atomic.js +66 -0
  345. package/dist/src/loginrefresh/atomic.js.map +1 -0
  346. package/dist/src/loginrefresh/lock.js +93 -0
  347. package/dist/src/loginrefresh/lock.js.map +1 -0
  348. package/dist/src/loginrefresh/main.js +58 -0
  349. package/dist/src/loginrefresh/main.js.map +1 -0
  350. package/dist/src/loginrefresh/pirefresh.js +49 -0
  351. package/dist/src/loginrefresh/pirefresh.js.map +1 -0
  352. package/dist/src/loginrefresh/refresher.js +286 -0
  353. package/dist/src/loginrefresh/refresher.js.map +1 -0
  354. package/dist/src/loginrefresh/supervisor.js +253 -0
  355. package/dist/src/loginrefresh/supervisor.js.map +1 -0
  356. package/dist/src/loginrefresh/types.js +4 -0
  357. package/dist/src/loginrefresh/types.js.map +1 -0
  358. package/dist/src/memory/context.js +251 -0
  359. package/dist/src/memory/context.js.map +1 -0
  360. package/dist/src/memory/datafence.js +5 -0
  361. package/dist/src/memory/datafence.js.map +1 -0
  362. package/dist/src/memory/delivery.js +131 -0
  363. package/dist/src/memory/delivery.js.map +1 -0
  364. package/dist/src/memory/eventtags.js +81 -0
  365. package/dist/src/memory/eventtags.js.map +1 -0
  366. package/dist/src/memory/health.js +713 -0
  367. package/dist/src/memory/health.js.map +1 -0
  368. package/dist/src/memory/import.js +504 -0
  369. package/dist/src/memory/import.js.map +1 -0
  370. package/dist/src/memory/layout.js +252 -0
  371. package/dist/src/memory/layout.js.map +1 -0
  372. package/dist/src/memory/legacy-file.js +100 -0
  373. package/dist/src/memory/legacy-file.js.map +1 -0
  374. package/dist/src/memory/lessons.js +370 -0
  375. package/dist/src/memory/lessons.js.map +1 -0
  376. package/dist/src/memory/ovsession.js +449 -0
  377. package/dist/src/memory/ovsession.js.map +1 -0
  378. package/dist/src/memory/relayout.js +259 -0
  379. package/dist/src/memory/relayout.js.map +1 -0
  380. package/dist/src/memory/roundactivity.js +180 -0
  381. package/dist/src/memory/roundactivity.js.map +1 -0
  382. package/dist/src/memory/selection.js +690 -0
  383. package/dist/src/memory/selection.js.map +1 -0
  384. package/dist/src/memory/shadowstatus.js +69 -0
  385. package/dist/src/memory/shadowstatus.js.map +1 -0
  386. package/dist/src/memory/store.js +83 -0
  387. package/dist/src/memory/store.js.map +1 -0
  388. package/dist/src/memory/textidentity.js +8 -0
  389. package/dist/src/memory/textidentity.js.map +1 -0
  390. package/dist/src/metrics.js +773 -0
  391. package/dist/src/metrics.js.map +1 -0
  392. package/dist/src/missingconfig.js +8 -0
  393. package/dist/src/missingconfig.js.map +1 -0
  394. package/dist/src/npmverbs.js +226 -0
  395. package/dist/src/npmverbs.js.map +1 -0
  396. package/dist/src/onboard/choices.js +191 -0
  397. package/dist/src/onboard/choices.js.map +1 -0
  398. package/dist/src/onboard/engine.js +344 -0
  399. package/dist/src/onboard/engine.js.map +1 -0
  400. package/dist/src/onboard/launch.js +83 -0
  401. package/dist/src/onboard/launch.js.map +1 -0
  402. package/dist/src/onboard/steps/appslug.js +211 -0
  403. package/dist/src/onboard/steps/appslug.js.map +1 -0
  404. package/dist/src/onboard/steps/bringup.js +408 -0
  405. package/dist/src/onboard/steps/bringup.js.map +1 -0
  406. package/dist/src/onboard/steps/compose.js +343 -0
  407. package/dist/src/onboard/steps/compose.js.map +1 -0
  408. package/dist/src/onboard/steps/configdelivery.js +50 -0
  409. package/dist/src/onboard/steps/configdelivery.js.map +1 -0
  410. package/dist/src/onboard/steps/configfill.js +1148 -0
  411. package/dist/src/onboard/steps/configfill.js.map +1 -0
  412. package/dist/src/onboard/steps/deploysh.js +80 -0
  413. package/dist/src/onboard/steps/deploysh.js.map +1 -0
  414. package/dist/src/onboard/steps/embeddings.js +472 -0
  415. package/dist/src/onboard/steps/embeddings.js.map +1 -0
  416. package/dist/src/onboard/steps/escalation.js +571 -0
  417. package/dist/src/onboard/steps/escalation.js.map +1 -0
  418. package/dist/src/onboard/steps/githubapp.js +1092 -0
  419. package/dist/src/onboard/steps/githubapp.js.map +1 -0
  420. package/dist/src/onboard/steps/golive.js +506 -0
  421. package/dist/src/onboard/steps/golive.js.map +1 -0
  422. package/dist/src/onboard/steps/index.js +67 -0
  423. package/dist/src/onboard/steps/index.js.map +1 -0
  424. package/dist/src/onboard/steps/ingress.js +762 -0
  425. package/dist/src/onboard/steps/ingress.js.map +1 -0
  426. package/dist/src/onboard/steps/inventory.js +420 -0
  427. package/dist/src/onboard/steps/inventory.js.map +1 -0
  428. package/dist/src/onboard/steps/llmauth.js +893 -0
  429. package/dist/src/onboard/steps/llmauth.js.map +1 -0
  430. package/dist/src/onboard/steps/memory.js +1060 -0
  431. package/dist/src/onboard/steps/memory.js.map +1 -0
  432. package/dist/src/onboard/steps/memorykeys.js +43 -0
  433. package/dist/src/onboard/steps/memorykeys.js.map +1 -0
  434. package/dist/src/onboard/steps/preflight.js +172 -0
  435. package/dist/src/onboard/steps/preflight.js.map +1 -0
  436. package/dist/src/onboard/steps/proof.js +502 -0
  437. package/dist/src/onboard/steps/proof.js.map +1 -0
  438. package/dist/src/onboard/steps/ssh.js +300 -0
  439. package/dist/src/onboard/steps/ssh.js.map +1 -0
  440. package/dist/src/onboard/steps/topology.js +291 -0
  441. package/dist/src/onboard/steps/topology.js.map +1 -0
  442. package/dist/src/onboard/steps/transport.js +511 -0
  443. package/dist/src/onboard/steps/transport.js.map +1 -0
  444. package/dist/src/onboard/steps/verifyup.js +362 -0
  445. package/dist/src/onboard/steps/verifyup.js.map +1 -0
  446. package/dist/src/onboard/steps/vmdeploy.js +368 -0
  447. package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
  448. package/dist/src/onboard/toml.js +53 -0
  449. package/dist/src/onboard/toml.js.map +1 -0
  450. package/dist/src/opencli.js +508 -0
  451. package/dist/src/opencli.js.map +1 -0
  452. package/dist/src/openpr.js +457 -0
  453. package/dist/src/openpr.js.map +1 -0
  454. package/dist/src/openviking/client.js +821 -0
  455. package/dist/src/openviking/client.js.map +1 -0
  456. package/dist/src/openviking/context.js +218 -0
  457. package/dist/src/openviking/context.js.map +1 -0
  458. package/dist/src/openviking/embedder.js +457 -0
  459. package/dist/src/openviking/embedder.js.map +1 -0
  460. package/dist/src/openviking/gapsink.js +38 -0
  461. package/dist/src/openviking/gapsink.js.map +1 -0
  462. package/dist/src/openviking/health.js +69 -0
  463. package/dist/src/openviking/health.js.map +1 -0
  464. package/dist/src/openviking/index.js +24 -0
  465. package/dist/src/openviking/index.js.map +1 -0
  466. package/dist/src/openviking/lessonfind.js +54 -0
  467. package/dist/src/openviking/lessonfind.js.map +1 -0
  468. package/dist/src/openviking/probe.js +89 -0
  469. package/dist/src/openviking/probe.js.map +1 -0
  470. package/dist/src/openviking/proc.js +95 -0
  471. package/dist/src/openviking/proc.js.map +1 -0
  472. package/dist/src/openviking/provisionreport.js +144 -0
  473. package/dist/src/openviking/provisionreport.js.map +1 -0
  474. package/dist/src/openviking/reembed.js +317 -0
  475. package/dist/src/openviking/reembed.js.map +1 -0
  476. package/dist/src/openviking/relayout.js +153 -0
  477. package/dist/src/openviking/relayout.js.map +1 -0
  478. package/dist/src/openviking/sidecar-main.js +39 -0
  479. package/dist/src/openviking/sidecar-main.js.map +1 -0
  480. package/dist/src/openviking/sidecar.js +79 -0
  481. package/dist/src/openviking/sidecar.js.map +1 -0
  482. package/dist/src/openviking/store.js +1758 -0
  483. package/dist/src/openviking/store.js.map +1 -0
  484. package/dist/src/openviking/supervisor.js +422 -0
  485. package/dist/src/openviking/supervisor.js.map +1 -0
  486. package/dist/src/openviking/types.js +5 -0
  487. package/dist/src/openviking/types.js.map +1 -0
  488. package/dist/src/operator-recovery.js +172 -0
  489. package/dist/src/operator-recovery.js.map +1 -0
  490. package/dist/src/operatormessage.js +44 -0
  491. package/dist/src/operatormessage.js.map +1 -0
  492. package/dist/src/outcomes.js +1361 -0
  493. package/dist/src/outcomes.js.map +1 -0
  494. package/dist/src/piauthcopy.js +5 -0
  495. package/dist/src/piauthcopy.js.map +1 -0
  496. package/dist/src/picli.js +45 -0
  497. package/dist/src/picli.js.map +1 -0
  498. package/dist/src/picwd.js +25 -0
  499. package/dist/src/picwd.js.map +1 -0
  500. package/dist/src/pilogin.js +35 -0
  501. package/dist/src/pilogin.js.map +1 -0
  502. package/dist/src/pingpong.js +1028 -0
  503. package/dist/src/pingpong.js.map +1 -0
  504. package/dist/src/posting-journal.js +441 -0
  505. package/dist/src/posting-journal.js.map +1 -0
  506. package/dist/src/posting-outbox.js +662 -0
  507. package/dist/src/posting-outbox.js.map +1 -0
  508. package/dist/src/posting-receipt.js +203 -0
  509. package/dist/src/posting-receipt.js.map +1 -0
  510. package/dist/src/posting.js +348 -0
  511. package/dist/src/posting.js.map +1 -0
  512. package/dist/src/postops.js +637 -0
  513. package/dist/src/postops.js.map +1 -0
  514. package/dist/src/pr-activity.js +11 -0
  515. package/dist/src/pr-activity.js.map +1 -0
  516. package/dist/src/pr-author.js +5 -0
  517. package/dist/src/pr-author.js.map +1 -0
  518. package/dist/src/pr-conversation.js +355 -0
  519. package/dist/src/pr-conversation.js.map +1 -0
  520. package/dist/src/pr-session-lifecycle.js +542 -0
  521. package/dist/src/pr-session-lifecycle.js.map +1 -0
  522. package/dist/src/preflight.js +341 -0
  523. package/dist/src/preflight.js.map +1 -0
  524. package/dist/src/priorresolve.js +275 -0
  525. package/dist/src/priorresolve.js.map +1 -0
  526. package/dist/src/qcrbin.js +180 -0
  527. package/dist/src/qcrbin.js.map +1 -0
  528. package/dist/src/recheck.js +1398 -0
  529. package/dist/src/recheck.js.map +1 -0
  530. package/dist/src/reconcile.js +820 -0
  531. package/dist/src/reconcile.js.map +1 -0
  532. package/dist/src/recordedfindings.js +15 -0
  533. package/dist/src/recordedfindings.js.map +1 -0
  534. package/dist/src/regression.js +546 -0
  535. package/dist/src/regression.js.map +1 -0
  536. package/dist/src/replay.js +598 -0
  537. package/dist/src/replay.js.map +1 -0
  538. package/dist/src/requiredpasses.js +113 -0
  539. package/dist/src/requiredpasses.js.map +1 -0
  540. package/dist/src/resumecheck.js +168 -0
  541. package/dist/src/resumecheck.js.map +1 -0
  542. package/dist/src/reviewcli.js +671 -0
  543. package/dist/src/reviewcli.js.map +1 -0
  544. package/dist/src/reviewcontext.js +22 -0
  545. package/dist/src/reviewcontext.js.map +1 -0
  546. package/dist/src/reviewdetach.js +574 -0
  547. package/dist/src/reviewdetach.js.map +1 -0
  548. package/dist/src/reviewhistory.js +284 -0
  549. package/dist/src/reviewhistory.js.map +1 -0
  550. package/dist/src/reviewhome.js +26 -0
  551. package/dist/src/reviewhome.js.map +1 -0
  552. package/dist/src/reviewmemory.js +110 -0
  553. package/dist/src/reviewmemory.js.map +1 -0
  554. package/dist/src/reviewrendercli.js +104 -0
  555. package/dist/src/reviewrendercli.js.map +1 -0
  556. package/dist/src/round-work.js +20 -0
  557. package/dist/src/round-work.js.map +1 -0
  558. package/dist/src/runhost.js +72 -0
  559. package/dist/src/runhost.js.map +1 -0
  560. package/dist/src/scanauth.js +25 -0
  561. package/dist/src/scanauth.js.map +1 -0
  562. package/dist/src/scanbox.js +846 -0
  563. package/dist/src/scanbox.js.map +1 -0
  564. package/dist/src/scancli.js +1074 -0
  565. package/dist/src/scancli.js.map +1 -0
  566. package/dist/src/scandriverlog.js +30 -0
  567. package/dist/src/scandriverlog.js.map +1 -0
  568. package/dist/src/session-bridge.js +75 -0
  569. package/dist/src/session-bridge.js.map +1 -0
  570. package/dist/src/session-command-payload.js +29 -0
  571. package/dist/src/session-command-payload.js.map +1 -0
  572. package/dist/src/session-command-supervisor.js +939 -0
  573. package/dist/src/session-command-supervisor.js.map +1 -0
  574. package/dist/src/sessionjsonl.js +85 -0
  575. package/dist/src/sessionjsonl.js.map +1 -0
  576. package/dist/src/sessions.js +541 -0
  577. package/dist/src/sessions.js.map +1 -0
  578. package/dist/src/setup/adapters.js +330 -0
  579. package/dist/src/setup/adapters.js.map +1 -0
  580. package/dist/src/setup/baselines.js +64 -0
  581. package/dist/src/setup/baselines.js.map +1 -0
  582. package/dist/src/setup/caserunner.js +554 -0
  583. package/dist/src/setup/caserunner.js.map +1 -0
  584. package/dist/src/setup/configcommit.js +358 -0
  585. package/dist/src/setup/configcommit.js.map +1 -0
  586. package/dist/src/setup/engine.js +672 -0
  587. package/dist/src/setup/engine.js.map +1 -0
  588. package/dist/src/setup/facts.js +593 -0
  589. package/dist/src/setup/facts.js.map +1 -0
  590. package/dist/src/setup/findings.js +948 -0
  591. package/dist/src/setup/findings.js.map +1 -0
  592. package/dist/src/setup/history.js +338 -0
  593. package/dist/src/setup/history.js.map +1 -0
  594. package/dist/src/setup/interview.js +185 -0
  595. package/dist/src/setup/interview.js.map +1 -0
  596. package/dist/src/setup/ledger.js +207 -0
  597. package/dist/src/setup/ledger.js.map +1 -0
  598. package/dist/src/setup/pins.js +43 -0
  599. package/dist/src/setup/pins.js.map +1 -0
  600. package/dist/src/setup/portrait.js +432 -0
  601. package/dist/src/setup/portrait.js.map +1 -0
  602. package/dist/src/setup/presentation.js +9 -0
  603. package/dist/src/setup/presentation.js.map +1 -0
  604. package/dist/src/setup/profile.js +417 -0
  605. package/dist/src/setup/profile.js.map +1 -0
  606. package/dist/src/setup/replay.js +240 -0
  607. package/dist/src/setup/replay.js.map +1 -0
  608. package/dist/src/setup/replaybuild.js +311 -0
  609. package/dist/src/setup/replaybuild.js.map +1 -0
  610. package/dist/src/setup/replayrun.js +806 -0
  611. package/dist/src/setup/replayrun.js.map +1 -0
  612. package/dist/src/setup/replaysource.js +169 -0
  613. package/dist/src/setup/replaysource.js.map +1 -0
  614. package/dist/src/setup/retune.js +333 -0
  615. package/dist/src/setup/retune.js.map +1 -0
  616. package/dist/src/setup/retunelog.js +51 -0
  617. package/dist/src/setup/retunelog.js.map +1 -0
  618. package/dist/src/setup/scan.js +725 -0
  619. package/dist/src/setup/scan.js.map +1 -0
  620. package/dist/src/setup/scanbuild.js +133 -0
  621. package/dist/src/setup/scanbuild.js.map +1 -0
  622. package/dist/src/setup/scanfacts.js +96 -0
  623. package/dist/src/setup/scanfacts.js.map +1 -0
  624. package/dist/src/setup/scanlease.js +251 -0
  625. package/dist/src/setup/scanlease.js.map +1 -0
  626. package/dist/src/setup/scanreport.js +289 -0
  627. package/dist/src/setup/scanreport.js.map +1 -0
  628. package/dist/src/setup/scanrun.js +1733 -0
  629. package/dist/src/setup/scanrun.js.map +1 -0
  630. package/dist/src/setup/scantree.js +257 -0
  631. package/dist/src/setup/scantree.js.map +1 -0
  632. package/dist/src/setup/stage2.js +617 -0
  633. package/dist/src/setup/stage2.js.map +1 -0
  634. package/dist/src/setup/stage2request.js +259 -0
  635. package/dist/src/setup/stage2request.js.map +1 -0
  636. package/dist/src/setup/survey.js +307 -0
  637. package/dist/src/setup/survey.js.map +1 -0
  638. package/dist/src/setup/surveyrun.js +549 -0
  639. package/dist/src/setup/surveyrun.js.map +1 -0
  640. package/dist/src/setup/textsniff.js +89 -0
  641. package/dist/src/setup/textsniff.js.map +1 -0
  642. package/dist/src/setup/trackersink.js +395 -0
  643. package/dist/src/setup/trackersink.js.map +1 -0
  644. package/dist/src/setup/triage.js +636 -0
  645. package/dist/src/setup/triage.js.map +1 -0
  646. package/dist/src/setup/tuning.js +585 -0
  647. package/dist/src/setup/tuning.js.map +1 -0
  648. package/dist/src/setup/unitcut.js +437 -0
  649. package/dist/src/setup/unitcut.js.map +1 -0
  650. package/dist/src/singleflight.js +72 -0
  651. package/dist/src/singleflight.js.map +1 -0
  652. package/dist/src/slack/announce.js +725 -0
  653. package/dist/src/slack/announce.js.map +1 -0
  654. package/dist/src/slack/api.js +227 -0
  655. package/dist/src/slack/api.js.map +1 -0
  656. package/dist/src/slack/messages.js +158 -0
  657. package/dist/src/slack/messages.js.map +1 -0
  658. package/dist/src/slack/origin.js +550 -0
  659. package/dist/src/slack/origin.js.map +1 -0
  660. package/dist/src/slack/policy.js +225 -0
  661. package/dist/src/slack/policy.js.map +1 -0
  662. package/dist/src/slack/reply.js +153 -0
  663. package/dist/src/slack/reply.js.map +1 -0
  664. package/dist/src/slack/router.js +189 -0
  665. package/dist/src/slack/router.js.map +1 -0
  666. package/dist/src/slack/socket.js +323 -0
  667. package/dist/src/slack/socket.js.map +1 -0
  668. package/dist/src/slack/types.js +11 -0
  669. package/dist/src/slack/types.js.map +1 -0
  670. package/dist/src/sqlitehint.js +30 -0
  671. package/dist/src/sqlitehint.js.map +1 -0
  672. package/dist/src/ssh-observation.js +25 -0
  673. package/dist/src/ssh-observation.js.map +1 -0
  674. package/dist/src/state.js +5869 -0
  675. package/dist/src/state.js.map +1 -0
  676. package/dist/src/statemachine.js +220 -0
  677. package/dist/src/statemachine.js.map +1 -0
  678. package/dist/src/stubs.js +52 -0
  679. package/dist/src/stubs.js.map +1 -0
  680. package/dist/src/takeover.js +506 -0
  681. package/dist/src/takeover.js.map +1 -0
  682. package/dist/src/takeoverpi.js +249 -0
  683. package/dist/src/takeoverpi.js.map +1 -0
  684. package/dist/src/terminal-failure.js +26 -0
  685. package/dist/src/terminal-failure.js.map +1 -0
  686. package/dist/src/thread-resolution-refusal.js +4 -0
  687. package/dist/src/thread-resolution-refusal.js.map +1 -0
  688. package/dist/src/tmux.js +126 -0
  689. package/dist/src/tmux.js.map +1 -0
  690. package/dist/src/trackerid.js +43 -0
  691. package/dist/src/trackerid.js.map +1 -0
  692. package/dist/src/transcriptindex.js +512 -0
  693. package/dist/src/transcriptindex.js.map +1 -0
  694. package/dist/src/tui/actions.js +404 -0
  695. package/dist/src/tui/actions.js.map +1 -0
  696. package/dist/src/tui/alerts.js +523 -0
  697. package/dist/src/tui/alerts.js.map +1 -0
  698. package/dist/src/tui/app.js +1835 -0
  699. package/dist/src/tui/app.js.map +1 -0
  700. package/dist/src/tui/archiveops.js +164 -0
  701. package/dist/src/tui/archiveops.js.map +1 -0
  702. package/dist/src/tui/autofile.js +191 -0
  703. package/dist/src/tui/autofile.js.map +1 -0
  704. package/dist/src/tui/board.js +1143 -0
  705. package/dist/src/tui/board.js.map +1 -0
  706. package/dist/src/tui/candidates.js +699 -0
  707. package/dist/src/tui/candidates.js.map +1 -0
  708. package/dist/src/tui/cellwidth.js +151 -0
  709. package/dist/src/tui/cellwidth.js.map +1 -0
  710. package/dist/src/tui/changelog-store.js +404 -0
  711. package/dist/src/tui/changelog-store.js.map +1 -0
  712. package/dist/src/tui/chord.js +26 -0
  713. package/dist/src/tui/chord.js.map +1 -0
  714. package/dist/src/tui/clipboard.js +81 -0
  715. package/dist/src/tui/clipboard.js.map +1 -0
  716. package/dist/src/tui/cmdbar.js +253 -0
  717. package/dist/src/tui/cmdbar.js.map +1 -0
  718. package/dist/src/tui/companion.js +1275 -0
  719. package/dist/src/tui/companion.js.map +1 -0
  720. package/dist/src/tui/companionplan.js +200 -0
  721. package/dist/src/tui/companionplan.js.map +1 -0
  722. package/dist/src/tui/companiontab.js +61 -0
  723. package/dist/src/tui/companiontab.js.map +1 -0
  724. package/dist/src/tui/confighome.js +105 -0
  725. package/dist/src/tui/confighome.js.map +1 -0
  726. package/dist/src/tui/connect.js +44 -0
  727. package/dist/src/tui/connect.js.map +1 -0
  728. package/dist/src/tui/deploy-ceremony.js +75 -0
  729. package/dist/src/tui/deploy-ceremony.js.map +1 -0
  730. package/dist/src/tui/deploy-drain.js +93 -0
  731. package/dist/src/tui/deploy-drain.js.map +1 -0
  732. package/dist/src/tui/deploy-process.js +234 -0
  733. package/dist/src/tui/deploy-process.js.map +1 -0
  734. package/dist/src/tui/deploytab.js +1263 -0
  735. package/dist/src/tui/deploytab.js.map +1 -0
  736. package/dist/src/tui/design.js +111 -0
  737. package/dist/src/tui/design.js.map +1 -0
  738. package/dist/src/tui/digesttab.js +148 -0
  739. package/dist/src/tui/digesttab.js.map +1 -0
  740. package/dist/src/tui/emptystate.js +32 -0
  741. package/dist/src/tui/emptystate.js.map +1 -0
  742. package/dist/src/tui/evidence.js +790 -0
  743. package/dist/src/tui/evidence.js.map +1 -0
  744. package/dist/src/tui/feed.js +415 -0
  745. package/dist/src/tui/feed.js.map +1 -0
  746. package/dist/src/tui/feedbacktab.js +907 -0
  747. package/dist/src/tui/feedbacktab.js.map +1 -0
  748. package/dist/src/tui/filters.js +72 -0
  749. package/dist/src/tui/filters.js.map +1 -0
  750. package/dist/src/tui/ghread.js +272 -0
  751. package/dist/src/tui/ghread.js.map +1 -0
  752. package/dist/src/tui/historypull.js +61 -0
  753. package/dist/src/tui/historypull.js.map +1 -0
  754. package/dist/src/tui/inboxpull.js +197 -0
  755. package/dist/src/tui/inboxpull.js.map +1 -0
  756. package/dist/src/tui/index.js +1425 -0
  757. package/dist/src/tui/index.js.map +1 -0
  758. package/dist/src/tui/issuesink.js +206 -0
  759. package/dist/src/tui/issuesink.js.map +1 -0
  760. package/dist/src/tui/jsonfile.js +32 -0
  761. package/dist/src/tui/jsonfile.js.map +1 -0
  762. package/dist/src/tui/keys.js +97 -0
  763. package/dist/src/tui/keys.js.map +1 -0
  764. package/dist/src/tui/layout.js +70 -0
  765. package/dist/src/tui/layout.js.map +1 -0
  766. package/dist/src/tui/learning.js +469 -0
  767. package/dist/src/tui/learning.js.map +1 -0
  768. package/dist/src/tui/lessonswitch.js +103 -0
  769. package/dist/src/tui/lessonswitch.js.map +1 -0
  770. package/dist/src/tui/livecontrol.js +167 -0
  771. package/dist/src/tui/livecontrol.js.map +1 -0
  772. package/dist/src/tui/memforward.js +310 -0
  773. package/dist/src/tui/memforward.js.map +1 -0
  774. package/dist/src/tui/memincident.js +287 -0
  775. package/dist/src/tui/memincident.js.map +1 -0
  776. package/dist/src/tui/memory.js +485 -0
  777. package/dist/src/tui/memory.js.map +1 -0
  778. package/dist/src/tui/memtab.js +513 -0
  779. package/dist/src/tui/memtab.js.map +1 -0
  780. package/dist/src/tui/metricstab.js +979 -0
  781. package/dist/src/tui/metricstab.js.map +1 -0
  782. package/dist/src/tui/model.js +104 -0
  783. package/dist/src/tui/model.js.map +1 -0
  784. package/dist/src/tui/mouse.js +83 -0
  785. package/dist/src/tui/mouse.js.map +1 -0
  786. package/dist/src/tui/msg.js +196 -0
  787. package/dist/src/tui/msg.js.map +1 -0
  788. package/dist/src/tui/nativecompanion.js +321 -0
  789. package/dist/src/tui/nativecompanion.js.map +1 -0
  790. package/dist/src/tui/openurl.js +17 -0
  791. package/dist/src/tui/openurl.js.map +1 -0
  792. package/dist/src/tui/paint.js +501 -0
  793. package/dist/src/tui/paint.js.map +1 -0
  794. package/dist/src/tui/panes.js +74 -0
  795. package/dist/src/tui/panes.js.map +1 -0
  796. package/dist/src/tui/pipane.js +166 -0
  797. package/dist/src/tui/pipane.js.map +1 -0
  798. package/dist/src/tui/poll-hooks.js +11 -0
  799. package/dist/src/tui/poll-hooks.js.map +1 -0
  800. package/dist/src/tui/poll.js +113 -0
  801. package/dist/src/tui/poll.js.map +1 -0
  802. package/dist/src/tui/proposals.js +715 -0
  803. package/dist/src/tui/proposals.js.map +1 -0
  804. package/dist/src/tui/prpause.js +132 -0
  805. package/dist/src/tui/prpause.js.map +1 -0
  806. package/dist/src/tui/reload.js +351 -0
  807. package/dist/src/tui/reload.js.map +1 -0
  808. package/dist/src/tui/remote.js +312 -0
  809. package/dist/src/tui/remote.js.map +1 -0
  810. package/dist/src/tui/rollover.js +767 -0
  811. package/dist/src/tui/rollover.js.map +1 -0
  812. package/dist/src/tui/scantab.js +1053 -0
  813. package/dist/src/tui/scantab.js.map +1 -0
  814. package/dist/src/tui/setupentry.js +81 -0
  815. package/dist/src/tui/setupentry.js.map +1 -0
  816. package/dist/src/tui/sidebar.js +602 -0
  817. package/dist/src/tui/sidebar.js.map +1 -0
  818. package/dist/src/tui/sshtriage.js +393 -0
  819. package/dist/src/tui/sshtriage.js.map +1 -0
  820. package/dist/src/tui/startup.js +172 -0
  821. package/dist/src/tui/startup.js.map +1 -0
  822. package/dist/src/tui/tabs.js +41 -0
  823. package/dist/src/tui/tabs.js.map +1 -0
  824. package/dist/src/tui/termmirror.js +217 -0
  825. package/dist/src/tui/termmirror.js.map +1 -0
  826. package/dist/src/tui/theme.js +95 -0
  827. package/dist/src/tui/theme.js.map +1 -0
  828. package/dist/src/tui/ticketstab.js +224 -0
  829. package/dist/src/tui/ticketstab.js.map +1 -0
  830. package/dist/src/tui/tmuxsocket.js +101 -0
  831. package/dist/src/tui/tmuxsocket.js.map +1 -0
  832. package/dist/src/tui/trackertickets.js +77 -0
  833. package/dist/src/tui/trackertickets.js.map +1 -0
  834. package/dist/src/tui/unwedge.js +245 -0
  835. package/dist/src/tui/unwedge.js.map +1 -0
  836. package/dist/src/tui/viewer.js +479 -0
  837. package/dist/src/tui/viewer.js.map +1 -0
  838. package/dist/src/tui/viewerinput.js +125 -0
  839. package/dist/src/tui/viewerinput.js.map +1 -0
  840. package/dist/src/updatenotice.js +143 -0
  841. package/dist/src/updatenotice.js.map +1 -0
  842. package/dist/src/usage.js +1007 -0
  843. package/dist/src/usage.js.map +1 -0
  844. package/dist/src/verdicts.js +3848 -0
  845. package/dist/src/verdicts.js.map +1 -0
  846. package/dist/src/version.js +58 -0
  847. package/dist/src/version.js.map +1 -0
  848. package/dist/src/wakes.js +411 -0
  849. package/dist/src/wakes.js.map +1 -0
  850. package/dist/src/webhook.js +725 -0
  851. package/dist/src/webhook.js.map +1 -0
  852. package/dist/src/webhookheal.js +238 -0
  853. package/dist/src/webhookheal.js.map +1 -0
  854. package/dist/src/wire.js +1801 -0
  855. package/dist/src/wire.js.map +1 -0
  856. package/dist/src/worker-session-host.js +531 -0
  857. package/dist/src/worker-session-host.js.map +1 -0
  858. package/dist/src/workerbreaker.js +112 -0
  859. package/dist/src/workerbreaker.js.map +1 -0
  860. package/dist/src/workercontrol.js +49 -0
  861. package/dist/src/workercontrol.js.map +1 -0
  862. package/dist/src/workerlease.js +351 -0
  863. package/dist/src/workerlease.js.map +1 -0
  864. package/dist/src/workerliveness.js +84 -0
  865. package/dist/src/workerliveness.js.map +1 -0
  866. package/dist/src/workerlog.js +65 -0
  867. package/dist/src/workerlog.js.map +1 -0
  868. package/dist/src/workerruntime.js +10 -0
  869. package/dist/src/workerruntime.js.map +1 -0
  870. package/dist/src/workersupervisor.js +1206 -0
  871. package/dist/src/workersupervisor.js.map +1 -0
  872. package/dist/src/workerusage.js +23 -0
  873. package/dist/src/workerusage.js.map +1 -0
  874. package/dist/src/workerworld.js +332 -0
  875. package/dist/src/workerworld.js.map +1 -0
  876. package/dist/src/worktrees.js +984 -0
  877. package/dist/src/worktrees.js.map +1 -0
  878. package/docker/Dockerfile +284 -0
  879. package/docker/Dockerfile.dockerignore +35 -0
  880. package/docker/README.md +221 -0
  881. package/docker/companion/Dockerfile +139 -0
  882. package/docker/companion/agents/evidence.md +34 -0
  883. package/docker/companion/agents/explore.md +26 -0
  884. package/docker/companion/agents/pass-survey.md +49 -0
  885. package/docker/companion/agents/study.md +26 -0
  886. package/docker/companion/entrypoint.sh +343 -0
  887. package/docker/companion/memory-health.mjs +269 -0
  888. package/docker/entrypoint.sh +782 -0
  889. package/docker/memory/Dockerfile +191 -0
  890. package/docker/memory/Dockerfile.dockerignore +20 -0
  891. package/docker/memory/README.md +60 -0
  892. package/docker/memory/entrypoint.sh +163 -0
  893. package/docker/setid/qcr-setid.c +109 -0
  894. package/docker/tunnel/Dockerfile.cloudflared +51 -0
  895. package/docker/tunnel/Dockerfile.ngrok +40 -0
  896. package/docker/tunnel/sidecar.mjs +411 -0
  897. package/docker/vm/tmux.conf +26 -0
  898. package/docker-compose.yml +348 -0
  899. package/extensions/cli/bash-guard-ambient.ts +71 -0
  900. package/extensions/cli/bash-guard.ts +12 -0
  901. package/extensions/cli/companion-guard.ts +8 -0
  902. package/extensions/cli/debug-round.ts +9 -0
  903. package/extensions/cli/inject-bridge.ts +8 -0
  904. package/extensions/cli/ledger-pin.ts +13 -0
  905. package/extensions/cli/lens.ts +22 -0
  906. package/extensions/cli/muted-theme.ts +9 -0
  907. package/extensions/cli/onboarding.ts +8 -0
  908. package/extensions/cli/setup.ts +8 -0
  909. package/extensions/cli/tui-view.ts +8 -0
  910. package/extensions/companion-guard.ts +411 -0
  911. package/extensions/debug-round.ts +281 -0
  912. package/extensions/inject-bridge.ts +630 -0
  913. package/extensions/inline-bash.ts +102 -0
  914. package/extensions/muted-theme.json +68 -0
  915. package/extensions/muted-theme.ts +23 -0
  916. package/extensions/onboarding.ts +233 -0
  917. package/extensions/setup-replay.ts +336 -0
  918. package/extensions/setup-scan.ts +1236 -0
  919. package/extensions/setup-triage.ts +190 -0
  920. package/extensions/setup.ts +1428 -0
  921. package/extensions/tui-view.ts +489 -0
  922. package/extensions/vendor/openviking-pi/LICENSE +201 -0
  923. package/extensions/vendor/openviking-pi/NOTICE +68 -0
  924. package/extensions/vendor/openviking-pi/README.md +550 -0
  925. package/extensions/vendor/openviking-pi/client.ts +588 -0
  926. package/extensions/vendor/openviking-pi/config.json +28 -0
  927. package/extensions/vendor/openviking-pi/config.ts +271 -0
  928. package/extensions/vendor/openviking-pi/index.ts +523 -0
  929. package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
  930. package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
  931. package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
  932. package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
  933. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
  934. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
  935. package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
  936. package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
  937. package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
  938. package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
  939. package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
  940. package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
  941. package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
  942. package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
  943. package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
  944. package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
  945. package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
  946. package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
  947. package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
  948. package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
  949. package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
  950. package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
  951. package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
  952. package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
  953. package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
  954. package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
  955. package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
  956. package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
  957. package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
  958. package/extensions/vendor/openviking-pi/recall.ts +149 -0
  959. package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
  960. package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
  961. package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
  962. package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
  963. package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
  964. package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
  965. package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
  966. package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
  967. package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
  968. package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
  969. package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
  970. package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
  971. package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
  972. package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
  973. package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
  974. package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
  975. package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  976. package/extensions/vendor/openviking-pi/sync.ts +163 -0
  977. package/extensions/vendor/openviking-pi/takeover.ts +36 -0
  978. package/extensions/vendor/openviking-pi/tools.ts +479 -0
  979. package/extensions/vendor/openviking-pi/upstream.json +48 -0
  980. package/package.json +138 -4
  981. package/profiles/default/README.md +21 -0
  982. package/profiles/default/addenda/.gitkeep +0 -0
  983. package/profiles/default/profile.toml +17 -0
  984. package/profiles/example-django/README.md +21 -0
  985. package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
  986. package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
  987. package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
  988. package/profiles/example-django/addenda/40-orm-usage.md +19 -0
  989. package/profiles/example-django/addenda/50-declared-shape.md +10 -0
  990. package/profiles/example-django/addenda/60-config-placement.md +14 -0
  991. package/profiles/example-django/heuristics.toml +24 -0
  992. package/profiles/example-django/profile.toml +9 -0
  993. package/profiles/fast/README.md +24 -0
  994. package/profiles/fast/profile.toml +59 -0
  995. package/profiles/minimal/README.md +17 -0
  996. package/profiles/minimal/addenda/.gitkeep +0 -0
  997. package/profiles/minimal/profile.toml +8 -0
  998. package/profiles/snappy/README.md +24 -0
  999. package/profiles/snappy/addenda/10-race-as-question.md +6 -0
  1000. package/profiles/snappy/profile.toml +46 -0
  1001. package/prompts/companion-day-open.md +46 -0
  1002. package/prompts/companion-first-run.md +104 -0
  1003. package/prompts/companion.md +821 -0
  1004. package/prompts/conversation-system.md +117 -0
  1005. package/prompts/eod-sweep.md +120 -0
  1006. package/prompts/onboarding.md +74 -0
  1007. package/prompts/pass-common.md +52 -0
  1008. package/prompts/pass-quality-common.md +22 -0
  1009. package/prompts/round-first.md +874 -0
  1010. package/prompts/round-follow-up.md +991 -0
  1011. package/prompts/round-re-review.md +889 -0
  1012. package/prompts/round-recheck.md +196 -0
  1013. package/prompts/round-staleness.md +856 -0
  1014. package/schemas/review-result.v1.json +391 -0
  1015. package/schemas/scan-result.v1.json +421 -0
  1016. package/schemas/version.v1.json +57 -0
  1017. package/scripts/approve-pr.sh +16 -0
  1018. package/scripts/build-stamp.mjs +67 -0
  1019. package/scripts/commit-verdict.sh +155 -0
  1020. package/scripts/companion-e2e.sh +250 -0
  1021. package/scripts/companion-memory-forward.mjs +1966 -0
  1022. package/scripts/config-fingerprint.sh +28 -0
  1023. package/scripts/deploy-manifest.mjs +141 -0
  1024. package/scripts/deploy.sh +1435 -0
  1025. package/scripts/directory-lock.mjs +193 -0
  1026. package/scripts/heartbeat-fresh.mjs +54 -0
  1027. package/scripts/install-locator.sh +328 -0
  1028. package/scripts/install-wrapper.sh +76 -0
  1029. package/scripts/memory-search-check.mjs +98 -0
  1030. package/scripts/mint-token.sh +212 -0
  1031. package/scripts/openviking-embedder.py +392 -0
  1032. package/scripts/openviking-ov-conf.mjs +161 -0
  1033. package/scripts/openviking-provision.mjs +1122 -0
  1034. package/scripts/openviking-reembed.mjs +1154 -0
  1035. package/scripts/post-review.sh +16 -0
  1036. package/scripts/pr-checks.sh +256 -0
  1037. package/scripts/pr-context.py +174 -0
  1038. package/scripts/pr-context.sh +63 -0
  1039. package/scripts/pr-self.sh +144 -0
  1040. package/scripts/pr-threads.sh +145 -0
  1041. package/scripts/qcr +15 -0
  1042. package/scripts/qcr-host.sh +769 -0
  1043. package/scripts/render-review.sh +24 -0
  1044. package/scripts/reply-thread.sh +16 -0
  1045. package/scripts/required-passes.py +112 -0
  1046. package/scripts/smoke-companion.sh +211 -0
  1047. package/scripts/tui-deploy-runner.mjs +61 -0
  1048. package/scripts/verify-dist.mjs +201 -0
  1049. package/scripts/vm-ghcr-login.sh +188 -0
  1050. package/seeds/agents/pass-baseline.md +61 -0
  1051. package/seeds/agents/pass-contracts.md +30 -0
  1052. package/seeds/agents/pass-data-safety.md +54 -0
  1053. package/seeds/agents/pass-delivery.md +37 -0
  1054. package/seeds/agents/pass-prod-risk.md +24 -0
  1055. package/seeds/agents/pass-security.md +31 -0
  1056. package/seeds/agents/pass-statelessness.md +27 -0
  1057. package/seeds/agents/pass-ui.md +23 -0
  1058. package/seeds/agents/pass-validate.md +50 -0
  1059. package/seeds/prompts/conversation-system.md +117 -0
  1060. package/seeds/prompts/eod-sweep.md +120 -0
  1061. package/seeds/prompts/pass-common.md +52 -0
  1062. package/skills/companion/adjudicate/SKILL.md +247 -0
  1063. package/skills/companion/evidence/SKILL.md +150 -0
  1064. package/skills/companion/land/SKILL.md +200 -0
  1065. package/skills/companion/replay/SKILL.md +138 -0
  1066. package/skills/companion/setup/SKILL.md +167 -0
  1067. package/skills/companion/survey/SKILL.md +160 -0
  1068. package/skills/selfimprove/README.md +34 -0
  1069. package/skills/selfimprove/SKILL.md +127 -0
  1070. package/skills/selfimprove/agents/openai.yaml +2 -0
  1071. package/skills/selfimprove/commands/selfimprove.md +6 -0
  1072. package/skills/selfimprove/references/fix-loop.md +49 -0
  1073. package/skills/selfimprove/references/ledger.md +20 -0
  1074. package/skills/selfimprove/references/setup.md +40 -0
  1075. package/src/anomalies.ts +566 -0
  1076. package/src/answerworker.ts +251 -0
  1077. package/src/appstamp.ts +65 -0
  1078. package/src/archive/admission.ts +3 -0
  1079. package/src/archive/archivedb.ts +548 -0
  1080. package/src/archive/archiver.ts +1963 -0
  1081. package/src/archive/archivewrite.ts +279 -0
  1082. package/src/archive/bundle.ts +333 -0
  1083. package/src/archive/dbmove.ts +911 -0
  1084. package/src/archive/duplicatecounts.ts +98 -0
  1085. package/src/archive/eligibility.ts +181 -0
  1086. package/src/archive/extract.ts +342 -0
  1087. package/src/archive/fsutil.ts +415 -0
  1088. package/src/archive/gzfile.ts +370 -0
  1089. package/src/archive/kinds.ts +21 -0
  1090. package/src/archive/livecheckpoint.ts +217 -0
  1091. package/src/archive/liveretention.ts +180 -0
  1092. package/src/archive/manifest.ts +519 -0
  1093. package/src/archive/overseer.ts +336 -0
  1094. package/src/archive/piglobal.ts +174 -0
  1095. package/src/archive/plan.ts +612 -0
  1096. package/src/archive/prarchive.ts +984 -0
  1097. package/src/archive/prfiles.ts +406 -0
  1098. package/src/archive/report.ts +252 -0
  1099. package/src/archive/restore.ts +330 -0
  1100. package/src/archive/rows.ts +187 -0
  1101. package/src/archive/seal.ts +630 -0
  1102. package/src/archive/status.ts +719 -0
  1103. package/src/archive/storepass.ts +581 -0
  1104. package/src/backfill.ts +362 -0
  1105. package/src/bench/cost.ts +113 -0
  1106. package/src/bench/exec.ts +90 -0
  1107. package/src/bench/values.ts +13 -0
  1108. package/src/botcontrol.ts +176 -0
  1109. package/src/botworker.ts +953 -0
  1110. package/src/capture.ts +321 -0
  1111. package/src/cases.ts +1789 -0
  1112. package/src/channelfs.ts +26 -0
  1113. package/src/checklist.ts +506 -0
  1114. package/src/ciretry.ts +141 -0
  1115. package/src/cli.ts +2271 -0
  1116. package/src/cliops.ts +384 -0
  1117. package/src/clonedir.ts +15 -0
  1118. package/src/commandeffects.ts +57 -0
  1119. package/src/commands.ts +1678 -0
  1120. package/src/companioninbox.ts +239 -0
  1121. package/src/compare.ts +957 -0
  1122. package/src/config.ts +2050 -0
  1123. package/src/configcheck.ts +133 -0
  1124. package/src/coreext.ts +47 -0
  1125. package/src/daemon.ts +1302 -0
  1126. package/src/debuground.ts +927 -0
  1127. package/src/debugroundpi.ts +262 -0
  1128. package/src/diagnostics.ts +14 -0
  1129. package/src/digest.ts +183 -0
  1130. package/src/dispatch.ts +2250 -0
  1131. package/src/engine/answerguard.ts +27 -0
  1132. package/src/engine/botsdk.ts +336 -0
  1133. package/src/engine/caserunner-sdk.ts +423 -0
  1134. package/src/engine/debugpi.ts +235 -0
  1135. package/src/eod.ts +669 -0
  1136. package/src/eodpinwatch.ts +556 -0
  1137. package/src/escalation.ts +1253 -0
  1138. package/src/eventloop.ts +1045 -0
  1139. package/src/execboundary.ts +633 -0
  1140. package/src/feedback.ts +1162 -0
  1141. package/src/findingscheck.ts +298 -0
  1142. package/src/fsdenied.ts +24 -0
  1143. package/src/ghread.ts +242 -0
  1144. package/src/github.ts +1497 -0
  1145. package/src/homes.ts +175 -0
  1146. package/src/hostroute.ts +53 -0
  1147. package/src/improvements.ts +787 -0
  1148. package/src/install.ts +1103 -0
  1149. package/src/installkind.ts +50 -0
  1150. package/src/ipc/capability.ts +333 -0
  1151. package/src/ipc/client.ts +269 -0
  1152. package/src/ipc/control.ts +90 -0
  1153. package/src/ipc/ghreadshim.ts +94 -0
  1154. package/src/ipc/launcher.ts +65 -0
  1155. package/src/ipc/permits.ts +362 -0
  1156. package/src/ipc/postshim.ts +164 -0
  1157. package/src/ipc/protocol.ts +342 -0
  1158. package/src/ipc/server.ts +371 -0
  1159. package/src/ipc/workerspec.ts +218 -0
  1160. package/src/learner/learner.ts +1655 -0
  1161. package/src/learner/miner.ts +1290 -0
  1162. package/src/learner/packet.ts +340 -0
  1163. package/src/learner/receipt.ts +168 -0
  1164. package/src/learner/status.ts +107 -0
  1165. package/src/ledgercheck.ts +157 -0
  1166. package/src/lessonledger.ts +703 -0
  1167. package/src/lessonselect.ts +888 -0
  1168. package/src/lessonshadow.ts +810 -0
  1169. package/src/levers.ts +393 -0
  1170. package/src/live/projection.ts +491 -0
  1171. package/src/live/publish.ts +92 -0
  1172. package/src/live/read.ts +86 -0
  1173. package/src/livecli.ts +336 -0
  1174. package/src/loginhint.ts +56 -0
  1175. package/src/loginrefresh/atomic.ts +76 -0
  1176. package/src/loginrefresh/lock.ts +104 -0
  1177. package/src/loginrefresh/main.ts +59 -0
  1178. package/src/loginrefresh/pirefresh.ts +57 -0
  1179. package/src/loginrefresh/refresher.ts +326 -0
  1180. package/src/loginrefresh/supervisor.ts +273 -0
  1181. package/src/loginrefresh/types.ts +35 -0
  1182. package/src/memory/context.ts +385 -0
  1183. package/src/memory/datafence.ts +9 -0
  1184. package/src/memory/delivery.ts +171 -0
  1185. package/src/memory/eventtags.ts +80 -0
  1186. package/src/memory/health.ts +953 -0
  1187. package/src/memory/import.ts +664 -0
  1188. package/src/memory/layout.ts +261 -0
  1189. package/src/memory/legacy-file.ts +104 -0
  1190. package/src/memory/lessons.ts +447 -0
  1191. package/src/memory/ovsession.ts +601 -0
  1192. package/src/memory/relayout.ts +335 -0
  1193. package/src/memory/roundactivity.ts +209 -0
  1194. package/src/memory/selection.ts +989 -0
  1195. package/src/memory/shadowstatus.ts +125 -0
  1196. package/src/memory/store.ts +275 -0
  1197. package/src/memory/textidentity.ts +8 -0
  1198. package/src/metrics.ts +1056 -0
  1199. package/src/missingconfig.ts +7 -0
  1200. package/src/npmverbs.ts +264 -0
  1201. package/src/onboard/choices.ts +222 -0
  1202. package/src/onboard/engine.ts +518 -0
  1203. package/src/onboard/launch.ts +115 -0
  1204. package/src/onboard/steps/appslug.ts +265 -0
  1205. package/src/onboard/steps/bringup.ts +482 -0
  1206. package/src/onboard/steps/compose.ts +459 -0
  1207. package/src/onboard/steps/configdelivery.ts +58 -0
  1208. package/src/onboard/steps/configfill.ts +1352 -0
  1209. package/src/onboard/steps/deploysh.ts +105 -0
  1210. package/src/onboard/steps/embeddings.ts +550 -0
  1211. package/src/onboard/steps/escalation.ts +640 -0
  1212. package/src/onboard/steps/githubapp.ts +1414 -0
  1213. package/src/onboard/steps/golive.ts +577 -0
  1214. package/src/onboard/steps/index.ts +71 -0
  1215. package/src/onboard/steps/ingress.ts +907 -0
  1216. package/src/onboard/steps/inventory.ts +526 -0
  1217. package/src/onboard/steps/llmauth.ts +1095 -0
  1218. package/src/onboard/steps/memory.ts +1290 -0
  1219. package/src/onboard/steps/memorykeys.ts +85 -0
  1220. package/src/onboard/steps/preflight.ts +191 -0
  1221. package/src/onboard/steps/proof.ts +624 -0
  1222. package/src/onboard/steps/ssh.ts +377 -0
  1223. package/src/onboard/steps/topology.ts +337 -0
  1224. package/src/onboard/steps/transport.ts +665 -0
  1225. package/src/onboard/steps/verifyup.ts +448 -0
  1226. package/src/onboard/steps/vmdeploy.ts +442 -0
  1227. package/src/onboard/toml.ts +50 -0
  1228. package/src/opencli.ts +591 -0
  1229. package/src/openpr.ts +568 -0
  1230. package/src/openviking/client.ts +1131 -0
  1231. package/src/openviking/context.ts +263 -0
  1232. package/src/openviking/embedder.ts +516 -0
  1233. package/src/openviking/gapsink.ts +63 -0
  1234. package/src/openviking/health.ts +83 -0
  1235. package/src/openviking/index.ts +119 -0
  1236. package/src/openviking/lessonfind.ts +92 -0
  1237. package/src/openviking/probe.ts +129 -0
  1238. package/src/openviking/proc.ts +110 -0
  1239. package/src/openviking/provisionreport.ts +160 -0
  1240. package/src/openviking/reembed.ts +412 -0
  1241. package/src/openviking/relayout.ts +177 -0
  1242. package/src/openviking/sidecar-main.ts +38 -0
  1243. package/src/openviking/sidecar.ts +106 -0
  1244. package/src/openviking/store.ts +2025 -0
  1245. package/src/openviking/supervisor.ts +474 -0
  1246. package/src/openviking/types.ts +174 -0
  1247. package/src/operator-recovery.ts +237 -0
  1248. package/src/operatormessage.ts +52 -0
  1249. package/src/outcomes.ts +1670 -0
  1250. package/src/piauthcopy.ts +12 -0
  1251. package/src/picli.ts +49 -0
  1252. package/src/picwd.ts +25 -0
  1253. package/src/pilogin.ts +37 -0
  1254. package/src/pingpong.ts +1339 -0
  1255. package/src/posting-journal.ts +549 -0
  1256. package/src/posting-outbox.ts +802 -0
  1257. package/src/posting-receipt.ts +324 -0
  1258. package/src/posting.ts +603 -0
  1259. package/src/postops.ts +832 -0
  1260. package/src/pr-activity.ts +20 -0
  1261. package/src/pr-author.ts +6 -0
  1262. package/src/pr-conversation.ts +432 -0
  1263. package/src/pr-session-lifecycle.ts +631 -0
  1264. package/src/preflight.ts +391 -0
  1265. package/src/priorresolve.ts +343 -0
  1266. package/src/qcrbin.ts +186 -0
  1267. package/src/recheck.ts +1775 -0
  1268. package/src/reconcile.ts +1030 -0
  1269. package/src/recordedfindings.ts +27 -0
  1270. package/src/regression.ts +833 -0
  1271. package/src/replay.ts +816 -0
  1272. package/src/requiredpasses.ts +148 -0
  1273. package/src/resumecheck.ts +246 -0
  1274. package/src/reviewcli.ts +787 -0
  1275. package/src/reviewcontext.ts +35 -0
  1276. package/src/reviewdetach.ts +734 -0
  1277. package/src/reviewhistory.ts +427 -0
  1278. package/src/reviewhome.ts +36 -0
  1279. package/src/reviewmemory.ts +133 -0
  1280. package/src/reviewrendercli.ts +139 -0
  1281. package/src/round-work.ts +42 -0
  1282. package/src/runhost.ts +108 -0
  1283. package/src/scanauth.ts +36 -0
  1284. package/src/scanbox.ts +1081 -0
  1285. package/src/scancli.ts +1296 -0
  1286. package/src/scandriverlog.ts +32 -0
  1287. package/src/session-bridge.ts +139 -0
  1288. package/src/session-command-payload.ts +35 -0
  1289. package/src/session-command-supervisor.ts +1142 -0
  1290. package/src/sessionjsonl.ts +88 -0
  1291. package/src/sessions.ts +659 -0
  1292. package/src/setup/adapters.ts +335 -0
  1293. package/src/setup/baselines.ts +110 -0
  1294. package/src/setup/caserunner.ts +611 -0
  1295. package/src/setup/configcommit.ts +443 -0
  1296. package/src/setup/engine.ts +1001 -0
  1297. package/src/setup/facts.ts +627 -0
  1298. package/src/setup/findings.ts +1264 -0
  1299. package/src/setup/history.ts +443 -0
  1300. package/src/setup/interview.ts +243 -0
  1301. package/src/setup/ledger.ts +324 -0
  1302. package/src/setup/pins.ts +66 -0
  1303. package/src/setup/portrait.ts +635 -0
  1304. package/src/setup/presentation.ts +18 -0
  1305. package/src/setup/profile.ts +508 -0
  1306. package/src/setup/replay.ts +370 -0
  1307. package/src/setup/replaybuild.ts +392 -0
  1308. package/src/setup/replayrun.ts +1096 -0
  1309. package/src/setup/replaysource.ts +198 -0
  1310. package/src/setup/retune.ts +415 -0
  1311. package/src/setup/retunelog.ts +95 -0
  1312. package/src/setup/scan.ts +1033 -0
  1313. package/src/setup/scanbuild.ts +168 -0
  1314. package/src/setup/scanfacts.ts +107 -0
  1315. package/src/setup/scanlease.ts +297 -0
  1316. package/src/setup/scanreport.ts +373 -0
  1317. package/src/setup/scanrun.ts +2268 -0
  1318. package/src/setup/scantree.ts +259 -0
  1319. package/src/setup/stage2.ts +926 -0
  1320. package/src/setup/stage2request.ts +347 -0
  1321. package/src/setup/survey.ts +458 -0
  1322. package/src/setup/surveyrun.ts +758 -0
  1323. package/src/setup/textsniff.ts +95 -0
  1324. package/src/setup/trackersink.ts +433 -0
  1325. package/src/setup/triage.ts +768 -0
  1326. package/src/setup/tuning.ts +766 -0
  1327. package/src/setup/unitcut.ts +523 -0
  1328. package/src/singleflight.ts +86 -0
  1329. package/src/slack/announce.ts +911 -0
  1330. package/src/slack/api.ts +303 -0
  1331. package/src/slack/messages.ts +288 -0
  1332. package/src/slack/origin.ts +599 -0
  1333. package/src/slack/policy.ts +354 -0
  1334. package/src/slack/reply.ts +201 -0
  1335. package/src/slack/router.ts +285 -0
  1336. package/src/slack/socket.ts +384 -0
  1337. package/src/slack/types.ts +108 -0
  1338. package/src/sqlitehint.ts +32 -0
  1339. package/src/ssh-observation.ts +30 -0
  1340. package/src/state.ts +8243 -0
  1341. package/src/statemachine.ts +369 -0
  1342. package/src/stubs.ts +68 -0
  1343. package/src/takeover.ts +634 -0
  1344. package/src/takeoverpi.ts +293 -0
  1345. package/src/terminal-failure.ts +41 -0
  1346. package/src/thread-resolution-refusal.ts +2 -0
  1347. package/src/tmux.ts +153 -0
  1348. package/src/trackerid.ts +41 -0
  1349. package/src/transcriptindex.ts +665 -0
  1350. package/src/tui/actions.ts +484 -0
  1351. package/src/tui/alerts.ts +666 -0
  1352. package/src/tui/app.ts +1979 -0
  1353. package/src/tui/archiveops.ts +192 -0
  1354. package/src/tui/autofile.ts +233 -0
  1355. package/src/tui/board.ts +1432 -0
  1356. package/src/tui/candidates.ts +889 -0
  1357. package/src/tui/cellwidth.ts +148 -0
  1358. package/src/tui/changelog-store.ts +503 -0
  1359. package/src/tui/chord.ts +25 -0
  1360. package/src/tui/clipboard.ts +92 -0
  1361. package/src/tui/cmdbar.ts +296 -0
  1362. package/src/tui/companion.ts +1427 -0
  1363. package/src/tui/companionplan.ts +247 -0
  1364. package/src/tui/companiontab.ts +83 -0
  1365. package/src/tui/confighome.ts +155 -0
  1366. package/src/tui/connect.ts +68 -0
  1367. package/src/tui/deploy-ceremony.ts +104 -0
  1368. package/src/tui/deploy-drain.ts +129 -0
  1369. package/src/tui/deploy-process.ts +265 -0
  1370. package/src/tui/deploytab.ts +1523 -0
  1371. package/src/tui/design.ts +125 -0
  1372. package/src/tui/digesttab.ts +214 -0
  1373. package/src/tui/emptystate.ts +39 -0
  1374. package/src/tui/evidence.ts +999 -0
  1375. package/src/tui/feed.ts +604 -0
  1376. package/src/tui/feedbacktab.ts +1077 -0
  1377. package/src/tui/filters.ts +90 -0
  1378. package/src/tui/ghread.ts +321 -0
  1379. package/src/tui/historypull.ts +81 -0
  1380. package/src/tui/inboxpull.ts +236 -0
  1381. package/src/tui/index.ts +1565 -0
  1382. package/src/tui/issuesink.ts +238 -0
  1383. package/src/tui/jsonfile.ts +42 -0
  1384. package/src/tui/keys.ts +109 -0
  1385. package/src/tui/layout.ts +108 -0
  1386. package/src/tui/learning.ts +627 -0
  1387. package/src/tui/lessonswitch.ts +118 -0
  1388. package/src/tui/livecontrol.ts +228 -0
  1389. package/src/tui/memforward.ts +443 -0
  1390. package/src/tui/memincident.ts +363 -0
  1391. package/src/tui/memory.ts +620 -0
  1392. package/src/tui/memtab.ts +606 -0
  1393. package/src/tui/metricstab.ts +1177 -0
  1394. package/src/tui/model.ts +131 -0
  1395. package/src/tui/mouse.ts +98 -0
  1396. package/src/tui/msg.ts +268 -0
  1397. package/src/tui/nativecompanion.ts +417 -0
  1398. package/src/tui/openurl.ts +21 -0
  1399. package/src/tui/paint.ts +511 -0
  1400. package/src/tui/panes.ts +98 -0
  1401. package/src/tui/pipane.ts +219 -0
  1402. package/src/tui/poll-hooks.ts +12 -0
  1403. package/src/tui/poll.ts +147 -0
  1404. package/src/tui/proposals.ts +838 -0
  1405. package/src/tui/prpause.ts +150 -0
  1406. package/src/tui/reload.ts +436 -0
  1407. package/src/tui/remote.ts +413 -0
  1408. package/src/tui/rollover.ts +969 -0
  1409. package/src/tui/scantab.ts +1290 -0
  1410. package/src/tui/setupentry.ts +93 -0
  1411. package/src/tui/sidebar.ts +749 -0
  1412. package/src/tui/sshtriage.ts +510 -0
  1413. package/src/tui/startup.ts +231 -0
  1414. package/src/tui/tabs.ts +45 -0
  1415. package/src/tui/termmirror.ts +224 -0
  1416. package/src/tui/theme.ts +95 -0
  1417. package/src/tui/ticketstab.ts +310 -0
  1418. package/src/tui/tmuxsocket.ts +129 -0
  1419. package/src/tui/trackertickets.ts +84 -0
  1420. package/src/tui/unwedge.ts +286 -0
  1421. package/src/tui/viewer.ts +575 -0
  1422. package/src/tui/viewerinput.ts +173 -0
  1423. package/src/updatenotice.ts +178 -0
  1424. package/src/usage.ts +1154 -0
  1425. package/src/verdicts.ts +4689 -0
  1426. package/src/version.ts +80 -0
  1427. package/src/wakes.ts +476 -0
  1428. package/src/webhook.ts +942 -0
  1429. package/src/webhookheal.ts +276 -0
  1430. package/src/wire.ts +2153 -0
  1431. package/src/worker-session-host.ts +602 -0
  1432. package/src/workerbreaker.ts +156 -0
  1433. package/src/workercontrol.ts +104 -0
  1434. package/src/workerlease.ts +441 -0
  1435. package/src/workerliveness.ts +103 -0
  1436. package/src/workerlog.ts +79 -0
  1437. package/src/workerruntime.ts +30 -0
  1438. package/src/workersupervisor.ts +1538 -0
  1439. package/src/workerusage.ts +42 -0
  1440. package/src/workerworld.ts +427 -0
  1441. package/src/worktrees.ts +1138 -0
  1442. package/tests/fixtures/regression-cases.json +795 -0
  1443. package/tests/fixtures/seed-manifest.json +19 -0
package/src/tui/app.ts ADDED
@@ -0,0 +1,1979 @@
1
+ // qcr tui — the app loop: terminal setup/teardown, poller wiring, key
2
+ // routing, repaint. Screens and logic are pure modules; this file owns the
3
+ // side effects. The board, detail and events live here; quick-message and
4
+ // the tabs plug in via AppHooks.
5
+ //
6
+ // ONE LAYOUT (AGENTS.md invariant 10): the tab on the left, the companion
7
+ // column on the right, on every frame. Nothing here may take the whole
8
+ // terminal, fold the column away or paint without it — see `sidebarWidth`
9
+ // and `repaint`, and tests/tui-fixed-layout.test.ts.
10
+ import type { WhyPayload } from "../cli.js";
11
+ import type { TuiConfig } from "../config.js";
12
+ import type { AlertBadge } from "./alerts.js";
13
+ import type { MessageOutcome } from "./msg.js";
14
+ import {
15
+ boardBarHeight,
16
+ boardKeys,
17
+ overlayInputBox,
18
+ piPaneSize,
19
+ renderBoard,
20
+ type BoardAction,
21
+ type BoardView,
22
+ } from "./board.js";
23
+ import { barHitKey, NO_LIVE_KEYS, registeredKeys, type LiveKeys } from "./cmdbar.js";
24
+ import { decodeInputKey, decodeKey, type BoardKey } from "./keys.js";
25
+ import type { Tab } from "./tabs.js";
26
+ import { visiblePrs, type BoardFilter } from "./filters.js";
27
+ import { formatEvent, moveSelection, reselect, type EventLine } from "./model.js";
28
+ import { TripleCtrlC } from "./chord.js";
29
+ import { MOUSE_OFF, MOUSE_ON, MouseDecoder, hitTest, type MouseEvent, type Zone } from "./mouse.js";
30
+ import {
31
+ ALT_SCREEN_OFF,
32
+ ALT_SCREEN_ON,
33
+ CLEAR,
34
+ Frame,
35
+ accentFrameRect,
36
+ cleanCopiedBlock,
37
+ cloneFrame,
38
+ composeColumn,
39
+ fit,
40
+ sliceDisplay,
41
+ spliceDisplay,
42
+ stripAnsi,
43
+ } from "./paint.js";
44
+ import { ACCENT, RESET, SELECTION } from "./design.js";
45
+ import { ClipboardSink } from "./clipboard.js";
46
+ import { PiMirror } from "./pipane.js";
47
+ import { TranscriptViewer, paneSurface, surfaceSession, type PaneSurface } from "./viewer.js";
48
+ import type { ControlOutcome, ControlRequest } from "./livecontrol.js";
49
+ import type { Command } from "../commands.js";
50
+ import { newViewerInput, viewerInputKey, type ViewerInput } from "./viewerinput.js";
51
+ import { PANE_ESCAPE, isEndKey, keysToTmux } from "./termmirror.js";
52
+ import { Poller } from "./poll.js";
53
+ import { parseRoundProgress } from "@quickchatai/pi-quickreview/src/round-progress.js";
54
+ import { prUrl } from "./theme.js";
55
+ import {
56
+ type CompanionSidebar,
57
+ paintSidebar,
58
+ sidebarHit,
59
+ sidebarResizeHit,
60
+ sidebarWidth,
61
+ } from "./sidebar.js";
62
+ import {
63
+ eventsArgv,
64
+ progressArgv,
65
+ sendKeysArgv,
66
+ whyArgv,
67
+ type PiTerminalSession,
68
+ type SshRunner,
69
+ } from "./remote.js";
70
+
71
+ export type { Tab } from "./tabs.js";
72
+
73
+ /**
74
+ * One installed surface (every tab but the board, which the App renders
75
+ * itself). The install functions in memtab / digesttab / ticketstab /
76
+ * deploytab / metricstab / feedbacktab / scantab each build one of these over their own
77
+ * state and register it under `AppHooks.tabs`.
78
+ */
79
+ export interface TabHooks {
80
+ /** The tab's frame at the INNER width — the companion column composes
81
+ * beside it; a bare, sidebar-less string is not a type a tab can return. */
82
+ screen: (app: App, cols: number, rows: number) => Frame;
83
+ /** Key handling; true when the tab consumed the key. */
84
+ key: (app: App, key: BoardKey) => boolean;
85
+ /** Mouse click (1-based terminal coords) — rows select, [buttons] fire
86
+ * verbs; true when consumed. */
87
+ click?: (app: App, x: number, y: number) => boolean;
88
+ /**
89
+ * What the tab answers to RIGHT NOW: the bar it paints and the
90
+ * keys it paints beside their objects. The App registers exactly this set —
91
+ * a key outside it reaches the companion column — so a tab that paints a
92
+ * button it will not answer is lying to the operator AND stealing the
93
+ * keystroke. Derived from the tab's state, never a constant.
94
+ */
95
+ keys: (app: App) => LiveKeys;
96
+ }
97
+
98
+ export interface AppHooks {
99
+ /** The seven installed surfaces, by tab. The board is not here: the App owns
100
+ * its render, its keys and its `boardKeys` (src/tui/board.ts). */
101
+ tabs: Partial<Record<Exclude<Tab, "board">, TabHooks>>;
102
+ // There is deliberately NO `attach` hook: the TUI has ONE layout — tab on
103
+ // the left, companion column on the right — and nothing may take the whole
104
+ // terminal (AGENTS.md invariant 10). The board's pi
105
+ // pane and the companion column are the only terminals, both embedded.
106
+ /** Open the quick-message box for the given session. */
107
+ message?: (session: string, app: App) => void;
108
+ /** Deliver a submitted quick-message (inject write + confirmation);
109
+ * resolves with how the delivery ended. */
110
+ sendMessage?: (session: string, text: string, app: App) => void | Promise<MessageOutcome>;
111
+ /** Cancel the PR's running round and re-queue it, via the daemon's
112
+ * `commands/` dir. The board's one write action beyond wakes — see
113
+ * src/tui/unwedge.ts for why it exists and what bounds it. */
114
+ unwedge?: (pr: number, app: App) => void;
115
+ /** Send an operator control (steer, follow-up, abort-round, message) to a
116
+ * PR's host through the operator command channel and resolve with how the
117
+ * daemon answered. The viewer's input line and `msg.ts` ride it
118
+ * (src/tui/livecontrol.ts). */
119
+ liveControl?: (req: ControlRequest, app: App) => Promise<ControlOutcome>;
120
+ /** Forward one command of the companion's tier (every operator verb, root
121
+ * the companion boundary), already validated and authorised, down the same
122
+ * operator route, and resolve with the daemon's answer. */
123
+ operatorCommand?: (command: Command, app: App) => Promise<ControlOutcome>;
124
+ /** The board's `I` — ask the companion to run its setup skill
125
+ * (an inject into its own column, src/tui/setupentry.ts) and focus it. */
126
+ setup?: (app: App) => void;
127
+ /** Open a URL in the user's browser (default: xdg-open, detached). */
128
+ openUrl?: (url: string) => void;
129
+ /** First-launch marker (read, write). Default: state/tui-help-shown. */
130
+ helpSeen?: { get(): boolean; set(): void };
131
+ /** Called after every poll and every events refresh — the ambient
132
+ * feed writer hangs off this (daemon-owned cadence). */
133
+ onPoll?: (app: App) => void;
134
+ /** The companion was asked for while its startup is down — re-run
135
+ * that startup (build/up/rollover/spawn) and focus the column on success. */
136
+ companionEnsure?: (app: App) => Promise<void>;
137
+ /** Start ./scripts/deploy.sh release <version> after the deploy tab has
138
+ * engaged the daemon drain and observed a fresh zero-running-round snapshot.
139
+ * Streams output back into the tab; the handle aborts a stuck release. */
140
+ deployRun?: (
141
+ app: App,
142
+ runId: string,
143
+ version: string,
144
+ onData: (chunk: string) => void,
145
+ onExit: (code: number | null) => void,
146
+ ) => { kill(): void };
147
+ /** Stop/start the VM app container, streamed like deployRun (S S or the
148
+ * power button on the deploy tab). */
149
+ powerRun?: (
150
+ app: App,
151
+ action: "stop" | "start",
152
+ onData: (chunk: string) => void,
153
+ onExit: (code: number | null) => void,
154
+ ) => { kill(): void };
155
+ /** Re-establish the VM ssh master on the operator's terminal (key s) —
156
+ * the one native GCP-passphrase prompt. */
157
+ reconnect?: (app: App) => void;
158
+ }
159
+
160
+ /**
161
+ * Decoded verbs the BOARD owns. `handleInput` refuses them off the board —
162
+ * see the comment at the guard.
163
+ *
164
+ * The membership test is "is this key advertised by the board's command bar"
165
+ * (`boardKeys`, src/tui/board.ts), NOT "does it read the selection". Most
166
+ * members happen to read it, but the reason they are here is that they are
167
+ * the board's bar, they are unregistered elsewhere, and the board is where an
168
+ * operator looks for them. Getting that reason wrong is how the next verb
169
+ * ends up in or out by accident — a missing `unwedge` would let `u` from
170
+ * another tab unwedge a PR nobody was looking at.
171
+ */
172
+ const BOARD_ONLY_VERBS: ReadonlySet<string> = new Set([
173
+ "attach",
174
+ "enter",
175
+ "message",
176
+ "github",
177
+ "events",
178
+ "unwedge",
179
+ ]);
180
+
181
+ /** The frame an UNWIRED tab paints: its notice on the first body row, at the
182
+ * inner width, so the companion column still composes beside it. */
183
+ function placeholderFrame(cols: number, rows: number, notice: string): Frame {
184
+ const frame = new Frame(cols, rows);
185
+ frame.set(Math.min(1, Math.max(0, rows - 1)), fit(notice, cols));
186
+ return frame;
187
+ }
188
+
189
+ export class App {
190
+ readonly view: BoardView = {
191
+ health: { kind: "unreachable", sinceS: Number.POSITIVE_INFINITY },
192
+ // UNKNOWN until a heartbeat proves one: a default of `true` would paint a
193
+ // DRY-RUN badge on a live installation.
194
+ dryRun: undefined,
195
+ events: [],
196
+ selected: 0,
197
+ eventsFocus: false,
198
+ eventsScroll: 0,
199
+ help: false,
200
+ };
201
+ tab: Tab = "board";
202
+ readonly hooks: AppHooks;
203
+ /** The companion sidebar — a right-hand column on every tab.
204
+ * Wired by index.ts; absent in bare test harnesses (no column painted). */
205
+ sidebar?: CompanionSidebar;
206
+ readonly poller: Poller;
207
+ /** Live mirror of the pi an operator is inside for the selected PR — its
208
+ * debug round or its takeover (board's lower region). */
209
+ readonly pi: PiMirror;
210
+ /** The same region for a worker-hosted PR: its transcript, read-only. Which
211
+ * of the two a PR gets is `surfaceOf` — status.json's `runtime`, no toggle. */
212
+ readonly viewer: TranscriptViewer;
213
+ /** Raw event rows behind view.events — the ambient feed writes these
214
+ * verbatim, so the companion sees what the daemon logged, not our prose. */
215
+ rawEvents: Array<{ ts: string; kind: string; pr: number | null; payload: string | null }> = [];
216
+ private selectedPr: number | undefined;
217
+ private detailSeq = 0;
218
+ private progressSeq = 0;
219
+ private eventsSeq = 0;
220
+ /** Stale-while-revalidate detail cache: paint the last known payload
221
+ * instantly on selection, refresh over ssh behind it. */
222
+ private readonly detailCache = new Map<number, { payload: WhyPayload; fetched_at: number }>();
223
+ private static readonly DETAIL_TTL_MS = 60_000;
224
+ /** The newest `why` read started per PR (its detailSeq), until it settles.
225
+ * Only that read may write the PR's cache entry — an older one resolving
226
+ * late must not stamp its older payload fresh — and while it is pending a
227
+ * poll does not stack another. Invalidation drops it, so a read begun
228
+ * before the invalidation cannot repopulate the cache either. */
229
+ private readonly detailPending = new Map<number, number>();
230
+ /** When a PR's last `why` read failed: a poll does not retry it sooner than
231
+ * DETAIL_RETRY_MS (the read reaches GitHub; a 3s retry loop against a rate
232
+ * limit is the wrong answer). Selection and `r` are not held by it. */
233
+ private readonly detailFailedAt = new Map<number, number>();
234
+ private static readonly DETAIL_RETRY_MS = 15_000;
235
+ private quitResolve: (() => void) | undefined;
236
+ private suspended = false;
237
+ private readonly onData = (buf: Buffer): void => this.handleInput(buf);
238
+ private readonly onResize = (): void => this.repaint();
239
+
240
+ constructor(
241
+ readonly cfg: TuiConfig,
242
+ readonly ssh: SshRunner,
243
+ private readonly out: NodeJS.WriteStream = process.stdout,
244
+ private readonly input: NodeJS.ReadStream = process.stdin,
245
+ hooks: Partial<AppHooks> = {},
246
+ private readonly now: () => Date = () => new Date(),
247
+ ) {
248
+ this.hooks = { ...hooks, tabs: hooks.tabs ?? {} };
249
+ this.poller = new Poller(cfg, ssh, () => this.onPoll(), now);
250
+ this.pi = new PiMirror(
251
+ cfg,
252
+ ssh,
253
+ () => {
254
+ this.syncPiPane();
255
+ // The session we were typing into went away — say so and hand the
256
+ // keyboard back rather than swallowing keystrokes into nothing.
257
+ const gone = this.view.pi?.session;
258
+ if (this.view.piFocus && this.view.pi?.missing && gone !== undefined)
259
+ this.blurPiPane(`pi session ${gone} is gone — keyboard back on the board`);
260
+ this.repaint();
261
+ },
262
+ 300,
263
+ (pr) => this.sessionFor(pr),
264
+ );
265
+ this.viewer = new TranscriptViewer(cfg, ssh, () => {
266
+ this.syncPiPane();
267
+ this.repaint();
268
+ });
269
+ }
270
+
271
+ // ---- which surface the PR pane is -----------------------------------------
272
+
273
+ /** What the pane shows for `pr`, from the PR's `runtime` in status.json — a
274
+ * pi terminal (`debug`, `takeover`) or, for every other runtime, the
275
+ * transcript viewer. */
276
+ private surfaceOf(pr: number): PaneSurface {
277
+ return paneSurface(this.view.snapshot?.prs.find((p) => p.number === pr)?.runtime, pr);
278
+ }
279
+
280
+ /** The pi terminal a pane types into and captures; undefined for the viewer
281
+ * (the bot runs no `pr-<n>` tmux session). */
282
+ private sessionFor(pr: number): PiTerminalSession | undefined {
283
+ return surfaceSession(this.surfaceOf(pr));
284
+ }
285
+
286
+ /** The PR's `runtime` in status.json, as the daemon last reported it. */
287
+ runtimeOf(pr: number): string | undefined {
288
+ return this.view.snapshot?.prs.find((p) => p.number === pr)?.runtime;
289
+ }
290
+
291
+ /** A round (or an answer) is running for the PR: the viewer's ⏎ steers it. */
292
+ private roundRunning(pr: number): boolean {
293
+ const runtime = this.runtimeOf(pr);
294
+ return runtime === "worker" || runtime === "answer";
295
+ }
296
+
297
+ /** The viewer's input line per PR: a draft survives moving to another PR. */
298
+ private readonly viewerInputs = new Map<number, ViewerInput>();
299
+
300
+ private viewerInputOf(pr: number): ViewerInput {
301
+ return this.viewerInputs.get(pr) ?? newViewerInput();
302
+ }
303
+
304
+ private viewerOn(pr: number | undefined): pr is number {
305
+ return pr !== undefined && this.surfaceOf(pr).kind === "viewer";
306
+ }
307
+
308
+ /** Read the PR pane's source now: the transcript logs or the tmux capture. */
309
+ private refreshPane(pr: number | undefined): Promise<void> {
310
+ if (pr === undefined) return Promise.resolve();
311
+ return this.viewerOn(pr) ? this.viewer.refresh(pr) : this.pi.refresh(pr);
312
+ }
313
+
314
+ /** Selection moved: read the PR pane's source behind a debounce. */
315
+ private selectPane(pr: number | undefined): void {
316
+ if (this.viewerOn(pr)) this.viewer.select(pr);
317
+ else this.pi.select(pr);
318
+ }
319
+
320
+ private paneOffset(pr: number): number {
321
+ return this.viewerOn(pr) ? this.viewer.offsetFor(pr) : this.pi.offsetFor(pr);
322
+ }
323
+
324
+ private setPaneOffset(pr: number, n: number): void {
325
+ if (this.viewerOn(pr)) this.viewer.setOffset(pr, n);
326
+ else this.pi.setOffset(pr, n);
327
+ }
328
+
329
+ // ---- the pi pane as an embedded terminal --------------------------------
330
+
331
+ /** The session the pane types into while focused (locked at focus time). */
332
+ private piFocusPr: number | undefined;
333
+ /** Whether that pane was the viewer when it took the keyboard: a PR whose
334
+ * runtime flips surface under the operator's hands gets the keyboard back. */
335
+ private piFocusViewer = false;
336
+ /** While focused the capture runs far faster than the poll tick, so typing
337
+ * echoes back at something close to terminal speed. */
338
+ private piTimer: NodeJS.Timeout | undefined;
339
+ private static readonly PI_FOCUS_POLL_MS = 400;
340
+
341
+ /** Hand the keyboard to the pi pane (click, or a wheel-driven focus). */
342
+ focusPiPane(): void {
343
+ const pr = this.panePr();
344
+ if (pr === undefined) return;
345
+ if (this.view.piFocus && this.piFocusPr === pr) return;
346
+ this.piFocusPr = pr;
347
+ this.piFocusViewer = this.viewerOn(pr);
348
+ this.view.piFocus = true;
349
+ this.sidebar?.blur(); // one keyboard owner at a time
350
+ this.syncPiPane();
351
+ void this.refreshPane(pr);
352
+ if (!this.piTimer) {
353
+ const t = setInterval(() => {
354
+ if (this.suspended || this.tab !== "board" || !this.view.piFocus) return;
355
+ void this.refreshPane(this.piFocusPr);
356
+ }, App.PI_FOCUS_POLL_MS);
357
+ t.unref?.();
358
+ this.piTimer = t;
359
+ }
360
+ this.repaint();
361
+ }
362
+
363
+ /** Ctrl-q, a click outside the pane, or a dead session: back to the list. */
364
+ blurPiPane(notice?: string): void {
365
+ if (this.piTimer) clearInterval(this.piTimer);
366
+ this.piTimer = undefined;
367
+ if (!this.view.piFocus) return;
368
+ this.view.piFocus = false;
369
+ this.piFocusPr = undefined;
370
+ this.syncPiPane();
371
+ if (notice !== undefined) this.view.noticeLine = notice;
372
+ this.selectPane(this.selectedPr); // follow resumes where the list is
373
+ }
374
+
375
+ /** True while the pane owns the keyboard (tests + hot-reload read this). */
376
+ piFocused(): boolean {
377
+ return this.view.piFocus === true;
378
+ }
379
+
380
+ /** Forward one raw stdin chunk into the focused session, then re-capture so
381
+ * the echo lands immediately instead of on the next tick. Typing also snaps
382
+ * the pane back to the live tail — you cannot type into history. */
383
+ private sendKeysToPane(buf: Buffer): void {
384
+ const pr = this.piFocusPr;
385
+ if (pr === undefined) return;
386
+ const groups = keysToTmux(buf);
387
+ if (groups.length === 0) return;
388
+ if (this.paneOffset(pr) !== 0) {
389
+ this.setPaneOffset(pr, 0);
390
+ this.syncPiPane();
391
+ this.repaint();
392
+ }
393
+ // One queue for every keystroke into a pane: each chunk is its own ssh
394
+ // round trip, and unqueued a slow "a" lets the Enter typed after it reach
395
+ // tmux first and submit pi's input without it. For the same reason a
396
+ // chunk that did not arrive drops everything queued behind it — an Enter
397
+ // must never submit input with a hole in it — and says so; the next
398
+ // keystroke typed once the queue has drained starts afresh.
399
+ if (this.paneSendsQueued === 0) this.paneSendDropped = false;
400
+ this.paneSendsQueued++;
401
+ this.paneSendTail = this.paneSendTail
402
+ .then(async () => {
403
+ for (const parts of groups) {
404
+ if (this.paneSendDropped) return;
405
+ // Only a pi terminal takes keys. A PR whose debug round or takeover
406
+ // ended under the operator's hands has none: drop, and say so.
407
+ const session = this.sessionFor(pr);
408
+ if (session === undefined) {
409
+ this.paneSendDropped = true;
410
+ this.notice(`✖ #${pr}: no pi terminal any more — input dropped`);
411
+ return;
412
+ }
413
+ const res = await this.ssh(this.cfg.ssh_host, sendKeysArgv(this.cfg, session, parts), {
414
+ timeoutMs: 5_000,
415
+ });
416
+ if (!res.ok) {
417
+ this.paneSendDropped = true;
418
+ this.notice(
419
+ `✖ ${session}: keys not delivered (${res.stderr.trim() || `exit ${res.code}`}) — input after them dropped`,
420
+ );
421
+ return;
422
+ }
423
+ }
424
+ })
425
+ .catch((err: unknown) => {
426
+ this.paneSendDropped = true;
427
+ this.notice(
428
+ `✖ #${pr}: keys not delivered (${err instanceof Error ? err.message : String(err)}) — input after them dropped`,
429
+ );
430
+ })
431
+ .finally(() => {
432
+ this.paneSendsQueued--;
433
+ void this.pi.refresh(pr);
434
+ });
435
+ }
436
+ private paneSendTail: Promise<void> = Promise.resolve();
437
+ /** Sends queued and not yet finished. */
438
+ private paneSendsQueued = 0;
439
+ /** A send in the current queue failed: the rest of it is not sent. */
440
+ private paneSendDropped = false;
441
+
442
+ /** Wheel over the pane: move its scrollback window, re-capture with history.
443
+ * Wheeling back to the bottom returns it to the live tail. */
444
+ private scrollPiPane(notches: number): void {
445
+ const pr = this.panePr();
446
+ if (pr === undefined) return;
447
+ if (this.viewerOn(pr)) this.viewer.scroll(pr, notches);
448
+ else this.pi.scroll(pr, notches);
449
+ this.syncPiPane();
450
+ this.repaint();
451
+ void this.refreshPane(pr);
452
+ }
453
+
454
+ /** The PR whose session the pane is showing. While the pane has the keyboard
455
+ * this is LOCKED — a poll that reorders the list must never redirect the
456
+ * operator's typing to another PR's session. */
457
+ private panePr(): number | undefined {
458
+ return this.view.piFocus ? this.piFocusPr : this.selectedPr;
459
+ }
460
+
461
+ /** Point the board's pane at whatever it should be showing right now (cache
462
+ * first, so switching back paints instantly). */
463
+ private syncPiPane(): void {
464
+ const pr = this.panePr();
465
+ this.view.piPr = pr;
466
+ this.view.pi = this.viewerOn(pr) ? this.viewer.view(pr) : this.pi.view(pr);
467
+ // The input line exists on the viewer's pane only: a pi terminal gets
468
+ // none (the PR-pane rule).
469
+ this.view.viewerInput = this.viewerOn(pr)
470
+ ? {
471
+ ...this.viewerInputOf(pr),
472
+ pr,
473
+ running: this.roundRunning(pr),
474
+ focused: this.view.piFocus === true && this.piFocusPr === pr,
475
+ }
476
+ : undefined;
477
+ }
478
+
479
+ /** Selection moved: paint the cached mirror at once, capture behind it.
480
+ * A focused pane ignores this — follow is locked until Ctrl-q. */
481
+ private followSelection(): void {
482
+ this.syncPiPane();
483
+ if (this.view.piFocus) return;
484
+ this.selectPane(this.selectedPr);
485
+ }
486
+
487
+ // ---- lifecycle ---------------------------------------------------------
488
+
489
+ async run(initialSelection?: number): Promise<void> {
490
+ if (initialSelection !== undefined) this.selectedPr = initialSelection;
491
+ // From here the TUI owns the screen, and this is the FIRST thing that
492
+ // happens: nothing external — no docker call, no network call, no
493
+ // filesystem walk — runs before this paint.
494
+ //
495
+ // The first frame is already the final layout: the column's width is a
496
+ // function of the terminal alone (`sidebarWidth`), the sidebar is wired
497
+ // before this call, and the container merely fills a column that is
498
+ // already the right size. Everything slow streams in behind
499
+ // this frame and reports into that column (src/tui/startup.ts).
500
+ this.started = true;
501
+ this.out.write(ALT_SCREEN_ON + CLEAR + MOUSE_ON);
502
+ if (this.input.isTTY) this.input.setRawMode(true);
503
+ this.input.resume();
504
+ this.input.on("data", this.onData);
505
+ this.out.on("resize", this.onResize);
506
+ if (this.hooks.helpSeen && !this.hooks.helpSeen.get()) {
507
+ this.view.help = true;
508
+ this.hooks.helpSeen.set();
509
+ }
510
+ this.repaint();
511
+ this.poller.start();
512
+ await new Promise<void>((resolve) => (this.quitResolve = resolve));
513
+ this.teardown();
514
+ }
515
+
516
+ /** Set once teardown has restored the terminal. From then on nothing may
517
+ * write to the output stream: an async load that resolves after quit
518
+ * (memory/proposals fetches take seconds over ssh) would otherwise paint
519
+ * a full TUI frame onto the user's restored NORMAL screen — a ghost board
520
+ * that looks like the TUI is still running. */
521
+ private stopped = false;
522
+
523
+ private teardown(): void {
524
+ this.stopped = true;
525
+ this.poller.stop();
526
+ this.pi.stop();
527
+ this.viewer.stop();
528
+ this.sidebar?.stop();
529
+ if (this.piTimer) clearInterval(this.piTimer);
530
+ this.piTimer = undefined;
531
+ this.input.off("data", this.onData);
532
+ this.out.off("resize", this.onResize);
533
+ if (this.input.isTTY) this.input.setRawMode(false);
534
+ this.input.pause();
535
+ // MOUSE_OFF, leave the alt screen, THEN clear. The clear lands
536
+ // on the normal screen and is what makes the exit look clean rather than
537
+ // dropping the operator back onto whatever the alt screen was hiding; it
538
+ // also wipes any half-line a crash left behind. Order matters: clearing
539
+ // before ALT_SCREEN_OFF would clear the screen we are about to discard.
540
+ this.out.write(MOUSE_OFF + ALT_SCREEN_OFF + CLEAR);
541
+ }
542
+
543
+ quit(): void {
544
+ this.quitResolve?.();
545
+ }
546
+
547
+ /** The terminal has been restored and nothing may write to it again. Public
548
+ * so the orchestrator's real-terminal escape hatch can check it: re-entering
549
+ * the alt screen after teardown leaves the operator staring at a blank
550
+ * alternate screen with no TUI behind it — the trap this file exists to
551
+ * avoid, arrived at from the other end. */
552
+ isStopped(): boolean {
553
+ return this.stopped;
554
+ }
555
+
556
+ /** Ctrl-C taps, for the two-tap exit (distinct from the three-tap kill). */
557
+ private ctrlCTaps: number[] = [];
558
+ private static readonly CTRL_C_EXIT_WINDOW_MS = 2_000;
559
+
560
+ /**
561
+ * Every route to "I want out", in one place.
562
+ *
563
+ * `q` asks first — quitting is cheap to redo but the confirmation is what
564
+ * stops a stray keystroke from dropping a board you were reading. Ctrl-C
565
+ * confirms by REPETITION instead: the second tap inside the window is the
566
+ * answer, so the reflex still works without a prompt in the way.
567
+ */
568
+ requestQuit(via: "key" | "ctrl-c"): void {
569
+ if (via === "ctrl-c") {
570
+ const t = this.now().getTime();
571
+ this.ctrlCTaps = [...this.ctrlCTaps.filter((x) => t - x <= App.CTRL_C_EXIT_WINDOW_MS), t];
572
+ if (this.ctrlCTaps.length >= 2) {
573
+ this.quit();
574
+ return;
575
+ }
576
+ this.notice("press Ctrl-C again to exit");
577
+ this.repaint();
578
+ return;
579
+ }
580
+ this.view.confirmQuit = true;
581
+ this.repaint();
582
+ }
583
+
584
+ /** A key while the quit prompt is up. Returns true when it consumed it. */
585
+ private handleConfirmQuit(buf: Buffer): boolean {
586
+ if (!this.view.confirmQuit) return false;
587
+ const raw = buf.toString("utf8");
588
+ // Only an explicit yes leaves. Anything else — including a second `q`,
589
+ // which is what a repeated keystroke looks like — cancels, because the
590
+ // prompt exists to catch exactly that.
591
+ if (raw === "y" || raw === "Y" || raw === "\r" || raw === "\n") {
592
+ this.view.confirmQuit = false;
593
+ this.quit();
594
+ return true;
595
+ }
596
+ this.view.confirmQuit = false;
597
+ this.repaint();
598
+ return true;
599
+ }
600
+
601
+ /** Injectable so tests never kill the runner. */
602
+ exiter: (code: number) => void = (code) => process.exit(code);
603
+
604
+ /** Triple-Ctrl-C hard kill: restore the terminal no matter what, then die.
605
+ * A hung ssh/pty must never trap the user in the alt screen — closing the
606
+ * terminal window cannot stay the only way out. */
607
+ die(): void {
608
+ try {
609
+ this.suspended = false; // teardown must run even while the terminal is lent out
610
+ this.teardown();
611
+ } catch {
612
+ /* a half-restored terminal still beats a trapped one */
613
+ }
614
+ this.exiter(130);
615
+ }
616
+
617
+ /** Lend the terminal out: stop painting, swallow board keys, drop our mouse
618
+ * reporting. The ONE remaining borrower is `s` (index.ts `onRealTerminal`):
619
+ * the GCP key-passphrase prompt has to run on the real terminal, outside the
620
+ * alt screen. It is not a TUI surface and paints no layout of its own; the
621
+ * board comes back exactly as it was the moment ssh returns. */
622
+ suspend(): void {
623
+ this.suspended = true;
624
+ this.input.off("data", this.onData);
625
+ this.out.write(MOUSE_OFF);
626
+ }
627
+
628
+ resume(): void {
629
+ if (this.stopped) return; // a borrower returning after quit must not repaint
630
+ this.suspended = false;
631
+ this.input.on("data", this.onData);
632
+ this.out.write(CLEAR + MOUSE_ON);
633
+ void this.refreshDetail();
634
+ void this.refreshProgress();
635
+ void this.refreshPane(this.panePr()); // the pane caught up while attached
636
+ this.repaint();
637
+ }
638
+
639
+ /** True while something other than the board owns the terminal or holds
640
+ * unsaved user input. Used to REFUSE a remote-driven action (index.ts), so
641
+ * it never fights the operator for the keyboard. The hot reload deliberately
642
+ * does NOT consult it — every build reloads (see reload.ts). */
643
+ isBusy(): boolean {
644
+ return (
645
+ this.suspended ||
646
+ this.view.inputBox !== undefined ||
647
+ this.view.piFocus === true ||
648
+ this.sidebar?.focused === true
649
+ );
650
+ }
651
+
652
+ /** The PR the board is pointing at (survives a hot-reload). */
653
+ currentPr(): number | undefined {
654
+ return this.selectedPr;
655
+ }
656
+
657
+ // ---- data --------------------------------------------------------------
658
+
659
+ private onPoll(): void {
660
+ // A tick already in flight when the operator quits resolves AFTER
661
+ // teardown, and this method kicks off four more remote reads. They would
662
+ // spawn ssh against a TUI that no longer exists — the same family as the
663
+ // `stopped` guard on `repaint` (an async load resolving after quit must
664
+ // not touch the restored terminal), one level earlier: it must not start
665
+ // new work either.
666
+ if (this.stopped) return;
667
+ const snap = this.poller.state.snapshot;
668
+ if (snap) {
669
+ // Invalidate cached details the snapshot proves stale (state/head moved).
670
+ for (const pr of snap.prs) {
671
+ const c = this.detailCache.get(pr.number);
672
+ if (c && (c.payload.head_sha !== pr.head_sha || c.payload.state !== pr.state))
673
+ this.invalidateDetail(pr.number);
674
+ }
675
+ this.view.snapshot = snap;
676
+ this.view.dryRun = snap.dry_run;
677
+ const sorted = visiblePrs(this.view);
678
+ this.view.selected = reselect(sorted, this.selectedPr, this.view.selected);
679
+ this.selectedPr = sorted[this.view.selected]?.number;
680
+ }
681
+ this.view.health = this.poller.health();
682
+ void this.refreshEvents();
683
+ if (this.detailWanted()) void this.refreshDetail();
684
+ // Live progress is a cheap host-volume read, separate from `why` because
685
+ // refreshing the enriched detail would call GitHub on every poll.
686
+ if (this.tab === "board" && !this.suspended) void this.refreshProgress();
687
+ // A PR whose runtime flipped the pane's surface under a focused keyboard
688
+ // (a worker round ended, a debug round began) hands it back: the same
689
+ // keys would now mean something else.
690
+ if (
691
+ this.view.piFocus &&
692
+ this.piFocusPr !== undefined &&
693
+ this.viewerOn(this.piFocusPr) !== this.piFocusViewer
694
+ )
695
+ this.blurPiPane(
696
+ `#${this.piFocusPr}'s pane changed what it shows — keyboard back on the board`,
697
+ );
698
+ // The pane only costs an ssh while the board is the visible tab.
699
+ this.syncPiPane();
700
+ if (this.tab === "board" && !this.suspended) void this.refreshPane(this.panePr());
701
+ try {
702
+ this.hooks.onPoll?.(this);
703
+ } catch {
704
+ /* a throwing wrapper must never kill the poll loop */
705
+ }
706
+ this.repaint();
707
+ }
708
+
709
+ private async refreshEvents(): Promise<void> {
710
+ const seq = ++this.eventsSeq;
711
+ const res = await this.ssh(this.cfg.ssh_host, eventsArgv(this.cfg, 50));
712
+ if (this.stopped || !res.ok || seq !== this.eventsSeq) return; // a newer read superseded this one
713
+ try {
714
+ const parsed = JSON.parse(res.stdout) as {
715
+ events: Array<{ ts: string; kind: string; pr: number | null; payload: string | null }>;
716
+ };
717
+ this.rawEvents = parsed.events;
718
+ const lines = parsed.events.map(formatEvent).reverse(); // newest first
719
+ this.view.events = this.mergeNotices(lines);
720
+ try {
721
+ this.hooks.onPoll?.(this); // feed the companion the fresh tail too
722
+ } catch {
723
+ /* a throwing wrapper must never kill the poll loop */
724
+ }
725
+ this.repaint();
726
+ } catch {
727
+ /* keep the previous sidebar on parse failure */
728
+ }
729
+ }
730
+
731
+ /** Override point to splice extra rows (e.g. quick-message pending/delivered)
732
+ * into the events strip; the base passes lines through. */
733
+ protected mergeNotices(lines: EventLine[]): EventLine[] {
734
+ return lines;
735
+ }
736
+
737
+ /** A poll refreshes the detail when the selection moved, or when the
738
+ * selected PR's cache entry is gone (invalidated) or past its TTL and no
739
+ * read for it is already in flight — a PR that stays selected must follow
740
+ * its new rounds without the operator moving away or pressing `r`. */
741
+ private detailWanted(): boolean {
742
+ const pr = this.selectedPr;
743
+ if (pr === undefined) return false;
744
+ // A read already in flight for it answers the question — stacking another
745
+ // each tick would bump detailSeq and starve a read slower than the poll.
746
+ if (this.detailPending.has(pr)) return false;
747
+ const failed = this.detailFailedAt.get(pr);
748
+ if (failed !== undefined && Date.now() - failed < App.DETAIL_RETRY_MS) return false;
749
+ if (this.view.detail?.pr !== pr) return true;
750
+ const c = this.detailCache.get(pr);
751
+ return !c || Date.now() - c.fetched_at >= App.DETAIL_TTL_MS;
752
+ }
753
+
754
+ /** Read the selected PR's detail. Served from cache at once; the fetch is
755
+ * skipped while the cache is fresh, unless `force` (the operator's `r`). */
756
+ async refreshDetail(opts: { force?: boolean } = {}): Promise<void> {
757
+ const pr = this.selectedPr;
758
+ if (pr === undefined) return;
759
+ const cached = this.detailCache.get(pr);
760
+ const now = Date.now();
761
+ if (cached) {
762
+ this.view.detail = cached.payload;
763
+ this.repaint();
764
+ if (!opts.force && now - cached.fetched_at < App.DETAIL_TTL_MS) return;
765
+ }
766
+ const seq = ++this.detailSeq;
767
+ this.detailPending.set(pr, seq);
768
+ const res = await this.ssh(this.cfg.ssh_host, whyArgv(this.cfg, pr));
769
+ // Superseded (a newer read for this PR started) or invalidated meanwhile:
770
+ // this payload may be older than what the cache should hold.
771
+ const newest = this.detailPending.get(pr) === seq;
772
+ if (newest) this.detailPending.delete(pr);
773
+ if (!res.ok) {
774
+ if (newest) this.detailFailedAt.set(pr, Date.now());
775
+ return;
776
+ }
777
+ try {
778
+ const payload = JSON.parse(res.stdout) as WhyPayload;
779
+ if (newest) this.detailFailedAt.delete(pr);
780
+ if (newest && payload.pr === pr)
781
+ this.detailCache.set(payload.pr, { payload, fetched_at: Date.now() });
782
+ if (seq !== this.detailSeq || payload.pr !== this.selectedPr) return;
783
+ this.view.detail = payload;
784
+ this.repaint();
785
+ } catch {
786
+ if (newest) this.detailFailedAt.set(pr, Date.now());
787
+ /* stale/garbled detail keeps the loading line */
788
+ }
789
+ }
790
+
791
+ private async refreshProgress(): Promise<void> {
792
+ const pr = this.selectedPr;
793
+ if (pr === undefined) return;
794
+ const seq = ++this.progressSeq;
795
+ const res = await this.ssh(this.cfg.ssh_host, progressArgv(this.cfg, pr), { timeoutMs: 3_000 });
796
+ if (seq !== this.progressSeq || pr !== this.selectedPr) return;
797
+ if (!res.ok) {
798
+ // Keep a same-PR snapshot: its age becomes the visible SILENT signal.
799
+ // A different PR must never inherit it while its own read is pending.
800
+ if (this.view.progress?.pr !== pr) this.view.progress = undefined;
801
+ this.repaint();
802
+ return;
803
+ }
804
+ const parsed = parseRoundProgress(res.stdout);
805
+ this.view.progress = parsed?.pr === pr ? parsed : undefined;
806
+ this.repaint();
807
+ }
808
+
809
+ /** Drop a PR's cached detail (a new verdict/round event makes it stale). */
810
+ invalidateDetail(pr: number): void {
811
+ this.detailCache.delete(pr);
812
+ this.detailPending.delete(pr);
813
+ }
814
+
815
+ // ---- input -------------------------------------------------------------
816
+
817
+ /**
818
+ * Point the board at `pr` (the companion's `select_pr` action, a row click
819
+ * and the events overlay land here). Once the board has loaded, a PR it does not list
820
+ * (closed, or outside the filter) is refused with a notice and the
821
+ * selection stays where it is — the highlighted row, the detail and the
822
+ * mirrored session never point at different PRs. Before the first snapshot
823
+ * the request is kept (a `qcr tui <pr>` deep link), and the first poll
824
+ * settles it. False when refused.
825
+ */
826
+ selectPr(pr: number): boolean {
827
+ const sorted = visiblePrs(this.view);
828
+ if (this.view.snapshot !== undefined && !sorted.some((p) => p.number === pr)) {
829
+ this.notice(`#${pr} is not on the board (closed, or outside the filter)`);
830
+ return false;
831
+ }
832
+ this.view.selected = reselect(sorted, pr, this.view.selected);
833
+ this.selectionMoved(pr);
834
+ return true;
835
+ }
836
+
837
+ /** The selection now points at `pr`: everything that shows the selected PR
838
+ * follows it — the progress panel (another PR's is never shown), the
839
+ * detail, and the mirrored session. */
840
+ private selectionMoved(pr: number | undefined): void {
841
+ if (pr !== this.selectedPr) this.msgDraft = ""; // a cancelled draft belongs to the PR it was typed for
842
+ this.selectedPr = pr;
843
+ if (this.view.progress?.pr !== pr) this.view.progress = undefined;
844
+ void this.refreshDetail();
845
+ void this.refreshProgress();
846
+ this.followSelection();
847
+ }
848
+
849
+ /** Move the events overlay's scroll, clamped to its lines. */
850
+ private scrollEvents(delta: number): void {
851
+ this.view.eventsScroll = moveSelection(this.view.eventsScroll, delta, this.view.events.length);
852
+ }
853
+
854
+ /** Narrow (or, with null, clear) the board. The selection stays on the
855
+ * same PR while it is still listed; otherwise it lands on the nearest row
856
+ * of the new list — and the row, the detail and the mirrored session move
857
+ * together, so the next key addresses the highlighted PR. */
858
+ setBoardFilter(filter: BoardFilter | null): void {
859
+ this.view.boardFilter = filter;
860
+ const sorted = visiblePrs(this.view);
861
+ this.view.selected = reselect(sorted, this.selectedPr, this.view.selected);
862
+ const pr = sorted[this.view.selected]?.number;
863
+ if (pr === this.selectedPr) return;
864
+ this.selectionMoved(pr);
865
+ }
866
+
867
+ selectedSession(): string | undefined {
868
+ const pr = this.selectedPrNumber();
869
+ return pr === undefined ? undefined : `pr-${pr}`;
870
+ }
871
+
872
+ notice(text: string): void {
873
+ this.view.noticeLine = text;
874
+ // Before the TUI owns the screen, a notice is startup progress and prints
875
+ // as a plain line — the companion can take minutes to build, and silence
876
+ // there looks like a hang, and painting a frame this early would flash.
877
+ // The alt screen hides these the
878
+ // moment run() begins, so they never linger.
879
+ if (!this.started) {
880
+ this.out.write(`${text}\n`);
881
+ return;
882
+ }
883
+ this.repaint();
884
+ }
885
+
886
+ /** The session the open input box targets; draft survives Esc. */
887
+ private msgSession: string | undefined;
888
+ private msgDraft = "";
889
+ /** When set, the input box belongs to a tab prompt (memory edits,
890
+ * proposal-reject reasons) — submit goes here, not to sendMessage. */
891
+ private inputSubmit: ((text: string) => void) | undefined;
892
+
893
+ openMessageBox(session: string): void {
894
+ this.msgSession = session;
895
+ this.inputSubmit = undefined;
896
+ const pr = /^pr-(\d+)$/.exec(session)?.[1];
897
+ this.view.inputBox = {
898
+ target: pr ? `#${pr} (a message to its session)` : session,
899
+ text: this.msgDraft,
900
+ };
901
+ this.repaint();
902
+ }
903
+
904
+ /** Generic one-line input over the same box UI. Esc cancels (no draft). */
905
+ promptInput(target: string, initial: string, onSubmit: (text: string) => void): void {
906
+ this.inputSubmit = onSubmit;
907
+ this.view.inputBox = { target, text: initial };
908
+ this.repaint();
909
+ }
910
+
911
+ private handleMessageBoxKey(buf: Buffer): void {
912
+ const box = this.view.inputBox!;
913
+ const key = decodeInputKey(buf);
914
+ switch (key.kind) {
915
+ case "char":
916
+ box.text += key.ch;
917
+ break;
918
+ case "backspace":
919
+ box.text = box.text.slice(0, -1);
920
+ break;
921
+ case "alt-enter":
922
+ box.text += "\n";
923
+ break;
924
+ case "escape":
925
+ if (this.inputSubmit)
926
+ this.inputSubmit = undefined; // tab prompts keep no draft
927
+ else this.msgDraft = box.text; // preserved until board navigation
928
+ this.view.inputBox = undefined;
929
+ break;
930
+ case "enter": {
931
+ const text = box.text.trim();
932
+ const submit = this.inputSubmit;
933
+ const session = this.msgSession;
934
+ this.view.inputBox = undefined;
935
+ this.inputSubmit = undefined;
936
+ if (submit) {
937
+ if (text.length > 0) submit(text);
938
+ break;
939
+ }
940
+ this.msgDraft = "";
941
+ if (text.length > 0 && session) {
942
+ if (this.hooks.sendMessage) void this.hooks.sendMessage(session, text, this);
943
+ else this.notice(`✖ msg ${session} undelivered (sender not wired)`);
944
+ }
945
+ break;
946
+ }
947
+ case "quit":
948
+ this.requestQuit(buf.toString("utf8") === "\x03" ? "ctrl-c" : "key");
949
+ return;
950
+ default:
951
+ return;
952
+ }
953
+ this.repaint();
954
+ }
955
+
956
+ private openGithub(pr?: number): void {
957
+ const n = pr ?? this.selectedPrNumber();
958
+ if (n === undefined || !this.view.repo) return;
959
+ this.hooks.openUrl?.(prUrl(this.view.repo, n));
960
+ this.notice(`opened #${n} on GitHub`);
961
+ }
962
+
963
+ /** The selected row's PR number. Public because `selectedSession()` is the
964
+ * session-name view of the same thing and callers want either. */
965
+ selectedPrNumber(): number | undefined {
966
+ const sorted = visiblePrs(this.view);
967
+ return sorted[this.view.selected]?.number;
968
+ }
969
+
970
+ /** Synthesize a keypress through the normal input path — the command bar's
971
+ * buttons and any tab code that wants "exactly what the key would do". */
972
+ pressKey(key: string): void {
973
+ this.handleInput(Buffer.from(key, "utf8"));
974
+ }
975
+
976
+ /** The installed surface under the current tab, if any (never the board). */
977
+ private tabHooks(): TabHooks | undefined {
978
+ return this.tab === "board" ? undefined : this.hooks.tabs[this.tab];
979
+ }
980
+
981
+ /**
982
+ * What the current frame answers to: the bar the tab paints plus
983
+ * its inline keys. ONE computation feeds the bar's hit-test, the companion
984
+ * routing (`registeredKeys`) and the paint, so the three cannot disagree.
985
+ * An unwired tab answers to nothing but the globals.
986
+ */
987
+ liveKeys(): LiveKeys {
988
+ if (this.tab === "board") return boardKeys(this.view);
989
+ return this.tabHooks()?.keys(this) ?? NO_LIVE_KEYS;
990
+ }
991
+
992
+ /** Every printable the current frame claims — the companion column gets
993
+ * the rest. Public for the fences: a registered key must answer. */
994
+ registeredKeys(): Set<string> {
995
+ return registeredKeys(this.liveKeys());
996
+ }
997
+
998
+ private handleMouse(ev: MouseEvent): void {
999
+ // The quit prompt is modal for the mouse as it is for the keyboard: a
1000
+ // click is its "any other key" (cancel), and nothing under the prompt —
1001
+ // a PR row, a GitHub link, the pi pane, the companion column, a tab's
1002
+ // zones — ever sees it. Wheel, drag and release are swallowed.
1003
+ if (this.view.confirmQuit) {
1004
+ if (ev.kind === "click") {
1005
+ this.view.confirmQuit = false;
1006
+ this.repaint();
1007
+ }
1008
+ // A release swallowed here still ends any gesture begun before the
1009
+ // prompt opened — never leave a seam resize armed behind it.
1010
+ if (ev.kind === "release") {
1011
+ this.sidebarResizeActive = false;
1012
+ this.selAnchor = undefined;
1013
+ }
1014
+ return;
1015
+ }
1016
+ const cols = this.out.columns ?? 80;
1017
+ // The sidebar's left seam is the ONE drag surface that is chrome rather
1018
+ // than selectable content. Everywhere else drag and release belong to the
1019
+ // rectangular copy.
1020
+ if (this.sidebar && ev.kind === "click") {
1021
+ const width = this.sidebarWidth(cols);
1022
+ if (sidebarResizeHit(ev.x, cols, width)) {
1023
+ this.sidebarResizeActive = true;
1024
+ this.selAnchor = undefined;
1025
+ this.selection = undefined;
1026
+ this.repaint();
1027
+ return;
1028
+ }
1029
+ }
1030
+ if (this.sidebarResizeActive && ev.kind === "drag") {
1031
+ this.sidebar?.resizeFromPointer(cols, ev.x);
1032
+ this.repaint();
1033
+ return;
1034
+ }
1035
+ if (this.sidebarResizeActive && ev.kind === "release") {
1036
+ this.sidebarResizeActive = false;
1037
+ this.notice(`sidebar ${this.sidebarWidth(cols)} cols`);
1038
+ return;
1039
+ }
1040
+
1041
+ // Selection owns every other drag and release. A drag is never
1042
+ // a click, so nothing below sees it and no row is selected or button fired.
1043
+ if (ev.kind === "drag") {
1044
+ const anchor = this.selAnchor;
1045
+ if (!anchor) return;
1046
+ // The first motion freezes the screen and fixes the pane the drag
1047
+ // belongs to; every later motion only moves the far corner.
1048
+ if (!this.selFrame) {
1049
+ this.selFrame = this.baseFrame;
1050
+ this.selPane = this.paneRectAt(anchor.x, anchor.y);
1051
+ }
1052
+ const p = this.selPane ?? { x1: 1, y1: 1, x2: cols, y2: this.out.rows ?? 24 };
1053
+ const cx = (x: number): number => Math.max(p.x1, Math.min(p.x2, x));
1054
+ const cy = (y: number): number => Math.max(p.y1, Math.min(p.y2, y));
1055
+ this.selection = {
1056
+ x1: cx(Math.min(anchor.x, ev.x)),
1057
+ y1: cy(Math.min(anchor.y, ev.y)),
1058
+ x2: cx(Math.max(anchor.x, ev.x)),
1059
+ y2: cy(Math.max(anchor.y, ev.y)),
1060
+ };
1061
+ this.repaint();
1062
+ return;
1063
+ }
1064
+ if (ev.kind === "release") {
1065
+ // A press with no drag is a plain click and has already been handled;
1066
+ // there is nothing to copy and the anchor just goes away.
1067
+ if (this.selection) this.copySelection();
1068
+ else this.selAnchor = undefined;
1069
+ this.selFrame = undefined;
1070
+ this.selPane = undefined;
1071
+ return;
1072
+ }
1073
+ // A new press starts a possible selection and clears the last highlight.
1074
+ if (ev.kind === "click") {
1075
+ this.selAnchor = { x: ev.x, y: ev.y };
1076
+ if (this.selection) {
1077
+ this.selection = undefined;
1078
+ this.repaint();
1079
+ }
1080
+ }
1081
+ // The command bar answers first, on every tab: a click on one of its
1082
+ // buttons IS that keypress, wherever it lands. EXCEPT while an input box
1083
+ // is open — a synthesized key would be typed into the box, and [⏎]
1084
+ // would SUBMIT the draft (a live memory edit or quick-message).
1085
+ if (ev.kind === "click" && !this.view.inputBox) {
1086
+ const rows = this.out.rows ?? 24;
1087
+ // Hit-test at the width the bar was PAINTED at — the tab's inner width
1088
+ // beside the companion column, not the terminal's. The bar wraps, so its
1089
+ // row count depends on the width: tested at the full width, a click on
1090
+ // the painted [r refresh] decoded as a nav slot one row up, and the
1091
+ // phantom slots extended into the column (terminal x is inner-frame x
1092
+ // because the tab is on the left).
1093
+ const k = barHitKey(this.tab, ev.x, ev.y, this.innerWidth(cols), rows, this.liveKeys().bar);
1094
+ if (k !== undefined) {
1095
+ // A focused pi pane would swallow the synthesized key as text.
1096
+ if (this.view.piFocus) {
1097
+ this.blurPiPane();
1098
+ this.repaint();
1099
+ }
1100
+ // So would a focused sidebar — and [⏎] would SUBMIT the draft. Blur
1101
+ // first (same rule as the pane): a bar click IS that keypress.
1102
+ if (this.sidebar?.focused) {
1103
+ this.sidebar.blur();
1104
+ this.repaint();
1105
+ }
1106
+ this.pressKey(k);
1107
+ return;
1108
+ }
1109
+ // The companion column is fixed chrome on every tab: clicking anywhere
1110
+ // in it takes the keyboard (its mirror IS the companion — there is no
1111
+ // full-screen view to open, and no collapsed state to toggle).
1112
+ if (this.sidebar) {
1113
+ const width = this.sidebarWidth(cols);
1114
+ const hit = width === 0 ? undefined : sidebarHit(ev.x, ev.y, cols, rows, width);
1115
+ if (hit !== undefined) {
1116
+ if (this.sidebar.available()) {
1117
+ this.focusCompanion();
1118
+ }
1119
+ return;
1120
+ }
1121
+ // Zellij-style spatial focus: clicking a different pane moves focus
1122
+ // there. Ctrl-q remains the keyboard-only escape hatch, not a tax on
1123
+ // mouse users who already pointed at the main TUI.
1124
+ if (this.sidebar.focused) {
1125
+ this.sidebar.blur();
1126
+ this.repaint();
1127
+ }
1128
+ }
1129
+ }
1130
+ // The wheel belongs to the companion column before any tab sees it. The
1131
+ // column is chrome on EVERY tab, so a wheel notch that the tab dispatch
1132
+ // below answers is a notch stolen from the surface the operator is
1133
+ // actually reading: scrolling the conversation while the column has focus
1134
+ // must never walk the board's selection (and drag the pi mirror away from
1135
+ // the PR being read).
1136
+ //
1137
+ // Two ways it belongs here, and they compose: the pointer is over the
1138
+ // column (focused or not — the same rule the pi pane already has), or the
1139
+ // column holds the keyboard, wherever the pointer happens to rest. Focus
1140
+ // means "this surface is the one I am working in"; the wheel is part of
1141
+ // that, not an exception to it.
1142
+ if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
1143
+ if (this.sidebar && this.wheelBelongsToSidebar(ev.x, cols)) {
1144
+ this.sidebar.scroll(ev.kind === "wheel-up" ? 1 : -1);
1145
+ this.repaint();
1146
+ return;
1147
+ }
1148
+ }
1149
+ // The KEYS overlay covers the tab, the board included (see repaint): what
1150
+ // it hides is not on screen, so a click must not reach it (a hidden [url]
1151
+ // or row would fire unseen) and the wheel must not move a hidden
1152
+ // selection. A click dismisses help, as any key does; the wheel is
1153
+ // swallowed.
1154
+ if (this.view.help) {
1155
+ if (ev.kind === "click") {
1156
+ this.view.help = false;
1157
+ this.view.helpScroll = 0;
1158
+ this.repaint();
1159
+ }
1160
+ return;
1161
+ }
1162
+ // The tab's input overlay owns its painted cells. Its renderer supplies
1163
+ // the bounds, so resize/column layout cannot expose hidden tab actions.
1164
+ if (this.view.inputBox) {
1165
+ const meta = this.lastFrame?.meta;
1166
+ const top = meta?.["inputBoxTop"];
1167
+ const bottom = meta?.["inputBoxBottom"];
1168
+ const width = meta?.["inputBoxCols"];
1169
+ if (
1170
+ top !== undefined &&
1171
+ bottom !== undefined &&
1172
+ width !== undefined &&
1173
+ ev.x >= 1 &&
1174
+ ev.x <= width &&
1175
+ ev.y >= top + 1 &&
1176
+ ev.y <= bottom + 1
1177
+ )
1178
+ return;
1179
+ }
1180
+ const tab = this.tabHooks();
1181
+ if (this.tab !== "board") {
1182
+ // An installed surface: clicks hit its own zones (rows select, buttons
1183
+ // act); the wheel is handed over as a key that says it was a wheel.
1184
+ if (ev.kind === "click") tab?.click?.(this, ev.x, ev.y);
1185
+ else tab?.key(this, { kind: ev.kind === "wheel-down" ? "down" : "up", wheel: true });
1186
+ return;
1187
+ }
1188
+ const zoneAt = (x: number, y: number): BoardAction | undefined =>
1189
+ this.lastFrame ? hitTest(this.lastFrame.zones as Zone<BoardAction>[], x, y) : undefined;
1190
+ if (ev.kind === "wheel-up" || ev.kind === "wheel-down") {
1191
+ const delta = ev.kind === "wheel-down" ? 1 : -1;
1192
+ // Pointer over the pi pane (focused or not) = that session's scrollback,
1193
+ // in place. Everything else keeps the wheel's board meaning.
1194
+ if (!this.view.eventsFocus && zoneAt(ev.x, ev.y)?.t === "pi-focus") {
1195
+ this.scrollPiPane(-delta);
1196
+ return;
1197
+ }
1198
+ // The events overlay owns the wheel while it is up; otherwise the wheel
1199
+ // moves the selection (and the mirror follows it).
1200
+ if (this.view.eventsFocus) {
1201
+ this.scrollEvents(delta);
1202
+ } else {
1203
+ const sorted = visiblePrs(this.view);
1204
+ this.view.selected = moveSelection(this.view.selected, delta, sorted.length);
1205
+ this.selectionMoved(sorted[this.view.selected]?.number);
1206
+ }
1207
+ this.repaint();
1208
+ return;
1209
+ }
1210
+ const action = zoneAt(ev.x, ev.y);
1211
+ // Any click outside the pane hands the keyboard back to the board — that
1212
+ // includes clicks on nothing in particular in the list/detail region.
1213
+ const paneWasFocused = this.view.piFocus;
1214
+ if (action?.t !== "pi-focus" && paneWasFocused) {
1215
+ this.blurPiPane();
1216
+ this.repaint();
1217
+ }
1218
+ if (!action) return;
1219
+ switch (action.t) {
1220
+ case "select-pr": {
1221
+ if (this.selectedPr === action.pr) {
1222
+ // Second click on the selected row = go to its pi, the same thing
1223
+ // ⏎ does. If the pane WAS focused, the click above already handed the
1224
+ // keyboard back, and that is the whole meaning of it.
1225
+ if (paneWasFocused) return;
1226
+ this.focusPiPane();
1227
+ this.repaint();
1228
+ return;
1229
+ }
1230
+ this.selectPr(action.pr);
1231
+ break;
1232
+ }
1233
+ case "open-github":
1234
+ this.openGithub(action.pr);
1235
+ return;
1236
+ case "event-jump":
1237
+ this.selectPr(action.pr);
1238
+ break;
1239
+ case "pi-focus":
1240
+ // Clicking the pane types into it in place — no full-screen takeover.
1241
+ this.focusPiPane();
1242
+ return;
1243
+ case "viewer-lens": {
1244
+ // A lens row of the progress panel, with the transcript viewer in the pane.
1245
+ const pr = this.selectedPr;
1246
+ if (!this.viewerOn(pr)) return;
1247
+ if (this.viewer.selectLens(pr, action.lens, action.nth)) {
1248
+ this.syncPiPane();
1249
+ this.repaint();
1250
+ void this.viewer.refresh(pr);
1251
+ } else this.notice(`${action.lens}: no transcript yet — its lens has not started logging`);
1252
+ return;
1253
+ }
1254
+ case "companion":
1255
+ // The top-bar alert badge: hand the keyboard to the companion column.
1256
+ this.openCompanion();
1257
+ return;
1258
+ }
1259
+ this.repaint();
1260
+ }
1261
+
1262
+ private readonly killChord = new TripleCtrlC();
1263
+ private readonly mouse = new MouseDecoder();
1264
+
1265
+ private handleInput(buf: Buffer): void {
1266
+ if (this.killChord.feed(buf)) {
1267
+ this.die();
1268
+ return;
1269
+ }
1270
+ if (this.suspended) return;
1271
+ const { events, rest } = this.mouse.feed(buf);
1272
+ for (const ev of events) this.handleMouse(ev);
1273
+ if (rest.length === 0) return;
1274
+ buf = rest;
1275
+ if (this.view.inputBox) {
1276
+ this.handleMessageBoxKey(buf);
1277
+ return;
1278
+ }
1279
+ // The quit prompt outranks every surface, focused sidebar included: it is
1280
+ // modal, and a keystroke meant for it must never reach a pi session.
1281
+ if (this.handleConfirmQuit(buf)) return;
1282
+ const key = decodeKey(buf);
1283
+
1284
+ if (this.view.help) {
1285
+ if ((key.kind === "down" || key.kind === "up") && key.arrow) {
1286
+ const max = this.lastFrame?.meta["helpMax"] ?? 0;
1287
+ const next = (this.view.helpScroll ?? 0) + (key.kind === "down" ? 1 : -1);
1288
+ this.view.helpScroll = Math.min(Math.max(0, next), max);
1289
+ } else {
1290
+ this.view.help = false;
1291
+ this.view.helpScroll = 0;
1292
+ }
1293
+ this.repaint();
1294
+ return;
1295
+ }
1296
+ // The pi pane owns the keyboard while it is focused: j/k/q/1-8/? and every
1297
+ // other board binding is just text as far as the session is concerned.
1298
+ // Ctrl-q — and only Ctrl-q — comes back to the board.
1299
+ if (this.tab === "board" && this.view.piFocus) {
1300
+ if (buf.length === 1 && buf[0] === PANE_ESCAPE) {
1301
+ this.blurPiPane();
1302
+ this.repaint();
1303
+ return;
1304
+ }
1305
+ // The transcript viewer is not a terminal: its keys are its own, and
1306
+ // nothing else goes anywhere.
1307
+ if (this.piFocusViewer && this.piFocusPr !== undefined) {
1308
+ this.handleViewerKey(buf, this.piFocusPr);
1309
+ return;
1310
+ }
1311
+ // End, while parked in scrollback, is the "back to live" key the header
1312
+ // advertises — it snaps the window to the tail instead of travelling.
1313
+ if (isEndKey(buf.toString("utf8")) && this.pi.offsetFor(this.piFocusPr) > 0) {
1314
+ this.pi.setOffset(this.piFocusPr!, 0);
1315
+ this.syncPiPane();
1316
+ this.repaint();
1317
+ void this.pi.refresh(this.piFocusPr);
1318
+ return;
1319
+ }
1320
+ this.sendKeysToPane(buf);
1321
+ return;
1322
+ }
1323
+ // A focused sidebar owns the keyboard, exactly as the pi pane does above:
1324
+ // the companion is a live terminal, so j/k/1-8/? are just text to it.
1325
+ // Ctrl-q releases — the same chord, so there is ONE way out of every
1326
+ // embedded surface; Esc belongs to pi.
1327
+ if (this.sidebar?.focused) {
1328
+ this.handleSidebarKey(buf);
1329
+ return;
1330
+ }
1331
+ const raw = buf.toString("utf8");
1332
+ // `/` always focuses the companion, on every tab — and TRAVELS.
1333
+ // It is the key that opens pi's command menu, so swallowing it as a pure
1334
+ // focus key is the worst possible choice: you press `/`, the column takes
1335
+ // the keyboard, and the menu you actually wanted never opens. Seeding it
1336
+ // makes `/` behave like every other printable — focus, then reach pi.
1337
+ if (raw === "/" && this.sidebar?.available()) {
1338
+ this.sidebar.focus(raw);
1339
+ this.repaint();
1340
+ return;
1341
+ }
1342
+ // `\` is plain text for pi like any other unclaimed printable: the column
1343
+ // is always at its full width and the layout never changes shape.
1344
+ // The installed surface gets first crack at every key — including the
1345
+ // nav digits, which the metrics tab reads as TEXT inside its `# <pr> ⏎`
1346
+ // box (`keyChar` maps a tab kind back to its digit). Outside that box no
1347
+ // tab answers to a digit, so the hook returns false and the key falls
1348
+ // through to the switch below.
1349
+ if (this.tabHooks()?.key(this, key)) return;
1350
+ // An unregistered printable is a message to the companion, not a dead
1351
+ // key: focus the sidebar seeded with it (paste seeds whole). Registered
1352
+ // means "this frame answers to it" — the set is derived from
1353
+ // the bar the tab is painting right now, never from a table.
1354
+ if (
1355
+ this.sidebar?.available() &&
1356
+ raw.length > 0 &&
1357
+ !raw.includes("\x1b") &&
1358
+ raw >= " " &&
1359
+ raw !== "\x7f" && // backspace (DEL) is an edit, never draft text
1360
+ raw !== "\b" &&
1361
+ !(raw.length === 1 && this.registeredKeys().has(raw))
1362
+ ) {
1363
+ this.sidebar.focus(raw);
1364
+ this.repaint();
1365
+ return;
1366
+ }
1367
+
1368
+ // The verbs below belong to the BOARD's command bar, so they only answer
1369
+ // on the board: off it, `a` would focus the board's pi pane behind your
1370
+ // back, and `u` would unwedge a PR nobody was looking at. A verb with no
1371
+ // meaning on the current surface must do nothing; the printable
1372
+ // spellings are unregistered off the board, so they reach the companion
1373
+ // column instead of dying here. The one exception is a live
1374
+ // alert, which owns ⏎ from wherever you are.
1375
+ if (
1376
+ this.tab !== "board" &&
1377
+ BOARD_ONLY_VERBS.has(key.kind) &&
1378
+ !(key.kind === "enter" && this.view.alertFocus)
1379
+ )
1380
+ return;
1381
+
1382
+ switch (key.kind) {
1383
+ case "quit":
1384
+ this.requestQuit(buf.toString("utf8") === "\x03" ? "ctrl-c" : "key");
1385
+ return;
1386
+ case "help":
1387
+ this.view.help = true;
1388
+ break;
1389
+ case "tab":
1390
+ this.tab = key.tab;
1391
+ break;
1392
+ case "up":
1393
+ case "down": {
1394
+ const delta = key.kind === "down" ? 1 : -1;
1395
+ if (this.view.eventsFocus) {
1396
+ this.scrollEvents(delta);
1397
+ break;
1398
+ }
1399
+ const sorted = visiblePrs(this.view);
1400
+ this.view.selected = moveSelection(this.view.selected, delta, sorted.length);
1401
+ this.msgDraft = ""; // board navigation drops a cancelled draft
1402
+ this.selectionMoved(sorted[this.view.selected]?.number);
1403
+ break;
1404
+ }
1405
+ case "escape":
1406
+ this.view.eventsFocus = false;
1407
+ this.view.alertFocus = false; // acknowledged: ⏎ goes back to the pi pane
1408
+ break;
1409
+ case "events":
1410
+ this.view.eventsFocus = !this.view.eventsFocus;
1411
+ this.view.eventsScroll = 0;
1412
+ break;
1413
+ case "refresh":
1414
+ void this.poller.tick();
1415
+ void this.refreshDetail({ force: true });
1416
+ void this.refreshPane(this.panePr());
1417
+ break;
1418
+ case "reconnect":
1419
+ if (this.hooks.reconnect) this.hooks.reconnect(this);
1420
+ else this.notice("reconnect not wired (test harness?)");
1421
+ void this.poller.tick();
1422
+ break;
1423
+ case "attach":
1424
+ case "enter": {
1425
+ // A live alert owns ⏎ until it is followed or dismissed — the point of
1426
+ // an alert is that the next thing you do is talk to the companion.
1427
+ if (key.kind === "enter" && this.view.alertFocus) {
1428
+ this.openCompanion();
1429
+ return;
1430
+ }
1431
+ // ⏎ on a PR row focuses the board's EMBEDDED pane. Nothing may take
1432
+ // the whole TUI (AGENTS.md invariant 10, the fixed layout). With no PR
1433
+ // selected there is nothing to focus, and nothing else to open.
1434
+ if (this.selectedPr !== undefined) {
1435
+ this.focusPiPane();
1436
+ this.repaint();
1437
+ }
1438
+ return;
1439
+ }
1440
+ case "github":
1441
+ this.openGithub();
1442
+ return;
1443
+ case "message": {
1444
+ const session = this.selectedSession();
1445
+ if (!session) break;
1446
+ if (this.hooks.message) this.hooks.message(session, this);
1447
+ else this.openMessageBox(session);
1448
+ break;
1449
+ }
1450
+ case "unwedge": {
1451
+ // Cancel the selected PR's running round and re-queue it. Safe
1452
+ // and recoverable by construction — the round re-dispatches through
1453
+ // the normal pump, nothing is posted and no GitHub state is touched —
1454
+ // so it acts immediately and reports through the events strip, the
1455
+ // same shape `m` uses. The harsher `restart-session` (which also kills
1456
+ // the conversation) is deliberately NOT bound to a key: it stays an
1457
+ // overseer command, where the extra deliberation is the point.
1458
+ const pr = this.selectedPrNumber();
1459
+ if (pr === undefined) break;
1460
+ if (this.hooks.unwedge) this.hooks.unwedge(pr, this);
1461
+ else this.notice("unwedge: not wired (no remote runner)");
1462
+ break;
1463
+ }
1464
+ case "older":
1465
+ case "newer":
1466
+ // `[ ]` step the viewer's streams — registered only while it is up.
1467
+ if (this.tab === "board" && this.viewerOn(this.selectedPr))
1468
+ this.stepViewer(this.selectedPr, "stream", key.kind === "older" ? -1 : 1);
1469
+ return;
1470
+ case "char":
1471
+ // `< >` step the viewer's rounds.
1472
+ if (
1473
+ (key.ch === "<" || key.ch === ">") &&
1474
+ this.tab === "board" &&
1475
+ this.viewerOn(this.selectedPr)
1476
+ ) {
1477
+ this.stepViewer(this.selectedPr, "round", key.ch === "<" ? -1 : 1);
1478
+ return;
1479
+ }
1480
+ // F clears the board filter — the indicator promises it.
1481
+ if (key.ch === "F" && this.tab === "board" && this.view.boardFilter != null) {
1482
+ this.setBoardFilter(null);
1483
+ break;
1484
+ }
1485
+ // I starts setup — the companion takes it from there.
1486
+ if (key.ch === "I" && this.tab === "board") {
1487
+ if (this.hooks.setup) this.hooks.setup(this);
1488
+ else this.notice("setup: not wired (no companion in this TUI)");
1489
+ break;
1490
+ }
1491
+ return;
1492
+ default:
1493
+ return; // unknown keys repaint nothing
1494
+ }
1495
+ this.repaint();
1496
+ }
1497
+
1498
+ /** Do what the input line asked: send a control, or say why not. */
1499
+ private applyViewerEffect(pr: number, effect: ReturnType<typeof viewerInputKey>["effect"]): void {
1500
+ if (effect.t === "notice") {
1501
+ this.view.noticeLine = effect.text;
1502
+ return;
1503
+ }
1504
+ if (effect.t === "none") return;
1505
+ const req: ControlRequest =
1506
+ effect.t === "abort"
1507
+ ? { kind: "abort-round", pr }
1508
+ : effect.kind === "message"
1509
+ ? { kind: "message", pr, text: effect.text, may_post: effect.may_post }
1510
+ : { kind: effect.kind, pr, text: effect.text };
1511
+ const send = this.hooks.liveControl;
1512
+ if (!send) {
1513
+ this.view.noticeLine = `✖ ${req.kind} #${pr} not sent (no control route wired)`;
1514
+ return;
1515
+ }
1516
+ void send(req, this).then(() => this.repaint());
1517
+ }
1518
+
1519
+ /** Step the viewer's stream (`[ ]`) or round (`< >`) and read what it now shows. */
1520
+ private stepViewer(pr: number, what: "stream" | "round", dir: 1 | -1): void {
1521
+ if (what === "stream") this.viewer.stepStream(pr, dir);
1522
+ else this.viewer.stepRound(pr, dir);
1523
+ this.syncPiPane();
1524
+ this.repaint();
1525
+ void this.viewer.refresh(pr);
1526
+ }
1527
+
1528
+ /**
1529
+ * Keys while the transcript viewer holds the keyboard. It is not a terminal
1530
+ * — there is no pi behind it — so nothing is forwarded, and what it does not
1531
+ * answer to is dropped. Ctrl-q (handled by the caller) releases it.
1532
+ */
1533
+ private handleViewerKey(buf: Buffer, pr: number): void {
1534
+ const raw = buf.toString("utf8");
1535
+ // The input line first: it takes text, ⏎, Alt-⏎, Alt-p and the abort
1536
+ // prompt. The navigation keys below are the line's only when it is empty.
1537
+ const typed = viewerInputKey(this.viewerInputOf(pr), raw, this.roundRunning(pr));
1538
+ if (typed.handled) {
1539
+ this.viewerInputs.set(pr, typed.state);
1540
+ this.applyViewerEffect(pr, typed.effect);
1541
+ this.syncPiPane();
1542
+ this.repaint();
1543
+ return;
1544
+ }
1545
+ if (raw === "[" || raw === "]") return this.stepViewer(pr, "stream", raw === "[" ? -1 : 1);
1546
+ if (raw === "<" || raw === ">") return this.stepViewer(pr, "round", raw === "<" ? -1 : 1);
1547
+ if (raw === "\x1b[5~") this.viewer.page(pr, 1);
1548
+ else if (raw === "\x1b[6~") this.viewer.page(pr, -1);
1549
+ else if (raw === "\x1b[A") this.viewer.setOffset(pr, this.viewer.offsetFor(pr) + 1);
1550
+ else if (raw === "\x1b[B") this.viewer.setOffset(pr, this.viewer.offsetFor(pr) - 1);
1551
+ else if (isEndKey(raw)) this.viewer.setOffset(pr, 0);
1552
+ else return;
1553
+ this.syncPiPane();
1554
+ this.repaint();
1555
+ void this.viewer.refresh(pr);
1556
+ }
1557
+
1558
+ /**
1559
+ * Go to the companion (the sidebar's rows, the alert badge, an alert-armed
1560
+ * ⏎). Following an alert clears its focus — it has been acted on.
1561
+ *
1562
+ * The companion is always on screen, at full width, so "open" means take
1563
+ * the keyboard. Nothing in the TUI covers the whole terminal, and the
1564
+ * column never collapses.
1565
+ */
1566
+ openCompanion(): void {
1567
+ this.view.alertFocus = false;
1568
+ if (this.sidebar?.available()) {
1569
+ this.focusCompanion();
1570
+ return;
1571
+ }
1572
+ // Down, but a failed startup can heal: hand it to the ensure hook, which
1573
+ // re-runs the startup and focuses on success (companiontab.ts).
1574
+ if (this.hooks.companionEnsure) {
1575
+ void this.hooks.companionEnsure(this);
1576
+ return;
1577
+ }
1578
+ this.notice("companion unavailable (started with --no-companion?)");
1579
+ }
1580
+
1581
+ /** Give the companion column the keyboard — taking it from the PR pane
1582
+ * first: one keyboard owner, whoever asked (a retry that heals can land
1583
+ * here after the operator focused a pane). */
1584
+ focusCompanion(): void {
1585
+ if (!this.sidebar) return;
1586
+ if (this.view.piFocus) this.blurPiPane();
1587
+ this.sidebar.focus();
1588
+ this.repaint();
1589
+ }
1590
+
1591
+ /**
1592
+ * Keys while the companion column has focus: RAW PASSTHROUGH to pi.
1593
+ *
1594
+ * The column is an embedded terminal, exactly like the board's pi pane, and
1595
+ * that is the whole point of running pi in tmux — `/` commands, the
1596
+ * completion menu, history, multi-line editing, Ctrl-C to interrupt a turn
1597
+ * are all pi's, and they only work if pi actually receives the bytes.
1598
+ *
1599
+ * DO NOT reintroduce a TUI-side draft line here: it would be a second, worse
1600
+ * input box in front of the real one, with no `/` commands and nothing
1601
+ * clickable. The rule is in
1602
+ * docs/tui.md; `tests/tui-sidebar.test.ts` pins it.
1603
+ *
1604
+ * Ctrl-q is the ONE byte that never travels — the same release chord the pi
1605
+ * pane uses, so there is one way out of every embedded surface. Everything
1606
+ * else, including Esc and Ctrl-C, belongs to pi.
1607
+ */
1608
+ private handleSidebarKey(buf: Buffer): void {
1609
+ const sidebar = this.sidebar!;
1610
+ if (buf.length === 1 && buf[0] === PANE_ESCAPE) {
1611
+ sidebar.blur();
1612
+ this.repaint();
1613
+ return;
1614
+ }
1615
+ // Scrollback is the MIRROR's, not pi's: pi cannot scroll a pane it does
1616
+ // not know is being mirrored. PgUp always enters scrollback; PgDn is the
1617
+ // mirror's only while parked (offset > 0), otherwise it reaches pi.
1618
+ const raw = buf.toString("utf8");
1619
+ if ((raw === "\x1b[5~" || raw === "\x1b[6~") && (raw === "\x1b[5~" || sidebar.offset > 0)) {
1620
+ sidebar.scroll(raw === "\x1b[5~" ? 1 : -1);
1621
+ this.repaint();
1622
+ return;
1623
+ }
1624
+ void sidebar
1625
+ .sendKeys(buf)
1626
+ .catch((err: unknown) =>
1627
+ this.notice(`✖ ${String(err instanceof Error ? err.message : err)}`),
1628
+ );
1629
+ }
1630
+
1631
+ /** The alert that last armed `alertFocus`. A folded repeat keeps the
1632
+ * consumer's `latest` object, so it updates the counters without re-arming
1633
+ * an alert the operator already acknowledged. */
1634
+ private armedAlert: AlertBadge["latest"];
1635
+
1636
+ /** Alert consumer → top bar. A NEW `alert`-tier alert also grabs ⏎ (see
1637
+ * handleInput); a folded repeat of it does not. */
1638
+ setAlertBadge(badge: AlertBadge): void {
1639
+ this.view.alerts = badge;
1640
+ if (badge.latest?.tier !== "alert") {
1641
+ this.view.alertFocus = false;
1642
+ this.armedAlert = undefined;
1643
+ }
1644
+ if (badge.latest?.tier === "alert" && badge.latest !== this.armedAlert) {
1645
+ this.armedAlert = badge.latest;
1646
+ this.view.alertFocus = true;
1647
+ }
1648
+ this.repaint();
1649
+ }
1650
+
1651
+ // ---- paint -------------------------------------------------------------
1652
+
1653
+ private lastFrame: Frame | undefined;
1654
+
1655
+ /**
1656
+ * The quit prompt, painted over the finished frame.
1657
+ *
1658
+ * Over the COMPOSED frame, not inside a renderer: it must appear on every
1659
+ * tab and over the companion column alike, and no tab should have to know
1660
+ * it exists. Three rows in the middle, so it cannot be mistaken for the
1661
+ * command bar's own `[q quit]` button.
1662
+ */
1663
+ private paintQuitPrompt(frame: Frame, cols: number, rows: number): void {
1664
+ if (!this.view.confirmQuit) return;
1665
+ const text = " Quit qcr tui? [y] yes [any other key] no ";
1666
+ const w = Math.max(2, Math.min(cols - 4, Math.max(text.length + 2, 24)));
1667
+ const x = Math.max(0, Math.floor((cols - w) / 2));
1668
+ const y = Math.max(0, Math.floor(rows / 2) - 1);
1669
+ const box = [`┌${"─".repeat(w - 2)}┐`, `│${fit(text, w - 2)}│`, `└${"─".repeat(w - 2)}┘`];
1670
+ for (let i = 0; i < box.length; i++) {
1671
+ const row = y + i;
1672
+ if (row < 0 || row >= rows) continue;
1673
+ const line = frame.lines[row] ?? "";
1674
+ // Splice by DISPLAY cells: the underlying row is full of SGR runs, and
1675
+ // slicing by code units would cut one in half and bleed colour.
1676
+ frame.set(row, spliceDisplay(line, x, box[i]!));
1677
+ }
1678
+ }
1679
+
1680
+ /**
1681
+ * Does this wheel notch belong to the companion column's scrollback?
1682
+ *
1683
+ * True when the pointer is inside the column, or when the column holds the
1684
+ * keyboard and the pointer is elsewhere.
1685
+ */
1686
+ private wheelBelongsToSidebar(x: number, cols: number): boolean {
1687
+ const sb = this.sidebar;
1688
+ if (!sb) return false;
1689
+ const width = this.sidebarWidth(cols);
1690
+ if (width === 0) return false;
1691
+ if (x > cols - width && x <= cols) return true;
1692
+ return sb.focused;
1693
+ }
1694
+
1695
+ /**
1696
+ * Width the companion column takes on THIS paint.
1697
+ *
1698
+ * A pure function of the terminal width and the operator's seam drag —
1699
+ * never of config, docker or the container's state: the column is part of
1700
+ * the layout whenever a sidebar exists at all, so the first frame is the final shape and nothing that
1701
+ * happens later can move it. `0` only for a harness that built no sidebar.
1702
+ */
1703
+ sidebarWidth(cols: number): number {
1704
+ if (!this.sidebar) return 0;
1705
+ return sidebarWidth(cols, { preferredWidth: this.sidebar.preferredWidth });
1706
+ }
1707
+
1708
+ /** The tab's own width: everything left of the companion column, the
1709
+ * shared border cell included — the width every tab renders and lays its
1710
+ * command bar out at. */
1711
+ innerWidth(cols: number): number {
1712
+ const sw = this.sidebarWidth(cols);
1713
+ return sw === 0 ? cols : cols - sw + 1;
1714
+ }
1715
+
1716
+ /** Compose the companion column onto a tab's frame. The tab was rendered at
1717
+ * the REDUCED width, so this only ever appends — no coordinate inside the
1718
+ * frame moves, and the tab renderers stay ignorant of the sidebar. */
1719
+ private withSidebar(inner: Frame, cols: number, rows: number): Frame {
1720
+ const width = this.sidebarWidth(cols);
1721
+ if (width === 0 || !this.sidebar) return inner;
1722
+ // joinSeam: the tab's right border and the column's left border are the
1723
+ // same wall, so the inner frame gives up its last cell — which is why the
1724
+ // tab is rendered one column WIDER than `cols - width` (see repaint).
1725
+ return composeColumn(inner, paintSidebar(this.sidebar.view(), width, rows), width, true);
1726
+ }
1727
+
1728
+ /** Set by run(): the TUI owns the screen and may paint. */
1729
+ private started = false;
1730
+
1731
+ // ---- mouse selection → clipboard -----------------------------------------
1732
+ //
1733
+ // Turning mouse reporting on takes the terminal's own selection away, so the
1734
+ // TUI owes one back. A selection is almost always made to copy it, so there
1735
+ // is no copy key and no copy mode: release IS the copy.
1736
+ //
1737
+ // RECTANGULAR, not linear. The screen is columns side by side — list next to
1738
+ // detail next to the companion — and a linear selection across a row would
1739
+ // splice text from panes the user never pointed at. A block keeps a drag
1740
+ // inside the pane it started in, which is what "select this bit of pi's
1741
+ // output" means beside the companion conversation.
1742
+ private sidebarResizeActive = false;
1743
+ private selAnchor: { x: number; y: number } | undefined;
1744
+ private selection: { x1: number; y1: number; x2: number; y2: number } | undefined;
1745
+ /** The last composed frame WITHOUT the highlight — what a drag freezes. */
1746
+ private baseFrame: Frame | undefined;
1747
+ /**
1748
+ * The screen as it was when the drag began, held until release.
1749
+ *
1750
+ * The mirrors refresh under the pointer several times a second while pi
1751
+ * streams: without the freeze the text would scroll out from under a
1752
+ * half-finished drag, and release would copy whatever had arrived in its
1753
+ * place rather than what the operator had highlighted.
1754
+ */
1755
+ private selFrame: Frame | undefined;
1756
+ /** The pane the drag started in (1-based, inclusive); the block never leaves it. */
1757
+ private selPane: { x1: number; y1: number; x2: number; y2: number } | undefined;
1758
+ /** Injectable so tests never touch the real clipboard. */
1759
+ clipboard: { write(text: string): Promise<boolean> } = new ClipboardSink();
1760
+
1761
+ /** The selected block as plain text, exactly as it appears on screen. */
1762
+ selectedText(): string {
1763
+ const sel = this.selection;
1764
+ const frame = this.selFrame ?? this.baseFrame ?? this.lastFrame;
1765
+ if (!sel || !frame) return "";
1766
+ const rows: string[] = [];
1767
+ for (let y = sel.y1; y <= sel.y2; y++) {
1768
+ const line = frame.lines[y - 1];
1769
+ if (line === undefined) continue;
1770
+ // BY DISPLAY CELL, not code unit: what is copied must be what was
1771
+ // highlighted, and a single wide glyph earlier in the row would put
1772
+ // those two out of step.
1773
+ rows.push(sliceDisplay(line, sel.x1 - 1, sel.x2));
1774
+ }
1775
+ return cleanCopiedBlock(rows);
1776
+ }
1777
+
1778
+ /**
1779
+ * The pane under a point, as the content rectangle a selection may cover
1780
+ * (1-based, inclusive, borders excluded).
1781
+ *
1782
+ * A rectangular block already keeps a drag in its COLUMN band, but a drag
1783
+ * that wandered a cell too far picked up the neighbour's border and first
1784
+ * letters — the board's list beside its detail, the companion column beside
1785
+ * the tab. Each pane is known from the layout, not guessed from glyphs: the
1786
+ * companion column, and on the board the list, the detail and the pi pane
1787
+ * (`frame.meta`, written by renderBoard). Any other tab is one pane.
1788
+ */
1789
+ private paneRectAt(
1790
+ x: number,
1791
+ y: number,
1792
+ ): { x1: number; y1: number; x2: number; y2: number } | undefined {
1793
+ const frame = this.baseFrame;
1794
+ if (!frame) return undefined;
1795
+ const cols = frame.cols;
1796
+ const rows = frame.rows;
1797
+ const sw = this.sidebarWidth(cols);
1798
+ // The seam: with a column, the tab's right border IS the column's left one.
1799
+ const seam = sw === 0 ? cols : this.innerWidth(cols);
1800
+ if (sw > 0 && x > seam) return { x1: seam + 1, y1: 2, x2: cols - 1, y2: rows - 1 };
1801
+ const half = { x1: 2, y1: 1, x2: seam - 1, y2: rows };
1802
+ const m = frame.meta;
1803
+ if (this.tab !== "board" || this.view.help || m["listW"] === undefined) return half;
1804
+ const listW = m["listW"];
1805
+ const bodyRows = m["bodyRows"] ?? 0;
1806
+ const piTop = m["piTop"] ?? rows; // 0-based divider row
1807
+ const piRows = m["piRows"] ?? 0;
1808
+ // Body rows are 0-based 1..bodyRows: 1-based 2..bodyRows+1.
1809
+ if (y >= 2 && y <= bodyRows + 1) {
1810
+ return x <= listW + 1
1811
+ ? { x1: 2, y1: 2, x2: listW + 1, y2: bodyRows + 1 }
1812
+ : { x1: listW + 3, y1: 2, x2: half.x2, y2: bodyRows + 1 };
1813
+ }
1814
+ // The pi pane: its header row and mirror lines, below the divider.
1815
+ if (piRows > 0 && y >= piTop + 2 && y <= piTop + piRows) {
1816
+ return { x1: 2, y1: piTop + 2, x2: half.x2, y2: piTop + piRows };
1817
+ }
1818
+ return half;
1819
+ }
1820
+
1821
+ /**
1822
+ * The WHOLE last painted frame as plain text: exactly what is on
1823
+ * screen, ANSI stripped, trailing blank rows dropped. Same source as
1824
+ * `selectedText()` (the last painted frame), for the same reason: what the
1825
+ * companion is shown must be what the operator is looking at, not a re-render of
1826
+ * the model behind it.
1827
+ *
1828
+ * `undefined` before the first frame is painted. Never a fetch: this is a
1829
+ * getter over state already painted.
1830
+ */
1831
+ screenText(): string | undefined {
1832
+ const frame = this.lastFrame;
1833
+ if (!frame) return undefined;
1834
+ const rows = frame.lines.map((line) => stripAnsi(line).replace(/\s+$/, ""));
1835
+ while (rows.length > 0 && rows[rows.length - 1] === "") rows.pop();
1836
+ return rows.join("\n");
1837
+ }
1838
+
1839
+ /**
1840
+ * Called at the end of every repaint, so the published view tracks
1841
+ * keystrokes and not just the 3 s poll. Installed by index.ts; the callee
1842
+ * throttles. It must never repaint (that would recurse) and never throw.
1843
+ */
1844
+ afterRepaint?: () => void;
1845
+
1846
+ private copySelection(): void {
1847
+ const text = this.selectedText();
1848
+ this.selAnchor = undefined;
1849
+ if (text.length === 0) {
1850
+ this.selection = undefined;
1851
+ this.repaint();
1852
+ return;
1853
+ }
1854
+ void this.copyText(text);
1855
+ // The highlight stays until the next click, so it is visible at the moment
1856
+ // the confirmation appears — proof of WHAT was copied, not just that
1857
+ // something was.
1858
+ this.repaint();
1859
+ }
1860
+
1861
+ /**
1862
+ * Put `text` on the system clipboard and say so on the notice line — the
1863
+ * ONE route for every copy the TUI makes: a released drag, and a copy made
1864
+ * inside the companion's container (`from`, e.g. pi's `/copy`), which the
1865
+ * column hands over because nothing else could deliver it (sidebar.ts
1866
+ * `onCopy`).
1867
+ */
1868
+ async copyText(text: string, from?: string): Promise<boolean> {
1869
+ if (text.length === 0) return true;
1870
+ const lines = text.split("\n").length;
1871
+ const what = `${text.length} chars${lines > 1 ? ` · ${lines} lines` : ""}`;
1872
+ try {
1873
+ const ok = await this.clipboard.write(text);
1874
+ this.notice(
1875
+ ok
1876
+ ? `✓ copied ${what}${from ? ` from ${from}` : ""}`
1877
+ : "✖ no clipboard tool (wl-copy / xclip / klipper)",
1878
+ );
1879
+ return ok;
1880
+ } catch {
1881
+ this.notice("✖ clipboard write failed");
1882
+ return false;
1883
+ }
1884
+ }
1885
+
1886
+ /** Paint the selection highlight over the finished frame. */
1887
+ private paintSelection(frame: Frame, cols: number, rows: number): void {
1888
+ const sel = this.selection;
1889
+ if (!sel) return;
1890
+ for (let y = sel.y1; y <= sel.y2; y++) {
1891
+ if (y < 1 || y > rows) continue;
1892
+ const line = frame.lines[y - 1];
1893
+ if (line === undefined) continue;
1894
+ const x1 = Math.max(1, Math.min(sel.x1, cols));
1895
+ const x2 = Math.max(1, Math.min(sel.x2, cols));
1896
+ const span = sliceDisplay(line, x1 - 1, x2);
1897
+ if (span.length === 0) continue;
1898
+ frame.set(y - 1, spliceDisplay(line, x1 - 1, `${SELECTION}${span}${RESET}`));
1899
+ }
1900
+ }
1901
+
1902
+ /** Fire `afterRepaint` — never let a publisher's failure reach the paint
1903
+ * path. The screen is the product; publishing it is a side effect. */
1904
+ private notifyRepaint(): void {
1905
+ try {
1906
+ this.afterRepaint?.();
1907
+ } catch {
1908
+ /* a failed publish is never worth a broken frame */
1909
+ }
1910
+ }
1911
+
1912
+ repaint(): void {
1913
+ if (!this.started || this.suspended || this.stopped) return;
1914
+ const cols = this.out.columns ?? 80;
1915
+ const rows = this.out.rows ?? 24;
1916
+ // +1 when a column is shown: the tab and the column share one border, and
1917
+ // `withSidebar` drops the tab's last cell to join them (composeColumn's
1918
+ // joinSeam). Without the +1 every tab would lose a usable column.
1919
+ const sw = this.sidebarWidth(cols);
1920
+ // The companion runs in a detached tmux window, whose implicit 80×24 size
1921
+ // otherwise survives forever. Feed the exact painted rectangle back to
1922
+ // the mirror; its next refresh atomically resizes then captures the pane.
1923
+ this.sidebar?.setViewport(sw, rows);
1924
+ const inner = this.innerWidth(cols);
1925
+ const paint = (screen: Frame | undefined, fallback: string): void => {
1926
+ // An unwired tab (a harness, or a hook that failed to install) paints
1927
+ // its one-line notice INSIDE the layout, never as a bare full-width
1928
+ // string: the column is part of every frame, wired tab or not.
1929
+ // Mid-drag the screen holds still (see selFrame): repaint the frozen
1930
+ // frame with the moving highlight, and nothing new underneath it.
1931
+ const frozen = this.selFrame;
1932
+ const frame = frozen
1933
+ ? cloneFrame(frozen)
1934
+ : this.withSidebar(screen ?? placeholderFrame(inner, rows, fallback), cols, rows);
1935
+ // Zellij-style spatial focus: exactly one pane owns the accent square.
1936
+ // The sidebar and board pi are terminals; otherwise the main TUI pane
1937
+ // owns focus. Colour existing frame glyphs only, never the content.
1938
+ const mainRight = sw === 0 ? frame.cols - 1 : inner - 1;
1939
+ if (frozen) {
1940
+ // already composed and accented
1941
+ } else if (sw > 0 && this.sidebar?.focused) {
1942
+ accentFrameRect(frame, inner - 1, 0, frame.cols - 1, rows - 1, ACCENT, RESET);
1943
+ } else if (this.tab === "board" && this.view.piFocus && frame.meta["piTop"] !== undefined) {
1944
+ accentFrameRect(frame, 0, frame.meta["piTop"], mainRight, rows - 1, ACCENT, RESET);
1945
+ } else {
1946
+ accentFrameRect(frame, 0, 0, mainRight, rows - 1, ACCENT, RESET);
1947
+ }
1948
+ if (!frozen) this.baseFrame = cloneFrame(frame);
1949
+ // Selection first, prompt second: the modal must sit ON TOP of a
1950
+ // highlight, never under it.
1951
+ this.paintSelection(frame, cols, rows);
1952
+ this.paintQuitPrompt(frame, cols, rows);
1953
+ this.lastFrame = frame;
1954
+ this.out.write(frame.render());
1955
+ this.notifyRepaint();
1956
+ };
1957
+ // `?` is a global key advertised on every tab's bar, and the KEYS overlay
1958
+ // (renderBoard's help branch) is the only thing that paints it — so while
1959
+ // help is up, every tab shows it. A tab screen here would leave the flag
1960
+ // set with nothing on screen, and the next keystroke silently eaten.
1961
+ if (this.tab !== "board" && !this.view.help) {
1962
+ const screen = this.tabHooks()?.screen(this, inner, rows);
1963
+ // A prompt the tab opened types into the input box: show it there.
1964
+ if (screen && this.view.inputBox) overlayInputBox(screen, this.view.inputBox);
1965
+ paint(screen, `${this.tab} tab not wired — [1] back to board`);
1966
+ return;
1967
+ }
1968
+ this.view.now = this.now();
1969
+ // Same reason as the sidebar above: a debug round's or a takeover's pi runs
1970
+ // in a detached tmux window stuck at 80×24, so without this it wraps at 80
1971
+ // columns however wide we paint the mirror. Fed here, applied by the next capture — and
1972
+ // sized against the bar rows THIS frame's board actually paints.
1973
+ const paneSize = piPaneSize(inner, rows, boardBarHeight(this.view, inner));
1974
+ this.pi.setViewport(paneSize);
1975
+ // The viewer keeps its last pane row for the input line.
1976
+ this.viewer.setViewport(paneSize && { ...paneSize, rows: Math.max(1, paneSize.rows - 1) });
1977
+ paint(renderBoard(this.view, inner, rows), "");
1978
+ }
1979
+ }