@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/config.ts ADDED
@@ -0,0 +1,2050 @@
1
+ // Typed access to config.toml — the single source of every runtime knob.
2
+ // The review sections (their types, defaults and parsers) live in
3
+ // packages/pi-plugin/src/reviewconfig.ts, the core's half; this module adds every other
4
+ // surface's, the install homes and the live config's loader, and re-exports
5
+ // the review half so its importers need not know where a knob lives.
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { resolve, dirname, isAbsolute, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parse } from "smol-toml";
11
+ import {
12
+ locateConfig,
13
+ locateSecrets,
14
+ missingConfigMessage,
15
+ strandedCheckoutConfig,
16
+ } from "./install.js";
17
+ import { packageRootFrom } from "@quickchatai/pi-quickreview/src/assetroot.js";
18
+ import { repoCloneDirName } from "./clonedir.js";
19
+ import { MissingConfigError } from "./missingconfig.js";
20
+ import { DEFAULT_LENS_GRANT_STAGGER_MS, DEFAULT_LENS_PERMITS } from "./ipc/permits.js";
21
+ import {
22
+ configDeprecations,
23
+ foldLegacyOwnerSlackId,
24
+ MEMORY_DELIVERY_MODES,
25
+ parseEngine,
26
+ parseEscalation,
27
+ parseGithubSection,
28
+ parseGuardsSection,
29
+ parseModels,
30
+ parseOwnerSection,
31
+ parsePiSection,
32
+ parseReviewSection,
33
+ parseScan,
34
+ presetOf,
35
+ req,
36
+ requireGithubKeys,
37
+ REVIEW_PRESETS,
38
+ validateReviewSection,
39
+ type CoreConfig,
40
+ type GithubConfig,
41
+ type MemoryDelivery,
42
+ type OwnerConfig,
43
+ } from "@quickchatai/pi-quickreview/src/reviewconfig.js";
44
+
45
+ export * from "@quickchatai/pi-quickreview/src/reviewconfig.js";
46
+
47
+ /** Directory name of the base clone: `[github] clone_dir` when set, else the
48
+ * repo slug's basename ("owner/name" → "name"; a URL's trailing path segment,
49
+ * `.git` stripped), e.g. "acme/webapp" ⇒ "webapp". */
50
+ export function githubCloneDir(g: Pick<GithubConfig, "clone_dir" | "repo">): string {
51
+ // ?? "": hand-rolled configs (tests) may omit both keys.
52
+ const explicit = (g.clone_dir ?? "").trim();
53
+ if (explicit) return explicit;
54
+ return repoCloneDirName(g.repo ?? "");
55
+ }
56
+
57
+ /** Where the daemon container lives relative to the TUI. `"ssh"` is
58
+ * the VM topology (every remote op rides `ssh <ssh_host> …`); `"local"` is the
59
+ * laptop topology (the SAME commands run against local docker directly, and no
60
+ * ssh process is ever spawned). Always explicit — never inferred from a magic
61
+ * ssh_host value. */
62
+ export type TuiTarget = "ssh" | "local";
63
+
64
+ export interface TuiConfig {
65
+ /** Transport for every TUI remote op (see TuiTarget). Default "ssh". */
66
+ target: TuiTarget;
67
+ /** ssh alias of the box running the container. Ignored when target = "local". */
68
+ ssh_host: string;
69
+ container: string;
70
+ /** The container the memory forward relays into, when OpenViking does not
71
+ * live in `container`: the memory sidecar (`[openviking] endpoint =
72
+ * "remote"`, docker/memory/). Empty = `container` (the in-container
73
+ * server). See `tuiMemoryContainer`. */
74
+ memory_container: string;
75
+ remote_state_dir: string;
76
+ /** The app root inside the daemon container. "" (the default) = resolved
77
+ * from the running container's `qcr.layout` label (src/tui/layout.ts): the
78
+ * release image is /opt/qcr, the legacy container (no label)
79
+ * /opt/quickreview/sandbox. A set value wins and is never probed. */
80
+ remote_sandbox_root: string;
81
+ sudo_docker: boolean;
82
+ poll_interval_s: number;
83
+ /** DATA STALE banner when status.json's written_at ages past this — sized
84
+ * to the daemon's ~30s write cadence, not the TUI's fetch cadence. */
85
+ stale_after_s: number;
86
+ msg_timeout_s: number;
87
+ /** Per-remote-read budget for the finished-round evidence copier.
88
+ * Every artefact fetch is killed at this age, so a wedged ssh control master
89
+ * costs one sweep and nothing else — never the poll, never the paint loop. */
90
+ evidence_timeout_s: number;
91
+ /** The evidence sweep's pace — the minimum gap between the end of
92
+ * one `qcr why` exec and the start of the next (each costs the container
93
+ * ~0.5–1 s of CPU), whatever the poll cadence. Clamped to 5–600 s
94
+ * (`evidenceWhyIntervalMs`, src/tui/evidence.ts); the pacing cannot be
95
+ * switched off. */
96
+ evidence_why_interval_s: number;
97
+ }
98
+
99
+ /** The local companion container. Host paths are expanded; empty
100
+ * strings mean "derive from the sandbox root" at parse time. */
101
+ export interface CompanionConfig {
102
+ /** `qcr tui` starts the companion unless this is off or --no-companion. */
103
+ enabled: boolean;
104
+ container_name: string;
105
+ image: string;
106
+ /** Named volume for the companion's own pihome (OAuth, sessions, memory). */
107
+ pihome_volume: string;
108
+ /** tmux session name inside the container. */
109
+ session: string;
110
+ model: string;
111
+ /** Host path of the review-sandbox repo → /workspace (rw). */
112
+ repo_dir: string;
113
+ /** Host path of the issue tracker → /tracker (rw, actor `companion`). */
114
+ tracker_dir: string;
115
+ /** Host path of the tracker's protocol docs → /tracker-docs (READ-ONLY).
116
+ * Sits outside `tracker_dir`, so it needs its own mount: without it the
117
+ * companion drives wayfinder from a summary instead of the real adapter. */
118
+ tracker_docs_dir: string;
119
+ /** Host path of the REVIEWED repo → /reviewed-repo, READ-ONLY. The
120
+ * companion reads the code a review is about; it can never write to it, and
121
+ * rule 8's boundary is otherwise unchanged (no GitHub write, no gate, no
122
+ * deploy.sh, no VM credential). A missing checkout simply drops the mount —
123
+ * under-equipped, never broken. */
124
+ reviewed_repo_dir: string;
125
+ /** Host dir the TUI copies finished-round evidence into → /evidence,
126
+ * READ-ONLY. The companion reads transcripts, ledgers, prompts and
127
+ * verdicts as plain files; it gains NO ssh, no credential and no new reach —
128
+ * the TUI holds the VM access and does all the fetching. Read-only because
129
+ * this is a RECORD: the companion reads history, it never rewrites it.
130
+ * Empty ⇒ `<sandbox root>/evidence` (gitignored); the default lives outside
131
+ * the repo because the store grows without bound and is kept indefinitely. */
132
+ evidence_dir: string;
133
+ /** Host dir the TUI writes ambient snapshots into → /feed. */
134
+ feed_dir: string;
135
+ /** Host dir the companion writes alert files into → /alerts. */
136
+ alerts_dir: string;
137
+ /** Accepted so an older config still loads, and read by nothing: the
138
+ * companion never mounts `~/.ssh` (no ssh config, no agent socket). */
139
+ ssh_dir: string;
140
+ /** Docker build context for the companion image (auto-built when
141
+ * missing): the app root. The image is the release image, built from
142
+ * `docker/Dockerfile` under it, which compiles the app itself. */
143
+ build_context: string;
144
+ // --- watcher knobs ---
145
+ /** A round overdue by more than this is worth waking the operator for. */
146
+ overdue_grace_min: number;
147
+ /** status.json older than this counts as a stale-daemon alert. */
148
+ stale_alert_s: number;
149
+ /** A PR sitting in awaiting_author longer than this is nag-worthy. */
150
+ awaiting_age_h: number;
151
+ /** Minimum gap between two alerts about the same subject. */
152
+ cooldown_min: number;
153
+ }
154
+
155
+ /** [backfill] — knobs for the one-shot GitHub history importer.
156
+ * Every default is derived from [github] at parse time; the section exists so
157
+ * a re-run against a renamed label/identity needs no code change. */
158
+ export interface BackfillConfig {
159
+ /** Review author to import (GraphQL `reviews(author:)`, with the [bot] suffix). */
160
+ bot_login: string;
161
+ /** The same identity as a timeline *actor* — GraphQL drops the [bot] suffix. */
162
+ bot_actor: string;
163
+ /** Label whose presence marks a PR as part of the bot-review era. */
164
+ reviewed_label: string;
165
+ /** Label a human adds to request a review — a trigger candidate. */
166
+ trigger_label: string;
167
+ /** Label the bot adds when a round starts — the fallback trigger candidate. */
168
+ in_progress_label: string;
169
+ /** Team slug whose review request is a trigger (bare slug, no org prefix). */
170
+ review_team_slug: string;
171
+ /** Per-PR detail queries in flight at once (kept small on purpose). */
172
+ concurrency: number;
173
+ /** Search pages per enumeration query (100 hits each; search caps at 1,000). */
174
+ max_search_pages: number;
175
+ }
176
+
177
+ // ---- [slack] — the Slack concierge ----
178
+ // Tokens are never in config. `bot_token_file` / `app_token_file` name flat
179
+ // files under [github].secrets_dir, read the way every other secret is
180
+ // (PiSessionManager.secret / TokenMinter): the config carries the file NAME.
181
+
182
+ /** One announcement cause: may it speak at all, and how often. */
183
+ export interface SlackCausePolicy {
184
+ enabled: boolean;
185
+ /** 0 = no cooldown (every occurrence fires). */
186
+ cooldown_min: number;
187
+ }
188
+
189
+ export interface SlackPolicyConfig {
190
+ /** Channel-wide model turns per policy day. */
191
+ daily_turns: number;
192
+ /** Per-person model turns per policy day. */
193
+ per_user_daily_turns: number;
194
+ /** Daemon posts into any ONE thread per policy day. */
195
+ thread_replies_per_day: number;
196
+ /** Accepted so an older config still loads, and read by nothing: the
197
+ * policy's turn caps are the daily and per-person ones above. */
198
+ soft_exchange_limit: number;
199
+ /** Rolling-hour ceiling on announcements (they bypass turn caps, not this). */
200
+ announcements_per_hour: number;
201
+ /** Cooldown for a cause with none of its own: one outside the built-in
202
+ * table that is unlisted, or listed in [slack.policy.causes] without a
203
+ * `cooldown_min`. A built-in cause keeps its own unless that table sets one. */
204
+ default_cooldown_min: number;
205
+ /** The day boundary for every cap here — DST-correct via src/eod.ts. */
206
+ timezone: string;
207
+ pickup_grace_min: number;
208
+ downtime_notice_min: number;
209
+ heartbeat_interval_min: number;
210
+ /** Accepted so an older config still loads, and read by nothing: no
211
+ * code raises the `reconcile.storm` announcement this would threshold. */
212
+ storm_corrections: number;
213
+ /** Accepted so an older config still loads, and read by nothing: no
214
+ * code raises the `pr.awaiting_author_stale` announcement this would time. */
215
+ awaiting_author_stale_h: number;
216
+ /** Accepted so an older config still loads, and read by nothing: a
217
+ * verdict always stays in-thread, never `reply_broadcast`, whatever this
218
+ * says — the announcer has no broadcast parameter. */
219
+ broadcast_verdicts: "never" | "always";
220
+ causes: Record<string, SlackCausePolicy>;
221
+ }
222
+
223
+ export interface SlackConfig {
224
+ enabled: boolean;
225
+ /** The ONE channel id the concierge lives in; empty ⇒ cannot enable. */
226
+ channel: string;
227
+ channel_kind: "public" | "private";
228
+ /** File name under [github].secrets_dir holding the xoxb- bot token. */
229
+ bot_token_file: string;
230
+ /** File name under [github].secrets_dir holding the xapp- socket token. */
231
+ app_token_file: string;
232
+ post_min_interval_ms: number;
233
+ reconnect_backoff_ms: number[];
234
+ server_ping_timeout_ms: number;
235
+ /** Accepted so an older config still loads, and read by nothing: Slack
236
+ * inbound turns are retired, so no backlog message is ever replayed or aged. */
237
+ catchup_max_age_s: number;
238
+ max_reply_chars: number;
239
+ name_cache_ttl_s: number;
240
+ /** Slack member id → GitHub login. The ONLY source of own-PR rights. */
241
+ users: Record<string, string>;
242
+ policy: SlackPolicyConfig;
243
+ }
244
+
245
+ /** Per-cause defaults. Absent causes fall back to `default_cooldown_min`. */
246
+ export const SLACK_CAUSE_DEFAULTS: Record<string, SlackCausePolicy> = {
247
+ "round.overdue": { enabled: true, cooldown_min: 60 },
248
+ "round.malformed_verdict": { enabled: true, cooldown_min: 60 },
249
+ "review.failed_loud": { enabled: true, cooldown_min: 60 },
250
+ "pr.not_picked_up": { enabled: true, cooldown_min: 120 },
251
+ "daemon.up": { enabled: true, cooldown_min: 0 },
252
+ "daemon.stopping": { enabled: true, cooldown_min: 0 },
253
+ "slack.cap_reached": { enabled: true, cooldown_min: 0 },
254
+ "reconcile.storm": { enabled: false, cooldown_min: 180 },
255
+ "digest.eod": { enabled: false, cooldown_min: 0 },
256
+ "pr.awaiting_author_stale": { enabled: false, cooldown_min: 1440 },
257
+ };
258
+
259
+ export const SLACK_POLICY_DEFAULTS: SlackPolicyConfig = {
260
+ daily_turns: 80,
261
+ per_user_daily_turns: 20,
262
+ thread_replies_per_day: 6,
263
+ soft_exchange_limit: 3,
264
+ announcements_per_hour: 20,
265
+ default_cooldown_min: 60,
266
+ // Effective value comes from [slack.policy] timezone, else [ops] timezone
267
+ // (parseSlack) — this static default (= OPS_DEFAULTS.timezone, declared
268
+ // below) only reaches direct constructions that bypass parseConfig.
269
+ timezone: "UTC",
270
+ pickup_grace_min: 20,
271
+ downtime_notice_min: 5,
272
+ heartbeat_interval_min: 5,
273
+ storm_corrections: 5,
274
+ awaiting_author_stale_h: 48,
275
+ broadcast_verdicts: "never",
276
+ causes: SLACK_CAUSE_DEFAULTS,
277
+ };
278
+
279
+ /** [slack] is optional in config.toml, exactly like [tui] / [companion]. */
280
+ export const SLACK_DEFAULTS: SlackConfig = {
281
+ enabled: false,
282
+ channel: "",
283
+ channel_kind: "public",
284
+ bot_token_file: "slack_bot_token",
285
+ app_token_file: "slack_app_token",
286
+ post_min_interval_ms: 1100,
287
+ reconnect_backoff_ms: [1000, 2000, 5000, 15000, 30000],
288
+ server_ping_timeout_ms: 30000,
289
+ catchup_max_age_s: 300,
290
+ max_reply_chars: 3000,
291
+ name_cache_ttl_s: 86400,
292
+ users: {},
293
+ policy: SLACK_POLICY_DEFAULTS,
294
+ };
295
+
296
+ /**
297
+ * [recheck] — the post-approval delta recheck. One knob per trigger,
298
+ * so a noisy trigger can be taken out of the path deliberately, in one place.
299
+ * All on: the uphold-by-default design is what makes the broad set affordable.
300
+ */
301
+ export interface RecheckConfig {
302
+ enabled: boolean;
303
+ /** A push landing after an approve — the core case. */
304
+ on_push: boolean;
305
+ /** CI completing at the approved head; an adverse conclusion is evidence. */
306
+ on_ci: boolean;
307
+ /** The base branch moving under an approved PR (the noisiest by far). */
308
+ on_base_advance: boolean;
309
+ }
310
+
311
+ export const RECHECK_DEFAULTS: RecheckConfig = {
312
+ enabled: true,
313
+ on_push: true,
314
+ on_ci: true,
315
+ on_base_advance: true,
316
+ };
317
+
318
+ /**
319
+ * OpenViking (github.com/volcengine/OpenViking) — the context store the daemon
320
+ * runs as a SECOND process inside the container, on loopback :1933.
321
+ *
322
+ * Three invariants live in this block rather than in prose:
323
+ *
324
+ * * `version` + `llama_cpp_python_version` are the TRIPLE-MIRRORED pins —
325
+ * this block == `deploy/ansible/group_vars/all.yml` == the Dockerfile venv
326
+ * layer, exactly like the `[pi]` pin. `tests/openviking.test.ts` fails on
327
+ * drift, and the running server's self-reported version is drift-checked
328
+ * against `version` in the heartbeat.
329
+ * * `host` is loopback and the port is NEVER published out of the container
330
+ * (the playbook's `published_ports` carries only the webhook port).
331
+ * * every path here is a CONTAINER path; on a dev PC the binary is absent,
332
+ * which the supervisor treats as "unavailable", not as a crash loop.
333
+ */
334
+ export interface OpenVikingConfig {
335
+ /** Off ⇒ the supervisor is never constructed and status.json says so. */
336
+ enabled: boolean;
337
+ /** Who runs the server. `local` (the default, and what every live daemon
338
+ * does) supervises it, and the local embedder, as children of this process.
339
+ * `remote` is the memory sidecar (docker/memory/): `host`:`port` names a
340
+ * server some other container runs, the daemon only probes its health, and
341
+ * the sidecar owns the processes, the boot provisioning and the re-embed
342
+ * migration. */
343
+ endpoint: OpenVikingEndpoint;
344
+ /** The pinned server version (mirror 1 of 3). */
345
+ version: string;
346
+ /** Pinned llama-cpp-python: an OPTIONAL extra openviking does not declare,
347
+ * without which the default local dense embedder raises
348
+ * EmbeddingConfigurationError at startup (verified against 0.4.15). */
349
+ llama_cpp_python_version: string;
350
+ /** Upstream base commit of the vendored pi extension. Pinned beside the
351
+ * server version so `tests/openviking-fork.test.ts` gates drift instead of
352
+ * letting a bump rebase silently onto new upstream behaviour. */
353
+ extension_commit: string;
354
+ /** OpenViking user the daemon and review sessions act as. Their key is
355
+ * confined to this user server-side — that confinement is what keeps
356
+ * companion memory out of a posted review. */
357
+ user: string;
358
+ /** Tenant account; empty ⇒ derived from the key. */
359
+ account: string;
360
+ /** Secret FILE NAME under [github].secrets_dir holding the reviewer USER
361
+ * key. Never an ADMIN or ROOT key: ADMIN grants no cross-user data access
362
+ * and can list every user key in plaintext. */
363
+ reviewer_key_file: string;
364
+ /** ov.conf `server.auth_mode`. `api_key` is the required posture:
365
+ * every request carries a USER key the server CONFINES to its own tree, and
366
+ * that confinement is the only thing keeping companion memory out of a
367
+ * posted review. `dev` bypasses authentication entirely — it is a local
368
+ * development escape hatch, never the deployed value. MIRRORED into
369
+ * `deploy/ansible/group_vars/all.yml` (`openviking_auth_mode`), which the
370
+ * entrypoint template renders; `tests/openviking.test.ts` and the
371
+ * `scripts/deploy.sh` preflight both fail on drift. */
372
+ auth_mode: string;
373
+ /** Secret FILE NAME under the container's secrets mount holding the
374
+ * server's OWN root key — the credential ov.conf needs to run in `api_key`
375
+ * mode and to mint users at boot. It is the server's, never a client's: no
376
+ * daemon surface, no review session and no companion process ever reads it
377
+ * (`writeOvcliConf` refuses it as a session credential and the bash guard
378
+ * refuses to read the files that hold it). Mirrored as
379
+ * `openviking_root_key_file`. */
380
+ root_key_file: string;
381
+ /** The OpenViking users boot provisioning ENSURES exist — created if
382
+ * absent, left exactly as they are if present. Never rotated: a rotation
383
+ * would orphan the keys already distributed to the companion's PC. Mirrored
384
+ * as `openviking_provision_users` (comma-joined). */
385
+ provision_users: string[];
386
+ request_timeout_ms: number;
387
+ /** Tighter than `request_timeout_ms`: a search sits on the round-build
388
+ * path, where the deadline in [memory] is the real guard. It must exceed
389
+ * one document embedding on the live VM. Clamped to
390
+ * `SEARCH_TIMEOUT_BOUNDS_MS`; a seed that sent its search late passes its
391
+ * time left, and the search takes the shorter. */
392
+ search_timeout_ms: number;
393
+ /** Where an entry lands when the writer names no category. */
394
+ default_category: string;
395
+ read_cache_ttl_ms: number;
396
+ host: string;
397
+ port: number;
398
+ /** `openviking-server` inside the image's venv. */
399
+ bin: string;
400
+ /** `~/.openviking` is symlinked here at boot — it must sit on the state
401
+ * volume so memory survives an image rebuild. */
402
+ home: string;
403
+ /** Empty ⇒ `<home>/ov.conf` (the path the boot template writes). */
404
+ config_file: string;
405
+ /** Empty ⇒ `<home>/workspace` (ov.conf `storage.workspace`). */
406
+ workspace: string;
407
+ /** ov.conf `server.workers` — one worker on a 2-CPU cap. */
408
+ workers: number;
409
+ /** Accepted, and read by nothing in the daemon: the entrypoint that
410
+ * templates ov.conf takes its queue concurrency from the deploy
411
+ * (`openviking_queue_concurrency` in group_vars, or
412
+ * `QCR_OPENVIKING_QUEUE_CONCURRENCY` for the plain image), never from here. */
413
+ queue_concurrency: number;
414
+ /** Health-poll cadence; the heartbeat reports the CACHED sample, so
415
+ * status.json writing never waits on the network. */
416
+ health_interval_s: number;
417
+ /** The supervisor's `/health` + `/ready` probe and the local embedder's
418
+ * `/health` probe. `/ready` runs an embedding, so it queues behind a case
419
+ * batch's embeddings — a shorter timeout makes `/ready` flap and can tip
420
+ * memory health to degraded. It is the timeout behind memory health's
421
+ * "serving" half (`memoryServing`). Clamped to `HEALTH_TIMEOUT_BOUNDS_MS`. */
422
+ health_timeout_ms: number;
423
+ /** Restart backoff ladder; the last entry is the ceiling. The supervisor
424
+ * never gives up — a memory engine that stays down is worse than a slow
425
+ * retry, and every call site already degrades memory-less. */
426
+ restart_backoff_ms: number[];
427
+ /** A child that stayed up this long resets the backoff ladder. */
428
+ restart_reset_after_s: number;
429
+ /** Which embedder memory runs. `local` is the shipped
430
+ * default: the LOCAL ENGLISH EMBEDDER (`LOCAL_EMBEDDING`) — a loopback
431
+ * service inside the container, so no memory text leaves the machine.
432
+ * `openai` is the explicit hosted opt-in and needs `embedding_key_file` in
433
+ * the secrets dir. Mirrored as `openviking_embedding_provider`. This is the
434
+ * operator's word, not ov.conf's: under `local` the entrypoint renders
435
+ * ov.conf's OpenAI-COMPATIBLE provider with `api_base` on loopback
436
+ * (`ovConfEmbedder`). */
437
+ embedding_provider: EmbeddingProvider;
438
+ /** ov.conf `embedding.dense.model` / `.dimension`. Under `local` they are
439
+ * the local embedder's model and its fixed dimension (`LOCAL_EMBEDDING`);
440
+ * under `openai` the operator names the hosted model and its dimension. */
441
+ embedding_model: string;
442
+ embedding_dimension: number;
443
+ /** Secrets-dir file name of the HOSTED embeddings credential — deliberately
444
+ * NOT `openai_api_key` (pi's model credential), so the two can be set
445
+ * independently. Read only by the ov.conf render, only under `openai`. */
446
+ embedding_key_file: string;
447
+ /** The local embedder's MODEL FILE pin (the version-pinning rule): the GGUF the
448
+ * image downloads at BUILD time from `local_embedder_url`, verified against
449
+ * `local_embedder_sha256` there and again by the service at every start.
450
+ * TRIPLE-MIRRORED like `version`: this block == group_vars
451
+ * (`openviking_local_embedder_*`) == the memory Dockerfile's model layer —
452
+ * `tests/openviking.test.ts`, `tests/memory-image.test.ts` and the
453
+ * `scripts/deploy.sh` preflight refuse on drift. The file's basename is
454
+ * `<LOCAL_EMBEDDING.model>.gguf`; the image carries the file whatever the
455
+ * provider, so a hosted opt-in can come back to local without a rebuild. */
456
+ local_embedder_file: string;
457
+ local_embedder_url: string;
458
+ local_embedder_sha256: string;
459
+ /** The service listens on 127.0.0.1 at this port — never published; ov.conf's
460
+ * `api_base` names it. Mirrored as `openviking_local_embedder_port`. */
461
+ local_embedder_port: number;
462
+ /** llama.cpp threads for the service. One is the budget the model was chosen
463
+ * on (~25 ms for a round's context query): the container has two CPUs and a
464
+ * review round to run. Mirrored as `openviking_local_embedder_threads`. */
465
+ local_embedder_threads: number;
466
+ }
467
+
468
+ export const OPENVIKING_ENDPOINTS = ["local", "remote"] as const;
469
+ export type OpenVikingEndpoint = (typeof OPENVIKING_ENDPOINTS)[number];
470
+
471
+ export const EMBEDDING_PROVIDERS = ["local", "openai"] as const;
472
+ export type EmbeddingProvider = (typeof EMBEDDING_PROVIDERS)[number];
473
+
474
+ /**
475
+ * The LOCAL ENGLISH EMBEDDER: the model
476
+ * `embedding_provider = "local"` runs, served on loopback by
477
+ * `scripts/openviking-embedder.py` from the GGUF the image pins. Chosen by an
478
+ * offline retrieval evaluation over the reviewer's own memory
479
+ * (docs/memory-embeddings.md: MRR@10 0.56 against 0.34 for the engine's
480
+ * built-in Chinese-first model; ~25 ms per round query on one thread). The
481
+ * service's model table (`--describe`) carries the same name and dimension
482
+ * plus the query instruction; `tests/openviking-embedder.test.ts` holds the
483
+ * two together. Mirrored by `LOCAL_EMBEDDER` in scripts/openviking-reembed.mjs.
484
+ */
485
+ export const LOCAL_EMBEDDING = Object.freeze({ model: "bge-small-en-v1.5-f16", dimension: 384 });
486
+ /** The engine's BUILT-IN local embedder — the retired meaning of `local`, and
487
+ * what an older store was embedded under. A config.toml that still spells it
488
+ * under `local` is read as the current local embedder rather than refused: the
489
+ * daemon must not stop booting over a value an earlier release required. The
490
+ * re-embed migration carries the store across. */
491
+ export const RETIRED_LOCAL_EMBEDDING = Object.freeze({
492
+ model: "bge-small-zh-v1.5-f16",
493
+ dimension: 512,
494
+ });
495
+ /** What an `openai` opt-in runs unless the operator names another model. */
496
+ export const HOSTED_EMBEDDING = Object.freeze({ model: "text-embedding-3-small", dimension: 1536 });
497
+
498
+ /** ov.conf's `embedding.dense.provider` under `local`: the engine's OpenAI-
499
+ * COMPATIBLE client — its async embedder — pointed at the loopback service.
500
+ * The wire format, never a destination. */
501
+ export const LOCAL_EMBEDDER_WIRE_PROVIDER = "openai";
502
+
503
+ /** `http://127.0.0.1:<port>/v1` — ov.conf's `api_base` under `local`. */
504
+ export function localEmbedderBaseUrl(c: Pick<OpenVikingConfig, "local_embedder_port">): string {
505
+ return `http://127.0.0.1:${c.local_embedder_port}/v1`;
506
+ }
507
+
508
+ /** What ov.conf's `embedding.dense` must say for this config: provider,
509
+ * model and dimension as the ENGINE spells them (the record its vector
510
+ * collection stores), plus the loopback `api_base` under `local`. The
511
+ * daemon's drift check holds the rendered ov.conf to it. */
512
+ export interface OvConfEmbedder {
513
+ provider: string;
514
+ model: string;
515
+ dimension: number;
516
+ api_base?: string;
517
+ }
518
+
519
+ export function ovConfEmbedder(
520
+ c: Pick<
521
+ OpenVikingConfig,
522
+ "embedding_provider" | "embedding_model" | "embedding_dimension" | "local_embedder_port"
523
+ >,
524
+ ): OvConfEmbedder {
525
+ if (c.embedding_provider === "local")
526
+ return {
527
+ provider: LOCAL_EMBEDDER_WIRE_PROVIDER,
528
+ model: c.embedding_model,
529
+ dimension: c.embedding_dimension,
530
+ api_base: localEmbedderBaseUrl(c),
531
+ };
532
+ return {
533
+ provider: c.embedding_provider,
534
+ model: c.embedding_model,
535
+ dimension: c.embedding_dimension,
536
+ };
537
+ }
538
+
539
+ /** Round-seeding knobs: how much memory rides each round prompt and
540
+ * how long the daemon is willing to wait for it. Every value is defaulted —
541
+ * an absent `[memory]` block is a supported configuration, and a store that
542
+ * misses the deadline costs the round its context, never the round. */
543
+ export interface MemoryConfig {
544
+ /** Token budget for the assembled context block. */
545
+ context_tokens: number;
546
+ /** Deadline for the head + block assembly, after which the round proceeds
547
+ * memory-less. It must exceed one document embedding on the live VM; the
548
+ * seed's wait for the case writer's
549
+ * embedding tail is inside it. Clamped to `CONTEXT_TIMEOUT_BOUNDS_MS`. */
550
+ context_timeout_ms: number;
551
+ /** The embedding tail of one written finding case (ms): after
552
+ * a case batch's write returns the server goes on embedding it for about
553
+ * this long a case, and a round seed starting inside that waits for it —
554
+ * within its own `context_timeout_ms`. Clamped to 0…30000; 0 turns the
555
+ * tail off. */
556
+ case_embed_tail_ms: number;
557
+ /** Memory health (`src/memory/health.ts`): how often the daemon's
558
+ * AUTHENTICATED probe lists the reviewer memories root with each credential
559
+ * in use (its key file and the sessions' ovcli.conf). A leftover
560
+ * `alert_cooldown_min` key in a live config.toml is ignored. */
561
+ probe_interval_s: number;
562
+ /** No authenticated answer for this long ⇒ `down` (wakes `memory_down`). */
563
+ down_after_min: number;
564
+ /** After boot, a server still warming up reads `unknown`, not degraded,
565
+ * for this long. Never applies to an authentication failure. */
566
+ boot_grace_min: number;
567
+ /** An outage that lasts re-wakes the overseer this often. */
568
+ rewake_h: number;
569
+ /** The TUI files ONE tracker ticket for a memory incident older than this
570
+ * (sourced from status.json, never from memory). */
571
+ incident_ticket_after_min: number;
572
+ /** Writes that stick: how often the backfill pump writes the
573
+ * [feedback] signals memory still owes (at most 20 a tick, only while
574
+ * memory health reads healthy). */
575
+ backfill_interval_s: number;
576
+ /** An owed signal older than this wakes `memory_write_backlog` (and again
577
+ * every `rewake_h` while the backlog lasts). */
578
+ write_backlog_after_min: number;
579
+ /** Finding cases (src/cases.ts): how often the case writer syncs the cases
580
+ * state.db says are owed and, only while memory health reads healthy,
581
+ * writes up to 2 of them (one batch-write, never started while a round
582
+ * reads memory) and backfills two more PRs. */
583
+ cases_interval_s: number;
584
+ /** How far back the case backfill reaches: PRs with a finding posted in
585
+ * the last this-many days get their cases written (resumable, a few PRs a
586
+ * tick, only while memory reads healthy). 0 turns the backfill off. */
587
+ cases_backfill_days: number;
588
+ /** The learner (src/learner/learner.ts). The miner runs this
589
+ * many minutes before the EOD sweep (and on `qcr lessons mine`). */
590
+ lessons_mine_lead_min: number;
591
+ /** How far back a mine reads human outcomes (fates, the feedback ledger,
592
+ * recall sources). */
593
+ lessons_mine_days: number;
594
+ /** No `eod-curation-receipt` this long after the EOD sweep was delivered
595
+ * wakes `eod_curation_missing`. */
596
+ lessons_receipt_wait_min: number;
597
+ /** An offered candidate still undecided this many ops days later EXPIRES —
598
+ * recorded on the row, in an event and in the packet. */
599
+ lessons_expiry_ops_days: number;
600
+ /** At most this many candidates are offered for the FIRST time per EOD
601
+ * packet (misses first); the rest wait for a later one. Carried candidates
602
+ * are never capped. */
603
+ lessons_packet_new_max: number;
604
+ /** A bot-approved merged PR's later commits are read this long after the
605
+ * merge (post-merge recall, read-only git on the base clone). */
606
+ lessons_recall_window_days: number;
607
+ /** The window a cluster's recurrence and counterweight count over. */
608
+ lessons_recurrence_days: number;
609
+ /** The digests whose "Learnings" never reached memory — the one-time
610
+ * backfill into candidates (inclusive, YYYY-MM-DD). */
611
+ lessons_digest_backfill_from: string;
612
+ lessons_digest_backfill_to: string;
613
+ /** How many items the round's context search may draw from each area it
614
+ * searches. A round searches `experiences/` ONLY
615
+ * (`ROUND_SEARCH_AREAS`, src/memory/layout.ts): any other key here is
616
+ * ignored with one log line (src/openviking/context.ts
617
+ * `roundSearchQuotas`), and a missing or non-positive `experiences` count
618
+ * takes the default — the search is never left to the server's own balance
619
+ * across every scope (see the comment in config.toml). */
620
+ context_quotas: Record<string, number>;
621
+ /**
622
+ * How curated lessons and past finding cases reach a review:
623
+ * `off` — nothing runs: no selection, no shadow ledger;
624
+ * `shadow` — the SHIPPED value: every round selects lessons and cases
625
+ * through OpenViking (one bounded prefetch, never delaying the
626
+ * round) and the shadow ledger records what WOULD have been
627
+ * delivered and which posted findings a delivered dont-raise
628
+ * lesson would have touched — and NO prompt, task file or
629
+ * agent changes;
630
+ * `on` — delivery: the same selection, AWAITED beside the
631
+ * round's memory seed within the same budget, and rendered —
632
+ * each lens's task file, the lessons files of the validator
633
+ * and the lenses a session adds, the main session's "Lessons
634
+ * in force" index and past-review cases (the bleed rule
635
+ * reworded), the validator's `lesson-shape` addendum, and the
636
+ * repo facts for the paths a recheck or an author reply
637
+ * touches. A round that selects nothing renders as `off`.
638
+ * The owner switches it to `on` after reading `qcr lessons shadow-report`.
639
+ */
640
+ delivery: MemoryDelivery;
641
+ /** The round's ONE lesson prefetch may take at most this long (ms) when the
642
+ * round WAITS for it — under `on` — then whatever arrived is used and every
643
+ * find still out is a recorded timeout. Clamped to 1…2000: the contract's
644
+ * hard ceiling is 2 s. */
645
+ delivery_budget_ms: number;
646
+ /** The same prefetch's budget (ms) when NOTHING waits for it — under
647
+ * `shadow`, where it starts after the round's handoff: looser,
648
+ * so a shadow selection behind a busy embedder still records what it would
649
+ * have delivered. Clamped to 1…20000 — it holds the selector's two places
650
+ * in the embedder queue for at most that long. */
651
+ shadow_budget_ms: number;
652
+ }
653
+
654
+ /** The three memory timeouts are BOUNDED — memory must never
655
+ * hold a round past a budget an operator can read off the config, and a
656
+ * mistyped 0 must not turn every seed memory-less. Inclusive ms. */
657
+ export const CONTEXT_TIMEOUT_BOUNDS_MS = { min: 100, max: 15_000 } as const;
658
+ export const SEARCH_TIMEOUT_BOUNDS_MS = { min: 100, max: 15_000 } as const;
659
+ export const HEALTH_TIMEOUT_BOUNDS_MS = { min: 100, max: 20_000 } as const;
660
+ export const CASE_EMBED_TAIL_MAX_MS = 30_000;
661
+
662
+ /** `v` as an integer inside `bounds`, or `fallback` when it is not a finite
663
+ * number at all. */
664
+ function clampMs(v: unknown, bounds: { min: number; max: number }, fallback: number): number {
665
+ const n = typeof v === "number" ? v : Number.NaN;
666
+ if (!Number.isFinite(n)) return fallback;
667
+ return Math.max(bounds.min, Math.min(bounds.max, Math.floor(n)));
668
+ }
669
+
670
+ /** The contract's hard ceiling on the lesson prefetch. */
671
+ export const LESSON_PREFETCH_MAX_MS = 2000;
672
+ /** The shadow prefetch's ceiling and default. The default covers
673
+ * the live VM's measured worst case — a find's 2.5 s p90 behind a case batch
674
+ * already on the embedder (two ~3.4 s embeddings, src/cases.ts). */
675
+ export const LESSON_SHADOW_PREFETCH_MAX_MS = 20_000;
676
+ export const LESSON_SHADOW_PREFETCH_DEFAULT_MS = 10_000;
677
+
678
+ /**
679
+ * The [memory] block, defaults filled and the delivery switch checked: an
680
+ * unknown `delivery` value REFUSES to load (a misspelt `"onn"` must not
681
+ * quietly mean shadow, or off), and the prefetch budget is clamped to the
682
+ * contract's ceiling.
683
+ */
684
+ export function parseMemory(raw: Record<string, unknown>): MemoryConfig {
685
+ const m = { ...MEMORY_DEFAULTS, ...(raw as Partial<MemoryConfig>) };
686
+ if (!(MEMORY_DELIVERY_MODES as readonly unknown[]).includes(m.delivery))
687
+ throw new Error(
688
+ `[memory] delivery must be one of ${MEMORY_DELIVERY_MODES.map((v) => `"${v}"`).join(", ")} (got ${JSON.stringify(m.delivery)})`,
689
+ );
690
+ const budget = Number(m.delivery_budget_ms);
691
+ m.delivery_budget_ms = Number.isFinite(budget)
692
+ ? Math.max(1, Math.min(LESSON_PREFETCH_MAX_MS, Math.floor(budget)))
693
+ : LESSON_PREFETCH_MAX_MS;
694
+ const shadow = Number(m.shadow_budget_ms);
695
+ m.shadow_budget_ms = Number.isFinite(shadow)
696
+ ? Math.max(1, Math.min(LESSON_SHADOW_PREFETCH_MAX_MS, Math.floor(shadow)))
697
+ : LESSON_SHADOW_PREFETCH_DEFAULT_MS;
698
+ m.context_timeout_ms = clampMs(
699
+ m.context_timeout_ms,
700
+ CONTEXT_TIMEOUT_BOUNDS_MS,
701
+ MEMORY_DEFAULTS.context_timeout_ms,
702
+ );
703
+ m.case_embed_tail_ms = clampMs(
704
+ m.case_embed_tail_ms,
705
+ { min: 0, max: CASE_EMBED_TAIL_MAX_MS },
706
+ MEMORY_DEFAULTS.case_embed_tail_ms,
707
+ );
708
+ return m;
709
+ }
710
+
711
+ /** The quota a round's context search gives each area it searches when
712
+ * `[memory] context_quotas` names none (the round context assembler reads
713
+ * this same constant). */
714
+ export const DEFAULT_ROUND_QUOTA = 24;
715
+
716
+ export const MEMORY_DEFAULTS: MemoryConfig = {
717
+ context_tokens: 2000,
718
+ context_timeout_ms: 9000,
719
+ case_embed_tail_ms: 4000,
720
+ probe_interval_s: 120,
721
+ down_after_min: 10,
722
+ boot_grace_min: 5,
723
+ rewake_h: 6,
724
+ incident_ticket_after_min: 120,
725
+ backfill_interval_s: 60,
726
+ write_backlog_after_min: 120,
727
+ cases_interval_s: 120,
728
+ cases_backfill_days: 60,
729
+ lessons_mine_lead_min: 15,
730
+ lessons_mine_days: 45,
731
+ lessons_receipt_wait_min: 180,
732
+ lessons_expiry_ops_days: 7,
733
+ lessons_packet_new_max: 40,
734
+ lessons_recall_window_days: 14,
735
+ lessons_recurrence_days: 90,
736
+ lessons_digest_backfill_from: "2026-08-31",
737
+ lessons_digest_backfill_to: "2026-10-02",
738
+ context_quotas: { experiences: DEFAULT_ROUND_QUOTA },
739
+ delivery: "shadow",
740
+ delivery_budget_ms: LESSON_PREFETCH_MAX_MS,
741
+ shadow_budget_ms: LESSON_SHADOW_PREFETCH_DEFAULT_MS,
742
+ };
743
+
744
+ /** [openviking] is optional in config.toml — the defaults ARE the shipped
745
+ * configuration, exactly like [tui]/[companion]. */
746
+ export const OPENVIKING_DEFAULTS: OpenVikingConfig = {
747
+ enabled: true,
748
+ endpoint: "local",
749
+ version: "0.4.15",
750
+ llama_cpp_python_version: "0.3.35",
751
+ extension_commit: "d436baa23c83938b22bb0aab08b0ce48568b5ff4",
752
+ user: "reviewer",
753
+ account: "",
754
+ reviewer_key_file: "openviking_reviewer_key",
755
+ auth_mode: "api_key",
756
+ root_key_file: "openviking_root_key",
757
+ provision_users: ["reviewer", "companion"],
758
+ request_timeout_ms: 10000,
759
+ search_timeout_ms: 8000,
760
+ default_category: "experiences",
761
+ read_cache_ttl_ms: 15000,
762
+ host: "127.0.0.1",
763
+ port: 1933,
764
+ bin: "/opt/openviking/venv/bin/openviking-server",
765
+ home: "/opt/quickreview/state/openviking",
766
+ config_file: "", // → <home>/ov.conf
767
+ workspace: "", // → <home>/workspace
768
+ workers: 1,
769
+ queue_concurrency: 2,
770
+ health_interval_s: 30,
771
+ health_timeout_ms: 6000,
772
+ restart_backoff_ms: [1000, 2000, 5000, 15000, 30000, 60000],
773
+ restart_reset_after_s: 120,
774
+ embedding_provider: "local",
775
+ embedding_model: LOCAL_EMBEDDING.model,
776
+ embedding_dimension: LOCAL_EMBEDDING.dimension,
777
+ embedding_key_file: "openai_embeddings_api_key",
778
+ local_embedder_file: `/opt/openviking/models/${LOCAL_EMBEDDING.model}.gguf`,
779
+ local_embedder_url: `https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/d32f8c040ea3b516330eeb75b72bcc2d3a780ab7/${LOCAL_EMBEDDING.model}.gguf`,
780
+ local_embedder_sha256: "f0b2fef971e8366438bfd2d9aefea1b0115919389448806d290237f638bae999",
781
+ local_embedder_port: 1934,
782
+ local_embedder_threads: 1,
783
+ };
784
+
785
+ /**
786
+ * The [openviking] block, defaults filled and the embedder checked.
787
+ *
788
+ * The embedder is validated at LOAD time because the boot validates it too,
789
+ * and a memory engine that refuses to start is a silent outage: an unknown
790
+ * provider, a non-positive dimension, a `local` model/dimension that is not
791
+ * the local embedder's, or a local model-file pin that does not name that
792
+ * model are all config errors here.
793
+ */
794
+ export function parseOpenViking(raw: Record<string, unknown>): OpenVikingConfig {
795
+ const merged = { ...OPENVIKING_DEFAULTS, ...(raw as Partial<OpenVikingConfig>) };
796
+ if (!(OPENVIKING_ENDPOINTS as readonly string[]).includes(merged.endpoint)) {
797
+ throw new Error(
798
+ `config.toml: [openviking] endpoint must be one of ${OPENVIKING_ENDPOINTS.join(", ")} (got '${String(merged.endpoint)}')`,
799
+ );
800
+ }
801
+ // Bounded, never a tight loop or an unbounded wait.
802
+ merged.search_timeout_ms = clampMs(
803
+ merged.search_timeout_ms,
804
+ SEARCH_TIMEOUT_BOUNDS_MS,
805
+ OPENVIKING_DEFAULTS.search_timeout_ms,
806
+ );
807
+ merged.health_timeout_ms = clampMs(
808
+ merged.health_timeout_ms,
809
+ HEALTH_TIMEOUT_BOUNDS_MS,
810
+ OPENVIKING_DEFAULTS.health_timeout_ms,
811
+ );
812
+ // The defaults are the LOCAL embedder: a hosted opt-in that omits the model
813
+ // runs HOSTED_EMBEDDING, never the local model (which no hosted API serves).
814
+ // A named hosted model with no dimension has no default to fall back on.
815
+ if (merged.embedding_provider !== "local") {
816
+ if (raw.embedding_model === undefined) {
817
+ merged.embedding_model = HOSTED_EMBEDDING.model;
818
+ if (raw.embedding_dimension === undefined)
819
+ merged.embedding_dimension = HOSTED_EMBEDDING.dimension;
820
+ } else if (raw.embedding_dimension === undefined) {
821
+ if (merged.embedding_model !== HOSTED_EMBEDDING.model)
822
+ throw new Error(
823
+ `config.toml: [openviking] embedding_model = "${String(merged.embedding_model)}" needs its embedding_dimension set too`,
824
+ );
825
+ merged.embedding_dimension = HOSTED_EMBEDDING.dimension;
826
+ }
827
+ } else if (
828
+ merged.embedding_model === RETIRED_LOCAL_EMBEDDING.model &&
829
+ (raw.embedding_dimension === undefined ||
830
+ merged.embedding_dimension === RETIRED_LOCAL_EMBEDDING.dimension)
831
+ ) {
832
+ // An earlier release required this spelling under `local`. It means the
833
+ // current local embedder — the boot renders the same, and the re-embed
834
+ // migration moves the store across.
835
+ merged.embedding_model = LOCAL_EMBEDDING.model;
836
+ merged.embedding_dimension = LOCAL_EMBEDDING.dimension;
837
+ }
838
+ if (!(EMBEDDING_PROVIDERS as readonly string[]).includes(merged.embedding_provider)) {
839
+ throw new Error(
840
+ `config.toml: [openviking] embedding_provider must be one of ${EMBEDDING_PROVIDERS.join(", ")} (got '${String(merged.embedding_provider)}')`,
841
+ );
842
+ }
843
+ if (!Number.isInteger(merged.embedding_dimension) || merged.embedding_dimension <= 0) {
844
+ throw new Error(
845
+ `config.toml: [openviking] embedding_dimension must be a positive integer (got '${String(merged.embedding_dimension)}')`,
846
+ );
847
+ }
848
+ if (
849
+ merged.embedding_provider === "local" &&
850
+ (merged.embedding_model !== LOCAL_EMBEDDING.model ||
851
+ merged.embedding_dimension !== LOCAL_EMBEDDING.dimension)
852
+ ) {
853
+ throw new Error(
854
+ `config.toml: [openviking] embedding_provider = "local" runs the local English embedder, ${LOCAL_EMBEDDING.model} at ${LOCAL_EMBEDDING.dimension} dims — ` +
855
+ `the only local model the image pins; set embedding_model/embedding_dimension to it, or opt into a hosted provider`,
856
+ );
857
+ }
858
+ if (
859
+ typeof merged.embedding_key_file !== "string" ||
860
+ merged.embedding_key_file.trim() === "" ||
861
+ merged.embedding_key_file.includes("/")
862
+ ) {
863
+ throw new Error(
864
+ "config.toml: [openviking] embedding_key_file must be a bare file name inside the secrets dir",
865
+ );
866
+ }
867
+ checkLocalEmbedderPin(merged);
868
+ return resolveOpenVikingPaths(merged);
869
+ }
870
+
871
+ /** The local embedder's model-file pin and service knobs. The
872
+ * file and the URL must both name `LOCAL_EMBEDDING.model`: a pin for another
873
+ * model would put a file the service's model table does not describe under a
874
+ * name the vector store trusts. */
875
+ function checkLocalEmbedderPin(c: OpenVikingConfig): void {
876
+ const gguf = `${LOCAL_EMBEDDING.model}.gguf`;
877
+ const file = c.local_embedder_file;
878
+ if (typeof file !== "string" || !file.startsWith("/") || !file.endsWith(`/${gguf}`)) {
879
+ throw new Error(
880
+ `config.toml: [openviking] local_embedder_file must be an absolute container path ending in /${gguf} (got '${String(file)}')`,
881
+ );
882
+ }
883
+ const url = c.local_embedder_url;
884
+ if (typeof url !== "string" || !url.startsWith("https://") || !url.endsWith(`/${gguf}`)) {
885
+ throw new Error(
886
+ `config.toml: [openviking] local_embedder_url must be an https URL ending in /${gguf} (got '${String(url)}')`,
887
+ );
888
+ }
889
+ if (
890
+ typeof c.local_embedder_sha256 !== "string" ||
891
+ !/^[0-9a-f]{64}$/.test(c.local_embedder_sha256)
892
+ ) {
893
+ throw new Error(
894
+ "config.toml: [openviking] local_embedder_sha256 must be the file's sha256, 64 lowercase hex digits",
895
+ );
896
+ }
897
+ const port = c.local_embedder_port;
898
+ if (!Number.isInteger(port) || port < 1 || port > 65535 || port === c.port) {
899
+ throw new Error(
900
+ `config.toml: [openviking] local_embedder_port must be a TCP port other than the server's own ${c.port} (got '${String(port)}')`,
901
+ );
902
+ }
903
+ const threads = c.local_embedder_threads;
904
+ if (!Number.isInteger(threads) || threads < 1 || threads > 16) {
905
+ throw new Error(
906
+ `config.toml: [openviking] local_embedder_threads must be an integer in 1..16 (got '${String(threads)}')`,
907
+ );
908
+ }
909
+ }
910
+
911
+ /** Derived paths: `config_file`/`workspace` default off `home`, so the three
912
+ * never drift apart in a hand-edited config. */
913
+ export function resolveOpenVikingPaths(c: OpenVikingConfig): OpenVikingConfig {
914
+ const home = expandHome(c.home);
915
+ return {
916
+ ...c,
917
+ home,
918
+ config_file: c.config_file ? expandHome(c.config_file) : join(home, "ov.conf"),
919
+ workspace: c.workspace ? expandHome(c.workspace) : join(home, "workspace"),
920
+ };
921
+ }
922
+
923
+ /** `http://host:port` — the one place the base URL is spelled. */
924
+ export function openVikingBaseUrl(c: OpenVikingConfig): string {
925
+ return `http://${c.host}:${c.port}`;
926
+ }
927
+
928
+ /**
929
+ * [ops] — the operations timezone: the ONE knob that defines whose
930
+ * calendar day the system runs on. Every day-scoped feature derives its day
931
+ * from here — the [feedback] same-day addendum + agent-rebuild rollover,
932
+ * metrics day buckets, TUI alert fold memory and companion day rollover —
933
+ * and the two sections that carry their own zone ([eod] and [slack.policy]
934
+ * `timezone`) DEFAULT from it, each still individually overridable in its own
935
+ * section.
936
+ */
937
+ export interface OpsConfig {
938
+ /** IANA zone (e.g. "Europe/Warsaw") of the operator's day. */
939
+ timezone: string;
940
+ }
941
+
942
+ /** `[eod] pinned_watchdog_window_min`'s default, and the watchdog's own. */
943
+ export const EOD_PINNED_WATCHDOG_WINDOW_MIN = 90;
944
+
945
+ /** Neutral default — a config that never names a zone runs on UTC days. */
946
+ export const OPS_DEFAULTS: OpsConfig = { timezone: "UTC" };
947
+
948
+ /** The same Intl-based check the EOD machinery relies on (src/eod.ts builds
949
+ * an Intl.DateTimeFormat per zone): an unknown zone throws RangeError at
950
+ * formatter construction, so validate at parse time rather than at 18:00. */
951
+ export function assertIanaTimeZone(tz: string, where: string): string {
952
+ try {
953
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
954
+ } catch {
955
+ throw new Error(`config.toml: ${where} '${tz}' is not a valid IANA timezone`);
956
+ }
957
+ return tz;
958
+ }
959
+
960
+ /** A section carrying its own `timezone` (defaulted from [ops]), returned
961
+ * once that zone passes `assertIanaTimeZone`. */
962
+ function checkSectionTimeZone<T extends { timezone: string }>(section: T, where: string): T {
963
+ assertIanaTimeZone(String(section.timezone), where);
964
+ return section;
965
+ }
966
+
967
+ /**
968
+ * [ingress] — the webhook ingress shape. `tunnel-file` turns on the
969
+ * hostname-file watcher: a tunnel sidecar writes its assigned bare
970
+ * public hostname (e.g. `abc.trycloudflare.com`, trailing newline tolerated)
971
+ * to `hostname_file`, and the daemon re-points the App's own webhook URL when
972
+ * it changes — through the app-self-config gate class, which executes even in
973
+ * dry-run (AGENTS.md invariant 6). `none` (the default, and
974
+ * what an absent section means) starts nothing at all.
975
+ */
976
+ export interface IngressConfig {
977
+ /** "none" ⇒ no watcher is ever constructed; "tunnel-file" ⇒ self-heal on. */
978
+ shape: "none" | "tunnel-file";
979
+ /** File the tunnel sidecar writes the bare public hostname into. Empty ⇒
980
+ * `<root>/state/ingress/hostname` (see ingressHostnameFile). */
981
+ hostname_file: string;
982
+ /** Settle time after a file change before the re-point runs — quick-tunnel
983
+ * sidecars write, then sometimes rewrite, within a second or two. */
984
+ debounce_ms: number;
985
+ /** The reliable path: a periodic sweep backs up fs.watch (the fast path),
986
+ * exactly like the verdict watcher. */
987
+ sweep_interval_s: number;
988
+ }
989
+
990
+ export const INGRESS_DEFAULTS: IngressConfig = {
991
+ shape: "none",
992
+ hostname_file: "",
993
+ debounce_ms: 3000,
994
+ sweep_interval_s: 300,
995
+ };
996
+
997
+ /** The hostname file's resolved path — the one place the default is spelled. */
998
+ export function ingressHostnameFile(cfg: Config, root: string): string {
999
+ const configured = cfg.ingress.hostname_file.trim();
1000
+ return configured ? expandHome(configured) : join(root, "state", "ingress", "hostname");
1001
+ }
1002
+
1003
+ /** The whole config.toml: the review sections (`CoreConfig`) and every
1004
+ * other surface's. */
1005
+ export interface Config extends CoreConfig {
1006
+ ops: OpsConfig;
1007
+ ingress: IngressConfig;
1008
+ server: { hostname: string; port: number };
1009
+ daemon: {
1010
+ poll_interval_s: number;
1011
+ max_concurrent_rounds: number;
1012
+ round_deadline_min: number;
1013
+ /** How many times the daemon may self-requeue a stuck round (overdue +
1014
+ * no verdict + session dead) per PR head. Past the cap the round
1015
+ * terminalizes through the overdue-anomaly path. Optional
1016
+ * so hand-rolled test configs keep compiling; readers default to
1017
+ * DEFAULT_MAX_SELF_REQUEUES. */
1018
+ max_self_requeues?: number;
1019
+ /** Minutes after dispatch before a running round with no prompt receipt
1020
+ * counts as lost and is recovered. Optional; readers
1021
+ * default to DEFAULT_ROUND_STALL_GRACE_MIN. */
1022
+ round_stall_grace_min?: number;
1023
+ /** How many times a round's prompt may be RE-DELIVERED before the
1024
+ * expensive recovery takes over. Separate from `max_self_requeues` on
1025
+ * purpose — redelivery costs one file write and keeps the round, the
1026
+ * session and the conversation, where a self-requeue burns a round and
1027
+ * (for a live session) kills it. Optional; readers default to
1028
+ * DEFAULT_MAX_PROMPT_REDELIVERIES. */
1029
+ max_prompt_redeliveries?: number;
1030
+ /** Daemon→pi stage bounds. Optional only for old configs; the
1031
+ * parser supplies the exported defaults so production always has finite
1032
+ * values. Progress telemetry never changes any of them. */
1033
+ session_command_start_lease_s?: number;
1034
+ /** Maximum wall time from tmux birth to a generation-matched inject-bridge
1035
+ * startup receipt. Tmux existence alone is never delivery readiness. */
1036
+ session_bridge_ready_timeout_s?: number;
1037
+ session_command_running_max_min?: number;
1038
+ /** Round execution milestones, all anchored to durable command
1039
+ * start. They never renew from heartbeat/activity telemetry. */
1040
+ round_first_pass_max_min?: number;
1041
+ round_passes_max_min?: number;
1042
+ round_finalize_grace_min?: number;
1043
+ /** Once a round's review is CONFIRMED posted, a stage limit
1044
+ * no longer cancels it — the session's tail (ledger, findings record,
1045
+ * verdict file) gets this long after the posting to finish, and a round
1046
+ * still without a verdict then is settled from the posting receipt. Never
1047
+ * a self-requeue, so never a second posting. Readers default to
1048
+ * DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN. */
1049
+ round_posted_tail_grace_min?: number;
1050
+ /** The extra minutes a follow-up round whose pre-approval sweep
1051
+ * is OWED gets for its passes and its hard cap — the sweep is a whole
1052
+ * round-1 plan run after the delta's. Readers default to
1053
+ * DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN. */
1054
+ follow_up_sweep_extra_min?: number;
1055
+ posting_receipt_grace_s?: number;
1056
+ /** A review POST GitHub answered 5xx (or not at all) is handed
1057
+ * to the daemon as a `deferred` receipt and retried at these offsets, in
1058
+ * minutes from the first failure — one attempt per entry, each through
1059
+ * the MutationGate after a marker read. Readers default to
1060
+ * DEFAULT_POSTING_RETRY_OFFSETS_MIN. */
1061
+ posting_retry_offsets_min?: number[];
1062
+ /** Every retry lies inside this many minutes of the first failure; past
1063
+ * it, nothing is POSTed (one last marker read) and the PR goes
1064
+ * FAILED_LOUD. Readers default to DEFAULT_POSTING_RETRY_WINDOW_MIN. */
1065
+ posting_retry_window_min?: number;
1066
+ session_command_unknown_grace_s?: number;
1067
+ session_command_unknown_max_checks?: number;
1068
+ session_command_max_start_retries?: number;
1069
+ /** How long a compaction the session's inject bridge reports
1070
+ * running may hold a waiting command's start lease open. pi refuses every
1071
+ * prompt while it compacts, so a command queued behind one is late, not
1072
+ * lost; past this bound a compaction still "running" is no reason to wait,
1073
+ * and the bounded redelivery → kill-and-replay recovery takes over.
1074
+ * Readers default to DEFAULT_SESSION_COMPACTION_MAX_S. */
1075
+ session_compaction_max_s?: number;
1076
+ /** The central lens permit ceiling: the most lens sessions alive at
1077
+ * once across every worker. Readers default to DEFAULT_LENS_PERMITS (20,
1078
+ * ruled for the VM: the CPU gate measured 20 sessions fine on 2 cores). */
1079
+ lens_permits?: number;
1080
+ /** The least gap, in ms, between two permit grants, so a wave never creates
1081
+ * its sessions in one burst (the gate measured a 0.7 to 2.5 s loop stall for
1082
+ * 8 to 20 at once). 0 turns it off. Default DEFAULT_LENS_GRANT_STAGGER_MS (250). */
1083
+ lens_grant_stagger_ms?: number;
1084
+ /** The idle-PR answer worker's wall-clock cap, in seconds: the
1085
+ * operator's message to a PR with no running worker is answered by a short
1086
+ * worker that is ended at this age. Readers default to DEFAULT_ANSWER_TIMEOUT_S. */
1087
+ answer_timeout_s?: number;
1088
+ /** The answer worker's dollar cap per message (the session's own spend, in
1089
+ * USD). Readers default to DEFAULT_ANSWER_CAP_USD. */
1090
+ answer_cap_usd?: number;
1091
+ round_stage_supervisor_stale_s?: number;
1092
+ /** The daemon's event-loop watch (src/eventloop.ts). All
1093
+ * optional — `eventLoopPolicy` defaults each (300 s window, a 10 s single
1094
+ * delay or a 2 s p99 over a full window is a stall, one wake per 60 min,
1095
+ * "CPU starved" from 20% of CFS periods throttled) and clamps a number
1096
+ * into range; a non-number falls back to the default. */
1097
+ event_loop_window_s?: number;
1098
+ event_loop_stall_max_s?: number;
1099
+ event_loop_stall_p99_s?: number;
1100
+ event_loop_wake_cooldown_min?: number;
1101
+ event_loop_starved_throttle_ratio?: number;
1102
+ /** Zombie-round expiry horizon: a round stuck `overdue` for longer than
1103
+ * this — and whose PR has since verdicted or closed, so a self-requeue
1104
+ * would never touch it — moves to terminal `expired`.
1105
+ * 0 disables the sweep. */
1106
+ expire_overdue_after_h: number;
1107
+ };
1108
+ eod: {
1109
+ time: string;
1110
+ timezone: string;
1111
+ /** Daily overseer heartbeat wall time; same zone as `time`. */
1112
+ heartbeat_time: string;
1113
+ /** Watch the pinned area across the sweep and raise an anomaly if
1114
+ * curation touched it. Off ⇒ no watch. */
1115
+ pinned_watchdog: boolean;
1116
+ /** How long after the inject the watchdog keeps looking, when the day's
1117
+ * digest never appears to close the window early. */
1118
+ pinned_watchdog_window_min: number;
1119
+ };
1120
+ caps: {
1121
+ improvement_pr_max_lines: number;
1122
+ improvement_pr_max_files: number;
1123
+ improvement_pr_per_day: number;
1124
+ improvement_pr_per_week: number;
1125
+ issues_per_day: number;
1126
+ /** Path prefixes a bot improvement PR may never touch. A configured
1127
+ * value REPLACES the list (it is repo-shape, not an invariant —
1128
+ * `migrations/` is Django's name for the concept), and `.env`-named files
1129
+ * are refused unconditionally regardless of this knob. */
1130
+ improvement_pr_deny_prefixes: string[];
1131
+ };
1132
+ openviking: OpenVikingConfig;
1133
+ memory: MemoryConfig;
1134
+ tui: TuiConfig;
1135
+ companion: CompanionConfig;
1136
+ sink: SinkConfig;
1137
+ backfill: BackfillConfig;
1138
+ recheck: RecheckConfig;
1139
+ levers: LeversConfig;
1140
+ slack: SlackConfig;
1141
+ /** Where `qcr scan start` drives a run: `[scan] sandbox`,
1142
+ * `pihome_volume` and `image`, read from the same table and kept out of
1143
+ * the scan settings (a profile cannot set them). */
1144
+ scan_sandbox: ScanSandboxConfig;
1145
+ /** The closed-PR archiver (`src/archive/`). Optional in the file;
1146
+ * the parser fills every key. */
1147
+ archive: ArchiveConfig;
1148
+ /** The bot login's single refresher (`src/loginrefresh/`). Optional
1149
+ * in the file; off unless the cutover's config turns it on. */
1150
+ login_refresh: LoginRefreshConfig;
1151
+ }
1152
+
1153
+ /**
1154
+ * `[login_refresh]`: the VM-only exception to the login-refresh rule (packages/pi-plugin/src/engine/auth.ts). After
1155
+ * the cutover ONE refresher process, spawned and supervised by the daemon,
1156
+ * owns pihome/agent/auth.json and every worker borrows a refresh-blanked copy.
1157
+ * Off by default: while the bot's tmux pi sessions exist they refresh the file
1158
+ * themselves, and a second refresher would rotate the refresh token under them.
1159
+ */
1160
+ export interface LoginRefreshConfig {
1161
+ enabled: boolean;
1162
+ /** Refresh once the token has less than pi's own refresh window plus this
1163
+ * left. Must exceed `[daemon] round_deadline_min` (the longest worker cap), so
1164
+ * a worker that borrowed the file can finish on it. */
1165
+ margin_min: number;
1166
+ }
1167
+
1168
+ export const LOGIN_REFRESH_DEFAULTS: LoginRefreshConfig = Object.freeze({
1169
+ enabled: false,
1170
+ margin_min: 90,
1171
+ });
1172
+
1173
+ /** Parse `[login_refresh]`; with the refresher enabled, a margin that does not
1174
+ * outlast the longest worker cap REFUSES to load (a worker would then outlive
1175
+ * the token it borrowed). Disabled, nothing borrows, so the rule is not checked. */
1176
+ export function parseLoginRefresh(
1177
+ raw: Record<string, unknown>,
1178
+ roundDeadlineMin: number,
1179
+ ): LoginRefreshConfig {
1180
+ const enabled = raw.enabled ?? LOGIN_REFRESH_DEFAULTS.enabled;
1181
+ if (typeof enabled !== "boolean")
1182
+ throw new Error(`config.toml [login_refresh]: enabled must be true or false`);
1183
+ const m = raw.margin_min ?? LOGIN_REFRESH_DEFAULTS.margin_min;
1184
+ const margin = typeof m === "bigint" ? Number(m) : m;
1185
+ if (typeof margin !== "number" || !Number.isFinite(margin) || margin > 1440)
1186
+ throw new Error(
1187
+ `config.toml [login_refresh]: margin_min must be a number of minutes up to 1440 (got ${JSON.stringify(typeof m === "bigint" ? String(m) : m)})`,
1188
+ );
1189
+ if (enabled && margin <= roundDeadlineMin)
1190
+ throw new Error(
1191
+ `config.toml [login_refresh]: margin_min (${margin}) must exceed [daemon] round_deadline_min (${roundDeadlineMin}), the longest worker cap`,
1192
+ );
1193
+ return { enabled, margin_min: margin };
1194
+ }
1195
+
1196
+ /** Where a `qcr scan start` driver runs. `docker`: in a throwaway
1197
+ * container from the scan sandbox image that mounts only the reviewed repo
1198
+ * (read-only), the state home, the scan's own pi home and the config
1199
+ * (src/scanbox.ts). `host`: a detached process on this machine. */
1200
+ export const SCAN_SANDBOXES = ["docker", "host"] as const;
1201
+ export type ScanSandbox = (typeof SCAN_SANDBOXES)[number];
1202
+
1203
+ export interface ScanSandboxConfig {
1204
+ sandbox: ScanSandbox;
1205
+ /** Whether config.toml names `sandbox`: only the default gives way to
1206
+ * where the CLI runs (inside the companion container it is `host`). */
1207
+ sandbox_set: boolean;
1208
+ /** The pi home volume whose login the sandbox reads — read-only, a
1209
+ * refresh-disabled copy per case; by default the companion's own
1210
+ * (`[companion] pihome_volume`), which alone refreshes it. */
1211
+ pihome_volume: string;
1212
+ /** The image the sandbox runs; `SCAN_IMAGE_DEFAULT` when the key is omitted (an empty value is refused). */
1213
+ image: string;
1214
+ }
1215
+
1216
+ /** The scan sandbox's own image (docker/companion/Dockerfile, built by
1217
+ * `qcr scan rebuild`). Not the companion's: now that is the release
1218
+ * image in `companion` mode, which the sandbox's one-shot entrypoint does not
1219
+ * serve. */
1220
+ export const SCAN_IMAGE_DEFAULT = "qcr-scan";
1221
+
1222
+ export const SCAN_SANDBOX_DEFAULTS: Omit<ScanSandboxConfig, "image" | "pihome_volume"> = {
1223
+ sandbox: "docker",
1224
+ sandbox_set: false,
1225
+ };
1226
+
1227
+ /** `[scan] sandbox / pihome_volume / image`; the pi home volume defaults to
1228
+ * the companion's, the image to `SCAN_IMAGE_DEFAULT`. */
1229
+ export function parseScanSandbox(
1230
+ raw: unknown,
1231
+ companion: { pihome_volume: string },
1232
+ ): ScanSandboxConfig {
1233
+ const t = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
1234
+ const name = (key: string, fallback: string): string => {
1235
+ const v = t[key];
1236
+ if (v === undefined) return fallback;
1237
+ if (typeof v !== "string" || v.trim() === "")
1238
+ throw new Error(
1239
+ `config.toml [scan] ${key} must be a non-empty name (got ${JSON.stringify(v)})`,
1240
+ );
1241
+ return v.trim();
1242
+ };
1243
+ const sandbox = t["sandbox"] ?? SCAN_SANDBOX_DEFAULTS.sandbox;
1244
+ if (typeof sandbox !== "string" || !(SCAN_SANDBOXES as readonly string[]).includes(sandbox))
1245
+ throw new Error(
1246
+ `config.toml [scan] sandbox must be "docker" or "host" (got ${JSON.stringify(sandbox)})`,
1247
+ );
1248
+ return {
1249
+ sandbox: sandbox as ScanSandbox,
1250
+ sandbox_set: t["sandbox"] !== undefined,
1251
+ pihome_volume: name("pihome_volume", companion.pihome_volume),
1252
+ image: name("image", SCAN_IMAGE_DEFAULT),
1253
+ };
1254
+ }
1255
+
1256
+ /**
1257
+ * `[levers]`: the bounds on a SESSION-issued `drain` / `pause-pr`.
1258
+ * The operator's own levers (`qcr drain on`, the deploy tab) are not bounded
1259
+ * by anything here — the deploy flow's drain stands until the new boot's
1260
+ * first healthy reconcile lifts it. Every number is minutes; the budgets are
1261
+ * counted per `[ops] timezone` day.
1262
+ */
1263
+ export interface LeversConfig {
1264
+ /** Longest single session drain. */
1265
+ session_drain_max_min: number;
1266
+ /** Total session-drain minutes grantable per day; must be ≥ the cap. */
1267
+ session_drain_budget_min_per_day: number;
1268
+ /** Longest single session pause of one PR. */
1269
+ session_pause_max_min: number;
1270
+ /** Total session-pause minutes grantable per PR per day; must be ≥ the cap. */
1271
+ session_pause_budget_min_per_day: number;
1272
+ }
1273
+
1274
+ export const LEVERS_DEFAULTS: LeversConfig = {
1275
+ session_drain_max_min: 30,
1276
+ session_drain_budget_min_per_day: 90,
1277
+ session_pause_max_min: 240,
1278
+ session_pause_budget_min_per_day: 480,
1279
+ };
1280
+
1281
+ /** Parse `[levers]`; a bound that is not a positive integer, or a budget
1282
+ * smaller than its cap, refuses to load — a lever bound of zero or a budget
1283
+ * that can never fund one full grant is a misconfiguration, not a setting. */
1284
+ export function parseLevers(raw: Record<string, any>): LeversConfig {
1285
+ const cfg: LeversConfig = { ...LEVERS_DEFAULTS, ...(raw as Partial<LeversConfig>) };
1286
+ for (const key of Object.keys(LEVERS_DEFAULTS) as Array<keyof LeversConfig>) {
1287
+ const v = cfg[key];
1288
+ if (!Number.isInteger(v) || v < 1) {
1289
+ throw new Error(
1290
+ `config.toml [levers]: ${key} must be a positive integer number of minutes (got ${JSON.stringify(v)})`,
1291
+ );
1292
+ }
1293
+ }
1294
+ if (cfg.session_drain_budget_min_per_day < cfg.session_drain_max_min) {
1295
+ throw new Error(
1296
+ "config.toml [levers]: session_drain_budget_min_per_day must be at least session_drain_max_min",
1297
+ );
1298
+ }
1299
+ if (cfg.session_pause_budget_min_per_day < cfg.session_pause_max_min) {
1300
+ throw new Error(
1301
+ "config.toml [levers]: session_pause_budget_min_per_day must be at least session_pause_max_min",
1302
+ );
1303
+ }
1304
+ return cfg;
1305
+ }
1306
+
1307
+ /** The values `[archive] mode` takes (docs/archiving.md §12). */
1308
+ export const ARCHIVE_MODES = ["off", "plan", "on"] as const;
1309
+ export type ArchiveMode = (typeof ARCHIVE_MODES)[number];
1310
+
1311
+ /**
1312
+ * `[archive]` (docs/archiving.md): closed-PR history out of every
1313
+ * live path into a compressed archive under `<root>/archive/`, nothing of
1314
+ * analysis value deleted. Read by `src/archive/` (the archiver's policy) and
1315
+ * `qcr archive status`.
1316
+ */
1317
+ export interface ArchiveConfig {
1318
+ /**
1319
+ * `off` — the archiver arms no timer at all;
1320
+ * `plan` — the SHIPPED value: it computes what it would move (status.json
1321
+ * `archive`, `qcr status`, `qcr archive status --plan`) and
1322
+ * writes nothing;
1323
+ * `on` — it MOVES, every tick: it restores reopened PRs' transcripts
1324
+ * and payloads first; then the old reader-less event rows and
1325
+ * webhook deliveries into `archive/db/`, sealing finished months;
1326
+ * then each eligible closed PR's files into `archive/pr-<n>/`,
1327
+ * removing each from the live tree once its copy is verified;
1328
+ * then the overseer's non-current transcripts and pi's day-old
1329
+ * global session files.
1330
+ * Nothing is deleted that is not verified in the archive or a
1331
+ * proven duplicate.
1332
+ */
1333
+ mode: ArchiveMode;
1334
+ /** Closed PRs' files and webhook deliveries leave the live tree this many
1335
+ * days after the close (clamped 7..3650). */
1336
+ after_days: number;
1337
+ /** The reader-less event kinds leave this many days after their row was
1338
+ * written (clamped 31..3650, so `qcr metrics` d30 stays exact). */
1339
+ events_after_days: number;
1340
+ /** The archiver's own timer (clamped 10..3600 s). */
1341
+ interval_s: number;
1342
+ /** No tick before the daemon's first healthy reconcile plus this (clamped
1343
+ * 0..1440 min). */
1344
+ start_delay_min: number;
1345
+ /** No new file is started after this much of a tick (clamped 1..300 s). */
1346
+ tick_budget_s: number;
1347
+ /** At most this many PRs a tick (clamped 1..100). */
1348
+ max_prs_per_tick: number;
1349
+ /** Input rate cap for compress + verify (clamped 1..200 MB/s). */
1350
+ max_mb_per_s: number;
1351
+ /** Free space kept on both the archive and the state mount (clamped
1352
+ * 0.5..1024 GB). */
1353
+ disk_headroom_gb: number;
1354
+ /** The DB mover's starting batch (clamped 100..2000 rows). */
1355
+ db_batch_rows: number;
1356
+ /** gzip level (clamped 1..9). */
1357
+ gzip_level: number;
1358
+ /** A worker round's logs under `state/live/` and `state/lens/`
1359
+ * are removed from the live tree this many days after the round's last write,
1360
+ * and only once the PR's archive holds a verified copy (clamped 7..3650). A
1361
+ * log nothing archived is never removed. */
1362
+ live_retention_days: number;
1363
+ /** The disk cap on `state/live/` plus `state/lens/` together, in GB (clamped
1364
+ * 0..4096; 0 turns the check off). Past it the archiver raises the
1365
+ * `archive_live_cap` wake once per incident and every 6 h; it never deletes
1366
+ * to get under the cap. */
1367
+ live_cap_gb: number;
1368
+ }
1369
+
1370
+ export const ARCHIVE_DEFAULTS: Readonly<ArchiveConfig> = Object.freeze({
1371
+ mode: "plan",
1372
+ after_days: 14,
1373
+ events_after_days: 35,
1374
+ interval_s: 60,
1375
+ start_delay_min: 10,
1376
+ tick_budget_s: 20,
1377
+ max_prs_per_tick: 5,
1378
+ max_mb_per_s: 8,
1379
+ disk_headroom_gb: 2,
1380
+ db_batch_rows: 500,
1381
+ gzip_level: 6,
1382
+ live_retention_days: 30,
1383
+ live_cap_gb: 20,
1384
+ });
1385
+
1386
+ /** Each numeric `[archive]` key's range, and whether it is a whole number. */
1387
+ export const ARCHIVE_LIMITS: Readonly<
1388
+ Record<Exclude<keyof ArchiveConfig, "mode">, readonly [min: number, max: number, int: boolean]>
1389
+ > = Object.freeze({
1390
+ after_days: [7, 3650, true],
1391
+ events_after_days: [31, 3650, true],
1392
+ interval_s: [10, 3600, true],
1393
+ start_delay_min: [0, 1440, true],
1394
+ tick_budget_s: [1, 300, true],
1395
+ max_prs_per_tick: [1, 100, true],
1396
+ max_mb_per_s: [1, 200, false],
1397
+ disk_headroom_gb: [0.5, 1024, false],
1398
+ db_batch_rows: [100, 2000, true],
1399
+ gzip_level: [1, 9, true],
1400
+ live_retention_days: [7, 3650, true],
1401
+ live_cap_gb: [0, 4096, false],
1402
+ });
1403
+
1404
+ /**
1405
+ * Parse `[archive]`. An unknown `mode` REFUSES to load (a misspelt `"onn"`
1406
+ * must not quietly mean plan, or off), and so does a key that is not a
1407
+ * number; a number out of its range is CLAMPED into it (`ARCHIVE_LIMITS`).
1408
+ */
1409
+ export function parseArchive(raw: Record<string, unknown>): ArchiveConfig {
1410
+ const mode = raw.mode ?? ARCHIVE_DEFAULTS.mode;
1411
+ if (!(ARCHIVE_MODES as readonly unknown[]).includes(mode))
1412
+ throw new Error(
1413
+ `config.toml [archive]: mode must be one of ${ARCHIVE_MODES.map((v) => `"${v}"`).join(", ")} (got ${JSON.stringify(mode)})`,
1414
+ );
1415
+ const out: ArchiveConfig = { ...ARCHIVE_DEFAULTS, mode: mode as ArchiveMode };
1416
+ for (const [key, [min, max, int]] of Object.entries(ARCHIVE_LIMITS) as Array<
1417
+ [Exclude<keyof ArchiveConfig, "mode">, readonly [number, number, boolean]]
1418
+ >) {
1419
+ const v = raw[key];
1420
+ if (v === undefined) continue;
1421
+ const n = typeof v === "bigint" ? Number(v) : v;
1422
+ if (typeof n !== "number" || !Number.isFinite(n))
1423
+ throw new Error(
1424
+ `config.toml [archive]: ${key} must be a number (got ${JSON.stringify(typeof v === "bigint" ? String(v) : v)})`,
1425
+ );
1426
+ out[key] = Math.min(max, Math.max(min, int ? Math.floor(n) : n));
1427
+ }
1428
+ return out;
1429
+ }
1430
+
1431
+ /** The team slug a configured `org/team` review team is requested as — the
1432
+ * one rule the webhook's trigger and withdrawal, the trigger consumption and
1433
+ * the backfill all match on. A bare slug is its own slug. */
1434
+ export function reviewTeamSlug(reviewTeam: string): string {
1435
+ return reviewTeam.split("/")[1] ?? reviewTeam;
1436
+ }
1437
+
1438
+ /** Defaults for [backfill], derived from the already-parsed [github] block. */
1439
+ export function backfillDefaults(github: GithubConfig): BackfillConfig {
1440
+ return {
1441
+ bot_login: github.bot_login,
1442
+ bot_actor: github.app_slug,
1443
+ reviewed_label: github.reviewed_label,
1444
+ trigger_label: github.trigger_label,
1445
+ in_progress_label: github.in_progress_label,
1446
+ review_team_slug: reviewTeamSlug(github.review_team),
1447
+ concurrency: 2,
1448
+ max_search_pages: 10,
1449
+ };
1450
+ }
1451
+
1452
+ /** [tui] is optional in config.toml — the TUI is an operator convenience and
1453
+ * older configs must keep parsing. */
1454
+ export const TUI_DEFAULTS: TuiConfig = {
1455
+ target: "ssh",
1456
+ ssh_host: "my-vm",
1457
+ container: "quickreview-sandbox",
1458
+ memory_container: "",
1459
+ remote_state_dir: "/opt/quickreview/state",
1460
+ remote_sandbox_root: "",
1461
+ sudo_docker: true,
1462
+ poll_interval_s: 3,
1463
+ stale_after_s: 90,
1464
+ msg_timeout_s: 30,
1465
+ evidence_timeout_s: 60,
1466
+ evidence_why_interval_s: 10,
1467
+ };
1468
+
1469
+ /** The local companion image's tag: the release image (docker/Dockerfile) in
1470
+ * `companion` mode, built on the host. */
1471
+ export const COMPANION_IMAGE = "qcr-companion:release";
1472
+
1473
+ /** [companion] is optional in config.toml, exactly like [tui]. */
1474
+ export const COMPANION_DEFAULTS: CompanionConfig = {
1475
+ enabled: true,
1476
+ container_name: "qcr-companion",
1477
+ // The release image, locally built (`qcr companion rebuild`). A tag of its own:
1478
+ // the name `qcr-companion` is the retired companion image on an existing
1479
+ // PC, which the new container's entrypoint modes do not serve, so a
1480
+ // recreate must find no image here and build the right one.
1481
+ image: COMPANION_IMAGE,
1482
+ pihome_volume: "companion-pihome",
1483
+ session: "companion",
1484
+ model: "openai-codex/gpt-6.1-sol:medium",
1485
+ repo_dir: "", // → sandboxRoot()
1486
+ tracker_dir: "", // → /tracker (rw) when set; empty = no tracker mount
1487
+ tracker_docs_dir: "", // → /tracker-docs (ro) when set
1488
+ reviewed_repo_dir: "", // → /reviewed-repo (ro) when set and present
1489
+ evidence_dir: "~/.local/share/qcr/evidence", // → /evidence (ro)
1490
+ feed_dir: "", // → <root>/state/companion/feed
1491
+ alerts_dir: "", // → <root>/state/companion/alerts
1492
+ ssh_dir: "~/.ssh",
1493
+ build_context: "", // → <root> (the release image is built from it; Dockerfile at docker/Dockerfile)
1494
+ overdue_grace_min: 30,
1495
+ stale_alert_s: 300,
1496
+ awaiting_age_h: 24,
1497
+ cooldown_min: 20,
1498
+ };
1499
+
1500
+ /**
1501
+ * The issue sink — where auto-filed defect flags (`#bug` memory entries)
1502
+ * land. Three kinds:
1503
+ *
1504
+ * `wayfinder` — tickets written into a filesystem wayfinder tracker on the
1505
+ * operator's PC, into the effort and under the parent this
1506
+ * block names.
1507
+ * `github` — defects filed as issues on the reviewed repo. The TUI never
1508
+ * talks to GitHub: it writes a `file-issue` COMMAND into the
1509
+ * daemon's commands/ channel, so the daemon's MutationGate,
1510
+ * day caps and title dedup enforce the filing.
1511
+ * `none` — auto-filing off. The default when `[sink]` is absent:
1512
+ * unconfigured = off.
1513
+ */
1514
+ export type IssueSinkKind = "wayfinder" | "github" | "none";
1515
+
1516
+ export interface SinkConfig {
1517
+ kind: IssueSinkKind;
1518
+ /** Wayfinder tracker root (also the ticket projection's root). Empty ⇒
1519
+ * defaults from `[companion] tracker_dir`. */
1520
+ root: string;
1521
+ /** Wayfinder: the effort auto-filed tickets are written into. */
1522
+ effort: string;
1523
+ /** Wayfinder: the `Parent:` map id stamped into each filed ticket. */
1524
+ parent: string;
1525
+ /** The TUI's read-only ticket projection's effort. Deliberately a
1526
+ * SEPARATE key: the board may read a different effort from the one the
1527
+ * filer writes into. Empty ⇒ same as `effort`. */
1528
+ tickets_effort: string;
1529
+ }
1530
+
1531
+ export const SINK_DEFAULTS: SinkConfig = {
1532
+ kind: "none",
1533
+ root: "",
1534
+ effort: "",
1535
+ parent: "",
1536
+ tickets_effort: "",
1537
+ };
1538
+
1539
+ /** [sink] parse + validation. Misconfig is loud at boot, not a quiet no-op:
1540
+ * a `wayfinder` sink missing its effort or parent would otherwise silently
1541
+ * file nothing (or file into a nameless effort) forever. */
1542
+ function parseSink(raw: Record<string, any>, companion: CompanionConfig): SinkConfig {
1543
+ const sink: SinkConfig = { ...SINK_DEFAULTS, ...(raw as Partial<SinkConfig>) };
1544
+ if (!["wayfinder", "github", "none"].includes(sink.kind)) {
1545
+ throw new Error(
1546
+ `config.toml [sink]: kind must be "wayfinder", "github" or "none" (got ${JSON.stringify(sink.kind)})`,
1547
+ );
1548
+ }
1549
+ sink.root = sink.root ? expandHome(sink.root) : companion.tracker_dir;
1550
+ if (sink.kind === "wayfinder") {
1551
+ if (!sink.root.trim())
1552
+ throw new Error(
1553
+ 'config.toml [sink]: kind = "wayfinder" requires a `root` (or a [companion] tracker_dir to default from)',
1554
+ );
1555
+ if (!sink.effort.trim())
1556
+ throw new Error('config.toml [sink]: kind = "wayfinder" requires a non-empty `effort`');
1557
+ if (!sink.parent.trim())
1558
+ throw new Error('config.toml [sink]: kind = "wayfinder" requires a non-empty `parent`');
1559
+ }
1560
+ return sink;
1561
+ }
1562
+
1563
+ /** The ticket projection's effort — its own key when set, else the filer's.
1564
+ * Empty ⇒ the projection has nothing configured to read. */
1565
+ export function sinkTicketsEffort(cfg: Config): string {
1566
+ return cfg.sink.tickets_effort.trim() || cfg.sink.effort.trim();
1567
+ }
1568
+
1569
+ /** Default lost-prompt grace when [daemon] carries no
1570
+ * `round_stall_grace_min`: long enough that a slow first tool call is never
1571
+ * mistaken for a dropped prompt, far shorter than the round deadline. */
1572
+ export const DEFAULT_ROUND_STALL_GRACE_MIN = 10;
1573
+
1574
+ /** Default self-requeue budget per PR head when [daemon] carries no
1575
+ * `max_self_requeues`. */
1576
+ export const DEFAULT_MAX_SELF_REQUEUES = 2;
1577
+
1578
+ /** Default prompt-redelivery budget per round. Two is enough to cover
1579
+ * a delivery that raced a busy session and one that raced a session restart;
1580
+ * a third failure is no longer a delivery problem, so escalating beats
1581
+ * re-sending the same file forever. */
1582
+ export const DEFAULT_MAX_PROMPT_REDELIVERIES = 2;
1583
+
1584
+ /** Finite daemon→pi stage bounds. The 45-minute hard run cap is above the
1585
+ * measured live maximum (~23 minutes) yet materially below the 75-minute
1586
+ * defense-in-depth round deadline. */
1587
+ export const DEFAULT_SESSION_COMMAND_START_LEASE_S = 120;
1588
+ export const DEFAULT_SESSION_BRIDGE_READY_TIMEOUT_S = 30;
1589
+ export const DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN = 45;
1590
+ export const DEFAULT_ROUND_FIRST_PASS_MAX_MIN = 5;
1591
+ export const DEFAULT_ROUND_PASSES_MAX_MIN = 35;
1592
+ export const DEFAULT_ROUND_FINALIZE_GRACE_MIN = 5;
1593
+ /** Measured posting→verdict tails run to ~6.5 min; 15 is over twice that and
1594
+ * still well inside the 75-minute round deadline. */
1595
+ export const DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN = 15;
1596
+ /** `[daemon] follow_up_sweep_extra_min`'s default. */
1597
+ export const DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN = 20;
1598
+ /** The round stage bounds in minutes, each with its code default: the one
1599
+ * resolution status publication and the stage supervisor both use. */
1600
+ export function resolveRoundStageBounds(daemon: Config["daemon"]): {
1601
+ first_pass_max_min: number;
1602
+ passes_max_min: number;
1603
+ running_max_min: number;
1604
+ follow_up_sweep_extra_min: number;
1605
+ } {
1606
+ return {
1607
+ first_pass_max_min: daemon.round_first_pass_max_min ?? DEFAULT_ROUND_FIRST_PASS_MAX_MIN,
1608
+ passes_max_min: daemon.round_passes_max_min ?? DEFAULT_ROUND_PASSES_MAX_MIN,
1609
+ running_max_min:
1610
+ daemon.session_command_running_max_min ?? DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN,
1611
+ follow_up_sweep_extra_min:
1612
+ daemon.follow_up_sweep_extra_min ?? DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN,
1613
+ };
1614
+ }
1615
+ export const DEFAULT_POSTING_RECEIPT_GRACE_S = 120;
1616
+ /** Three retries, exponential,
1617
+ * all inside an hour of the first failure. */
1618
+ export const DEFAULT_POSTING_RETRY_OFFSETS_MIN: readonly number[] = [4, 12, 36];
1619
+ export const DEFAULT_POSTING_RETRY_WINDOW_MIN = 60;
1620
+ export const DEFAULT_SESSION_COMMAND_UNKNOWN_GRACE_S = 60;
1621
+ export const DEFAULT_SESSION_COMMAND_UNKNOWN_MAX_CHECKS = 1;
1622
+ export const DEFAULT_SESSION_COMMAND_MAX_START_RETRIES = 2;
1623
+ /** A post-verdict compaction can take over a minute with the container
1624
+ * CPU-starved; ten minutes is several times the worst seen and still well
1625
+ * inside the 75-minute round deadline. */
1626
+ export const DEFAULT_SESSION_COMPACTION_MAX_S = 600;
1627
+ /** The idle-PR answer worker: ten minutes of wall clock, two dollars. */
1628
+ export const DEFAULT_ANSWER_TIMEOUT_S = 600;
1629
+ export const DEFAULT_ANSWER_CAP_USD = 2;
1630
+ export const DEFAULT_ROUND_STAGE_SUPERVISOR_STALE_S = 180;
1631
+
1632
+ /** The app's root: the checkout (or image tree) this module runs from. The
1633
+ * review core's shipped data is found through `assetRoot` (packages/pi-plugin/src/assetroot.ts). */
1634
+ export function sandboxRoot(): string {
1635
+ return packageRootFrom(dirname(fileURLToPath(import.meta.url)), "sandbox root");
1636
+ }
1637
+
1638
+ export function expandHome(p: string): string {
1639
+ return p.startsWith("~/") || p === "~" ? resolve(homedir(), p.slice(2)) : p;
1640
+ }
1641
+
1642
+ /** Host paths for every companion mount: a set path gets its `~` expanded (a relative one stays relative); "" means derive from root. */
1643
+ export function resolveCompanionPaths(
1644
+ c: CompanionConfig,
1645
+ root: string = sandboxRoot(),
1646
+ ): CompanionConfig {
1647
+ return {
1648
+ ...c,
1649
+ repo_dir: c.repo_dir ? expandHome(c.repo_dir) : root,
1650
+ tracker_dir: c.tracker_dir ? expandHome(c.tracker_dir) : "",
1651
+ tracker_docs_dir: c.tracker_docs_dir ? expandHome(c.tracker_docs_dir) : "",
1652
+ reviewed_repo_dir: expandHome(c.reviewed_repo_dir),
1653
+ evidence_dir: c.evidence_dir ? expandHome(c.evidence_dir) : resolve(root, "evidence"),
1654
+ feed_dir: c.feed_dir ? expandHome(c.feed_dir) : resolve(root, "state", "companion", "feed"),
1655
+ alerts_dir: c.alerts_dir
1656
+ ? expandHome(c.alerts_dir)
1657
+ : resolve(root, "state", "companion", "alerts"),
1658
+ ssh_dir: expandHome(c.ssh_dir),
1659
+ build_context: c.build_context ? expandHome(c.build_context) : root,
1660
+ };
1661
+ }
1662
+
1663
+ /**
1664
+ * `[caps]` parsed with each numeric cap CHECKED, not cast: a key missing
1665
+ * from an operator's file must never reach `checkDiffstat` as `undefined`,
1666
+ * where `files > undefined` is `false` and the cap would pass. A cap the
1667
+ * daemon cannot compare against is a startup refusal: the improvement
1668
+ * channel is the one place this system pushes code, and it must never run uncapped.
1669
+ */
1670
+ export function parseCaps(raw: Record<string, unknown>): Config["caps"] {
1671
+ const cap = (key: string): number => {
1672
+ const v = raw[key];
1673
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
1674
+ throw new Error(
1675
+ `config.toml [caps]: ${key} must be a non-negative number (got ${JSON.stringify(v)})`,
1676
+ );
1677
+ }
1678
+ return v;
1679
+ };
1680
+ const isStrings = (v: unknown): v is string[] =>
1681
+ Array.isArray(v) && v.every((x) => typeof x === "string");
1682
+ const denyPrefixes = (): string[] => {
1683
+ const prefixes = raw.improvement_pr_deny_prefixes;
1684
+ if (prefixes === undefined) return [...IMPROVEMENT_DENY_PREFIXES_DEFAULT];
1685
+ if (isStrings(prefixes)) return [...prefixes];
1686
+ throw new Error(
1687
+ `config.toml [caps]: improvement_pr_deny_prefixes must be an array of strings (got ${JSON.stringify(prefixes)})`,
1688
+ );
1689
+ };
1690
+ return {
1691
+ improvement_pr_max_lines: cap("improvement_pr_max_lines"),
1692
+ improvement_pr_max_files: cap("improvement_pr_max_files"),
1693
+ improvement_pr_per_day: cap("improvement_pr_per_day"),
1694
+ improvement_pr_per_week: cap("improvement_pr_per_week"),
1695
+ issues_per_day: cap("issues_per_day"),
1696
+ improvement_pr_deny_prefixes: denyPrefixes(),
1697
+ };
1698
+ }
1699
+
1700
+ /** The shipped improvement-PR path denylist. Lives here (not in
1701
+ * improvements.ts) so config carries the default without a runtime import cycle. */
1702
+ export const IMPROVEMENT_DENY_PREFIXES_DEFAULT: readonly string[] = [
1703
+ "migrations/",
1704
+ "infra/",
1705
+ "ansible/",
1706
+ ".github/workflows/",
1707
+ ];
1708
+
1709
+ export function parseConfig(toml: string): Config {
1710
+ const raw = parse(toml) as Record<string, any>;
1711
+ for (const line of configDeprecations(raw)) console.warn(`[quickreviewd] ${line}`);
1712
+ const github = parseGithubSection(raw);
1713
+ const preset = presetOf(raw);
1714
+ const engine = parseEngine((raw.engine ?? {}) as Record<string, unknown>);
1715
+ const companion = resolveCompanionPaths({
1716
+ ...COMPANION_DEFAULTS,
1717
+ ...((raw.companion ?? {}) as Partial<CompanionConfig>),
1718
+ });
1719
+ // [ops] is optional so older configs keep parsing; [eod] and
1720
+ // [slack.policy] timezones default from it below, own-section value wins.
1721
+ const ops: OpsConfig = {
1722
+ timezone: assertIanaTimeZone(
1723
+ String(((raw.ops ?? {}) as Partial<OpsConfig>).timezone ?? OPS_DEFAULTS.timezone),
1724
+ "[ops] timezone",
1725
+ ),
1726
+ };
1727
+ // [ingress] is optional: absent ⇒ shape "none", nothing starts.
1728
+ const ingress: IngressConfig = {
1729
+ ...INGRESS_DEFAULTS,
1730
+ ...((raw.ingress ?? {}) as Partial<IngressConfig>),
1731
+ };
1732
+ if (!["none", "tunnel-file"].includes(ingress.shape)) {
1733
+ throw new Error(
1734
+ `config.toml [ingress]: shape must be "none" or "tunnel-file" (got ${JSON.stringify(ingress.shape)})`,
1735
+ );
1736
+ }
1737
+ const cfg: Config = {
1738
+ dry_run: Boolean(req(raw, "dry_run")),
1739
+ ops,
1740
+ ingress,
1741
+ github,
1742
+ server: { ...(req(raw, "server") as Config["server"]) },
1743
+ // Every optional knob defaults so older configs keep parsing.
1744
+ daemon: {
1745
+ max_self_requeues: DEFAULT_MAX_SELF_REQUEUES,
1746
+ expire_overdue_after_h: 24,
1747
+ session_command_start_lease_s: DEFAULT_SESSION_COMMAND_START_LEASE_S,
1748
+ session_bridge_ready_timeout_s: DEFAULT_SESSION_BRIDGE_READY_TIMEOUT_S,
1749
+ session_command_running_max_min: DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN,
1750
+ round_first_pass_max_min: DEFAULT_ROUND_FIRST_PASS_MAX_MIN,
1751
+ round_passes_max_min: DEFAULT_ROUND_PASSES_MAX_MIN,
1752
+ round_finalize_grace_min: DEFAULT_ROUND_FINALIZE_GRACE_MIN,
1753
+ round_posted_tail_grace_min: DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN,
1754
+ follow_up_sweep_extra_min: DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN,
1755
+ posting_receipt_grace_s: DEFAULT_POSTING_RECEIPT_GRACE_S,
1756
+ posting_retry_offsets_min: [...DEFAULT_POSTING_RETRY_OFFSETS_MIN],
1757
+ posting_retry_window_min: DEFAULT_POSTING_RETRY_WINDOW_MIN,
1758
+ session_command_unknown_grace_s: DEFAULT_SESSION_COMMAND_UNKNOWN_GRACE_S,
1759
+ session_command_unknown_max_checks: DEFAULT_SESSION_COMMAND_UNKNOWN_MAX_CHECKS,
1760
+ session_command_max_start_retries: DEFAULT_SESSION_COMMAND_MAX_START_RETRIES,
1761
+ session_compaction_max_s: DEFAULT_SESSION_COMPACTION_MAX_S,
1762
+ lens_permits: DEFAULT_LENS_PERMITS,
1763
+ lens_grant_stagger_ms: DEFAULT_LENS_GRANT_STAGGER_MS,
1764
+ answer_timeout_s: DEFAULT_ANSWER_TIMEOUT_S,
1765
+ answer_cap_usd: DEFAULT_ANSWER_CAP_USD,
1766
+ round_stage_supervisor_stale_s: DEFAULT_ROUND_STAGE_SUPERVISOR_STALE_S,
1767
+ ...(req(raw, "daemon") as Partial<Config["daemon"]>),
1768
+ } as Config["daemon"],
1769
+ // heartbeat_time defaults so older configs keep parsing; the timezone
1770
+ // defaults from [ops], own [eod] value wins — and is checked as [ops] is.
1771
+ eod: checkSectionTimeZone(
1772
+ {
1773
+ heartbeat_time: "09:00",
1774
+ pinned_watchdog: true,
1775
+ pinned_watchdog_window_min: EOD_PINNED_WATCHDOG_WINDOW_MIN,
1776
+ timezone: ops.timezone,
1777
+ ...(req(raw, "eod") as Partial<Config["eod"]>),
1778
+ } as Config["eod"],
1779
+ "[eod] timezone",
1780
+ ),
1781
+ caps: parseCaps(req(raw, "caps") as Record<string, unknown>),
1782
+ models: parseModels(req(raw, "models") as Record<string, any>, REVIEW_PRESETS[preset].effort),
1783
+ engine,
1784
+ pi: parsePiSection(raw),
1785
+ openviking: parseOpenViking((raw.openviking ?? {}) as Record<string, unknown>),
1786
+ memory: parseMemory((raw.memory ?? {}) as Record<string, unknown>),
1787
+ tui: { ...TUI_DEFAULTS, ...((raw.tui ?? {}) as Partial<TuiConfig>) },
1788
+ companion,
1789
+ sink: parseSink((raw.sink ?? {}) as Record<string, any>, companion),
1790
+ backfill: { ...backfillDefaults(github), ...((raw.backfill ?? {}) as Partial<BackfillConfig>) },
1791
+ guards: parseGuardsSection(raw),
1792
+ recheck: { ...RECHECK_DEFAULTS, ...((raw.recheck ?? {}) as Partial<RecheckConfig>) },
1793
+ review: parseReviewSection(raw, preset),
1794
+ escalation: parseEscalation((raw.escalation ?? {}) as Record<string, any>),
1795
+ levers: parseLevers((raw.levers ?? {}) as Record<string, any>),
1796
+ slack: parseSlack((raw.slack ?? {}) as Record<string, any>, ops.timezone),
1797
+ owner: parseOwnerSection(raw),
1798
+ scan: parseScan(raw.scan),
1799
+ scan_sandbox: parseScanSandbox(raw.scan, companion),
1800
+ archive: parseArchive((raw.archive ?? {}) as Record<string, unknown>),
1801
+ login_refresh: parseLoginRefresh(
1802
+ (raw.login_refresh ?? {}) as Record<string, unknown>,
1803
+ Number((req(raw, "daemon") as { round_deadline_min?: number }).round_deadline_min ?? 75),
1804
+ ),
1805
+ };
1806
+ validateReviewSection(cfg);
1807
+ foldLegacyOwnerSlackId(raw, cfg.owner);
1808
+ requireGithubKeys(cfg.github);
1809
+ // Every secrets reader in the daemon reads THIS field, so the
1810
+ // install locator decides it here, once, for all of them.
1811
+ cfg.github.secrets_dir = locateSecrets({
1812
+ configured: typeof cfg.github.secrets_dir === "string" ? cfg.github.secrets_dir : "",
1813
+ }).path;
1814
+ if (cfg.tui.target !== "ssh" && cfg.tui.target !== "local") {
1815
+ throw new Error(
1816
+ `config.toml [tui]: target must be "ssh" or "local" (got ${JSON.stringify(cfg.tui.target)})`,
1817
+ );
1818
+ }
1819
+ const firstPass = cfg.daemon.round_first_pass_max_min ?? DEFAULT_ROUND_FIRST_PASS_MAX_MIN;
1820
+ const passes = cfg.daemon.round_passes_max_min ?? DEFAULT_ROUND_PASSES_MAX_MIN;
1821
+ const running =
1822
+ cfg.daemon.session_command_running_max_min ?? DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN;
1823
+ const finalize = cfg.daemon.round_finalize_grace_min ?? DEFAULT_ROUND_FINALIZE_GRACE_MIN;
1824
+ const stageBounds = [firstPass, passes, running, finalize] as unknown[];
1825
+ if (!(
1826
+ stageBounds.every((v) => typeof v === "number" && Number.isFinite(v)) &&
1827
+ firstPass > 0 &&
1828
+ firstPass < passes &&
1829
+ passes < running &&
1830
+ finalize > 0 &&
1831
+ finalize < running
1832
+ )) {
1833
+ throw new Error(
1834
+ "config.toml: require finite numbers 0 < round_first_pass_max_min < round_passes_max_min < session_command_running_max_min and 0 < round_finalize_grace_min < session_command_running_max_min",
1835
+ );
1836
+ }
1837
+ // The two second-scale daemon→pi bounds become command deadlines
1838
+ // (`toISOString`), which a non-finite value would throw at the first command.
1839
+ for (const key of ["session_command_start_lease_s", "session_bridge_ready_timeout_s"] as const) {
1840
+ const v: unknown = cfg.daemon[key];
1841
+ if (!(typeof v === "number" && Number.isFinite(v) && v > 0))
1842
+ throw new Error(`config.toml: require a finite number [daemon] ${key} > 0`);
1843
+ }
1844
+ const postedTail = cfg.daemon.round_posted_tail_grace_min ?? DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN;
1845
+ if (!(typeof postedTail === "number" && Number.isFinite(postedTail) && postedTail > 0)) {
1846
+ throw new Error("config.toml: require round_posted_tail_grace_min > 0");
1847
+ }
1848
+ const sweepExtra = cfg.daemon.follow_up_sweep_extra_min ?? DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN;
1849
+ if (!(typeof sweepExtra === "number" && Number.isFinite(sweepExtra) && sweepExtra >= 0)) {
1850
+ throw new Error("config.toml: require follow_up_sweep_extra_min >= 0");
1851
+ }
1852
+ const retryWindow = cfg.daemon.posting_retry_window_min ?? DEFAULT_POSTING_RETRY_WINDOW_MIN;
1853
+ const retryOffsets = cfg.daemon.posting_retry_offsets_min ?? DEFAULT_POSTING_RETRY_OFFSETS_MIN;
1854
+ if (
1855
+ !(typeof retryWindow === "number" && Number.isFinite(retryWindow) && retryWindow > 0) ||
1856
+ !Array.isArray(retryOffsets) ||
1857
+ retryOffsets.length === 0 ||
1858
+ !retryOffsets.every(
1859
+ (m, i) =>
1860
+ typeof m === "number" &&
1861
+ Number.isFinite(m) &&
1862
+ m > 0 &&
1863
+ m < retryWindow &&
1864
+ (i === 0 || m > (retryOffsets[i - 1] as number)),
1865
+ )
1866
+ ) {
1867
+ throw new Error(
1868
+ "config.toml: require posting_retry_offsets_min = a non-empty, strictly increasing list of minutes, each > 0 and < posting_retry_window_min (> 0)",
1869
+ );
1870
+ }
1871
+ const lensPermits = cfg.daemon.lens_permits ?? DEFAULT_LENS_PERMITS;
1872
+ if (!(Number.isSafeInteger(lensPermits) && lensPermits >= 1)) {
1873
+ throw new Error("config.toml: require an integer [daemon] lens_permits >= 1");
1874
+ }
1875
+ const lensStagger = cfg.daemon.lens_grant_stagger_ms ?? DEFAULT_LENS_GRANT_STAGGER_MS;
1876
+ if (!(typeof lensStagger === "number" && Number.isFinite(lensStagger) && lensStagger >= 0)) {
1877
+ throw new Error("config.toml: require a finite number [daemon] lens_grant_stagger_ms >= 0");
1878
+ }
1879
+ const answerTimeout = cfg.daemon.answer_timeout_s ?? DEFAULT_ANSWER_TIMEOUT_S;
1880
+ if (!(typeof answerTimeout === "number" && Number.isFinite(answerTimeout) && answerTimeout > 0)) {
1881
+ throw new Error("config.toml: require a finite number [daemon] answer_timeout_s > 0");
1882
+ }
1883
+ const answerCap = cfg.daemon.answer_cap_usd ?? DEFAULT_ANSWER_CAP_USD;
1884
+ if (!(typeof answerCap === "number" && Number.isFinite(answerCap) && answerCap > 0)) {
1885
+ throw new Error("config.toml: require a finite number [daemon] answer_cap_usd > 0");
1886
+ }
1887
+ const compactionMax = cfg.daemon.session_compaction_max_s ?? DEFAULT_SESSION_COMPACTION_MAX_S;
1888
+ if (!(typeof compactionMax === "number" && Number.isFinite(compactionMax) && compactionMax > 0)) {
1889
+ throw new Error("config.toml: require session_compaction_max_s > 0");
1890
+ }
1891
+ // Shape checks are pure, so they fire on every parse (tests included);
1892
+ // the token-file check is I/O and lives in validateSlackConfig.
1893
+ checkSlackShape(cfg.slack, cfg.owner);
1894
+ delete (cfg.daemon as Record<string, unknown>)["overseer_rotation_days"];
1895
+ return cfg;
1896
+ }
1897
+
1898
+ function parseSlack(raw: Record<string, any>, defaultTimezone: string): SlackConfig {
1899
+ // Inbound turns went with the overseer: nothing routes a Slack message to a
1900
+ // model any more. A config that still asks for it is refused, not ignored,
1901
+ // so nobody believes the bot is listening.
1902
+ if (raw.inbound !== undefined && raw.inbound !== false) {
1903
+ throw new Error(
1904
+ "config.toml [slack]: `inbound` is retired — Slack inbound turns went with the overseer " +
1905
+ "(the router refuses them: dropped_no_overseer); outbound announcements are unchanged. Remove the key.",
1906
+ );
1907
+ }
1908
+ const rawPolicy = (raw.policy ?? {}) as Record<string, any>;
1909
+ // A cause with no cooldown of its own takes the configured default — the
1910
+ // same one an unlisted cause gets (`causePolicy`).
1911
+ const defaultCooldown =
1912
+ rawPolicy.default_cooldown_min ?? SLACK_POLICY_DEFAULTS.default_cooldown_min;
1913
+ const causes: Record<string, SlackCausePolicy> = { ...SLACK_CAUSE_DEFAULTS };
1914
+ for (const [cause, v] of Object.entries(
1915
+ (rawPolicy.causes ?? {}) as Record<string, Partial<SlackCausePolicy>>,
1916
+ )) {
1917
+ causes[cause] = {
1918
+ enabled: v.enabled ?? causes[cause]?.enabled ?? true,
1919
+ cooldown_min: Number(v.cooldown_min ?? causes[cause]?.cooldown_min ?? defaultCooldown),
1920
+ };
1921
+ }
1922
+ const { causes: _drop, ...policyScalars } = rawPolicy;
1923
+ // `legacy_owner_slack_id` is the legacy alias for [owner] slack_id —
1924
+ // consumed by parseConfig's fold, never carried on the parsed shape.
1925
+ const { legacy_owner_slack_id: _legacyOwnerId, inbound: _retiredInbound, ...rawScalars } = raw;
1926
+ const slack: SlackConfig = {
1927
+ ...SLACK_DEFAULTS,
1928
+ ...(rawScalars as Partial<SlackConfig>),
1929
+ users: { ...((raw.users ?? {}) as Record<string, string>) },
1930
+ // The day boundary defaults from [ops]; an explicit
1931
+ // [slack.policy] timezone still wins.
1932
+ policy: {
1933
+ ...SLACK_POLICY_DEFAULTS,
1934
+ timezone: defaultTimezone,
1935
+ ...(policyScalars as Partial<SlackPolicyConfig>),
1936
+ causes,
1937
+ },
1938
+ };
1939
+ // An explicit zone is checked as [ops]'s is — when the concierge is on;
1940
+ // a switched-off section is a draft, and nothing reads its clock.
1941
+ if (slack.enabled) checkSectionTimeZone(slack.policy, "[slack.policy] timezone");
1942
+ // Token *files* are names under secrets_dir, but an absolute or ~-rooted
1943
+ // override must still work — expand here so every reader sees one shape.
1944
+ slack.bot_token_file = expandHome(slack.bot_token_file);
1945
+ slack.app_token_file = expandHome(slack.app_token_file);
1946
+ return slack;
1947
+ }
1948
+
1949
+ /** Absolute path of a Slack token file: a bare name resolves under
1950
+ * [github].secrets_dir, an absolute/~ path is taken as given. */
1951
+ export function slackTokenPath(cfg: Config, which: "bot" | "app"): string {
1952
+ const name = which === "bot" ? cfg.slack.bot_token_file : cfg.slack.app_token_file;
1953
+ return isAbsolute(name) ? name : join(cfg.github.secrets_dir, name);
1954
+ }
1955
+
1956
+ /** Pure (I/O-free) part of Slack boot validation. When disabled, nothing at
1957
+ * all is required — the concierge is simply not there. When enabled, the
1958
+ * owner's Slack id (`[owner] slack_id`, legacy alias
1959
+ * `[slack] legacy_owner_slack_id`) must be set and be a key of [slack.users]. */
1960
+ function checkSlackShape(slack: SlackConfig, owner: OwnerConfig): void {
1961
+ if (!slack.enabled) return;
1962
+ const fail = (msg: string): never => {
1963
+ throw new Error(`config.toml [slack]: ${msg}`);
1964
+ };
1965
+ if (!slack.channel.trim()) fail("enabled = true requires a non-empty `channel`");
1966
+ if (slack.channel_kind !== "public" && slack.channel_kind !== "private") {
1967
+ fail(`channel_kind must be "public" or "private" (got ${JSON.stringify(slack.channel_kind)})`);
1968
+ }
1969
+ if (!owner.slack_id.trim()) fail("enabled = true requires a non-empty `[owner] slack_id`");
1970
+ if (!(owner.slack_id in slack.users)) {
1971
+ fail(
1972
+ `[owner] slack_id '${owner.slack_id}' must appear as a key in [slack.users] (it is the only source of own-PR rights)`,
1973
+ );
1974
+ }
1975
+ for (const key of [
1976
+ "daily_turns",
1977
+ "per_user_daily_turns",
1978
+ "thread_replies_per_day",
1979
+ "announcements_per_hour",
1980
+ ] as const) {
1981
+ const cap = slack.policy[key];
1982
+ if (typeof cap !== "number" || !Number.isFinite(cap) || cap < 0)
1983
+ fail(`policy.${key} must be a finite non-negative number`);
1984
+ }
1985
+ }
1986
+
1987
+ /** How validation reads a secret file. Injectable so tests never need real
1988
+ * files; the default is the same read-and-trim every other secret gets.
1989
+ * Undefined means absent or empty; a file that is there but cannot be read
1990
+ * (permissions, a directory) throws with its cause, so a mount problem is
1991
+ * never reported as a missing token. */
1992
+ export type SecretReader = (path: string) => string | undefined;
1993
+
1994
+ export const fsSecretReader: SecretReader = (path) => {
1995
+ let v: string;
1996
+ try {
1997
+ v = readFileSync(path, "utf8").trim();
1998
+ } catch (err) {
1999
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined;
2000
+ throw new Error(`cannot read secret file '${path}': ${(err as Error).message}`, { cause: err });
2001
+ }
2002
+ return v.length > 0 ? v : undefined;
2003
+ };
2004
+
2005
+ /**
2006
+ * Full Slack boot validation — shape plus token-file presence. A missing or
2007
+ * empty token is a HARD startup error: the concierge never degrades silently
2008
+ * into a read-only half-presence in the channel. Disabled ⇒ nothing is read.
2009
+ */
2010
+ export function validateSlackConfig(cfg: Config, deps: { readSecret?: SecretReader } = {}): void {
2011
+ checkSlackShape(cfg.slack, cfg.owner);
2012
+ if (!cfg.slack.enabled) return;
2013
+ const read = deps.readSecret ?? fsSecretReader;
2014
+ for (const which of ["bot", "app"] as const) {
2015
+ const path = slackTokenPath(cfg, which);
2016
+ if (!read(path)) {
2017
+ throw new Error(
2018
+ `config.toml [slack]: ${which} token file '${path}' is missing or empty — Slack cannot start`,
2019
+ );
2020
+ }
2021
+ }
2022
+ }
2023
+
2024
+ let cached: Config | null = null;
2025
+
2026
+ /**
2027
+ * The live config's path for this process, decided by the install locator:
2028
+ * `$QCR_CONFIG`, else the install's config home — never the checkout's
2029
+ * `config.toml`. The ONE answer every no-argument
2030
+ * `loadConfig()` reads, and the one the daemon hands its sessions.
2031
+ */
2032
+ export function liveConfigPath(): string {
2033
+ return locateConfig().path;
2034
+ }
2035
+
2036
+ export function loadConfig(path?: string): Config {
2037
+ if (cached && !path) return cached;
2038
+ const p = path ?? liveConfigPath();
2039
+ if (!path && !existsSync(p)) {
2040
+ // A missing live config says where it was looked for, and — when the
2041
+ // checkout still strands one — how to move it.
2042
+ throw new MissingConfigError(
2043
+ missingConfigMessage(locateConfig(), strandedCheckoutConfig({ root: sandboxRoot() })),
2044
+ );
2045
+ }
2046
+ const cfg = parseConfig(readFileSync(p, "utf8"));
2047
+ validateSlackConfig(cfg);
2048
+ if (!path) cached = cfg;
2049
+ return cfg;
2050
+ }