@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
@@ -0,0 +1,1678 @@
1
+ // The overseer's action space: command files in commands/*.json, validated
2
+ // against a closed whitelist and executed by the daemon. There is deliberately
3
+ // no generic escape hatch — anything not on the whitelist is rejected, logged
4
+ // loudly, and answered with a result file the overseer can read.
5
+ //
6
+ // Envelope: {"command": "<name>", ...payload}. Schemas are hand-rolled and
7
+ // STRICT — unknown fields are a rejection, not a warning.
8
+ //
9
+ // Two issuers, decided by directory. The same vocabulary is served
10
+ // from the daemon-owned `commands/` (and `commands/slack/`), where every
11
+ // writer is a pi SESSION, and from `commands/operator/`, which the operator's
12
+ // tools (`qcr drain`, the board's `u`, the deploy tab) create and write as
13
+ // ROOT so that the daemon's uid — the uid every session runs as — cannot write
14
+ // into it. Nothing in a command file says who wrote it; the watcher stamps
15
+ // the issuer from the directory and the seal, and the lever handlers
16
+ // (`src/levers.ts`) bound a session's `drain`/`pause-pr` and never an
17
+ // operator's. A file in the operator directory without the seal is served as
18
+ // a session's, loudly.
19
+ import { createHash } from "node:crypto";
20
+ import {
21
+ closeSync,
22
+ constants as fsConstants,
23
+ existsSync,
24
+ fstatSync,
25
+ lstatSync,
26
+ mkdirSync,
27
+ openSync,
28
+ readFileSync,
29
+ readdirSync,
30
+ renameSync,
31
+ statSync,
32
+ watch,
33
+ writeFileSync,
34
+ type FSWatcher,
35
+ } from "node:fs";
36
+ import { basename, dirname, join } from "node:path";
37
+ import type { StateStore } from "./state.js";
38
+ import type { FileOutcome, ImprovementPayload, IssuePayload } from "./improvements.js";
39
+ import { BRANCH_MAX, BRANCH_RE, isBranchName, SLUG_RE } from "./improvements.js";
40
+ import type { LeverIssuer } from "./levers.js";
41
+ import type { LessonSwitchChange } from "./lessonledger.js";
42
+ import { validateReceipt, type CurationReceipt } from "./learner/receipt.js";
43
+ import { beltRestricts, type OriginRecord } from "./slack/origin.js";
44
+ import { SLACK_TS_RE } from "./slack/reply.js";
45
+
46
+ /** The session-writable command vocabulary — the one list `CommandName` is
47
+ * read from, so a name cannot be typed without being admitted. */
48
+ export const COMMAND_WHITELIST = [
49
+ "rerun-round",
50
+ "restart-session",
51
+ "resolve-terminal-failure",
52
+ "pause-pr",
53
+ "resume-pr",
54
+ "flag-owner",
55
+ "file-improvement-pr",
56
+ "file-issue",
57
+ "slack_reply",
58
+ "drain",
59
+ "retry-thread-decision",
60
+ "eod-curation-receipt",
61
+ ] as const;
62
+
63
+ export type CommandName = (typeof COMMAND_WHITELIST)[number];
64
+
65
+ /**
66
+ * The operator's round controls: what the viewer's input line, `msg.ts` and
67
+ * the companion send to a PR's running (or idle) session. `steer` goes straight
68
+ * to the running session (rule 9's one exception), `follow-up` queues behind
69
+ * the turn, `abort-round` stops the round, and `message` is the idle PR's line
70
+ * (`may_post` marks one the operator allows to post). `takeover`, `release` and
71
+ * `debug-round` take, free and replace the PR's host. They are operator
72
+ * commands, so they are valid only from the root-sealed channel, and no session
73
+ * or Slack turn can spell them. The daemon acts on them through the optional
74
+ * `operatorControl` dep; a daemon without it refuses with a reason.
75
+ */
76
+ export const CONTROL_COMMANDS = [
77
+ "steer",
78
+ "follow-up",
79
+ "abort-round",
80
+ "message",
81
+ "takeover",
82
+ "release",
83
+ "debug-round",
84
+ ] as const;
85
+
86
+ export type ControlCommandName = (typeof CONTROL_COMMANDS)[number];
87
+
88
+ /** The longest `text` a steer, follow-up or message carries, in characters. */
89
+ export const CONTROL_TEXT_MAX = 8000;
90
+
91
+ /**
92
+ * The operator's commands: the lesson kill switch and the archiver's two
93
+ * verbs (`archive-run`, one immediate bounded archiver tick, and
94
+ * `archive-restore`, one PR's archive back to its live paths, both run under
95
+ * the archiver's strict mutex). They are not in `COMMAND_WHITELIST` — the
96
+ * vocabulary a session (the overseer, the EOD curator, a Slack turn) may
97
+ * write — and they validate only for a file the watcher found in the
98
+ * root-sealed `commands/operator/` directory (`operatorSealVerdict`). A
99
+ * session's command file cannot spell them: in the session directories, and
100
+ * in an UNSEALED operator directory (served as a session's), they are refused
101
+ * at validation, and `executeCommand` refuses
102
+ * them again for any issuer but the operator. The kill switch's state lives
103
+ * in state.db (src/lessonledger.ts), never as a memory tag anyone holding a
104
+ * key could write.
105
+ */
106
+ export const OPERATOR_COMMANDS = [
107
+ "lessons-off",
108
+ "lessons-on",
109
+ "suppress-lesson",
110
+ "unsuppress-lesson",
111
+ "archive-run",
112
+ "archive-restore",
113
+ ...CONTROL_COMMANDS,
114
+ ] as const;
115
+
116
+ export type OperatorCommandName = (typeof OPERATOR_COMMANDS)[number];
117
+
118
+ /** A lesson the operator names: the full memory URI of one entry
119
+ * (`viking://user/<user>/memories/<area>/<file>.md`) — `qcr lessons
120
+ * suppress` expands a bare `experiences/<file>.md` to it. */
121
+ export const LESSON_URI_RE =
122
+ /^viking:\/\/user\/[A-Za-z0-9_-]{1,64}\/memories\/(?:[a-z][a-z0-9_-]*\/){1,2}[A-Za-z0-9._-]{1,200}\.md$/;
123
+
124
+ function isOperatorCommand(name: string): name is OperatorCommandName {
125
+ return (OPERATOR_COMMANDS as readonly string[]).includes(name);
126
+ }
127
+
128
+ /**
129
+ * Retired command spellings, folded to their current name at validate time —
130
+ * the codebase's ESCALATE→NEEDS_HUMAN pattern: accepted on input (sessions on
131
+ * older prompts may still emit them), never emitted by us — the parsed
132
+ * `Command`, the result file and the event ledger all carry the current name.
133
+ * Pinned by tests/legacy-acceptance.test.ts; an entry here is a permanent
134
+ * belt, not a cleanup candidate.
135
+ */
136
+ const LEGACY_COMMAND_NAMES: Readonly<Record<string, CommandName>> = {
137
+ "flag-owner-legacy": "flag-owner",
138
+ };
139
+
140
+ export interface TerminalFailurePayload {
141
+ pr: number;
142
+ incident_id: string;
143
+ command_id: string;
144
+ head_sha: string;
145
+ }
146
+
147
+ export type Command =
148
+ | { command: "rerun-round"; pr: number }
149
+ | { command: "restart-session"; pr: number }
150
+ | ({ command: "resolve-terminal-failure" } & TerminalFailurePayload)
151
+ /** `for_min`: how long the hold is asked for. Optional — a
152
+ * session that omits it gets the largest bounded grant the cap and the
153
+ * day's budget allow, never "until resumed"; an operator's omission IS
154
+ * "until resumed". Which of the two applies comes from the channel. */
155
+ | { command: "pause-pr"; pr: number; reason: string; for_min?: number }
156
+ | { command: "resume-pr"; pr: number }
157
+ | { command: "flag-owner"; message: string }
158
+ | ({ command: "file-improvement-pr" } & ImprovementPayload)
159
+ | ({ command: "file-issue" } & IssuePayload)
160
+ | { command: "slack_reply"; thread: string; text: string }
161
+ /** Deploy-window drain: a hard webhook stop + dispatch/recheck/ping-pong/EOD
162
+ * hold while in-flight rounds finish naturally. `for_min` as on `pause-pr`;
163
+ * only meaningful with `on: true`. `hold` asks for the operator's
164
+ * no-auto-lift drain: only with `on: true`, never with `for_min`.
165
+ * The field says WHAT is asked, never who asks: the lever handler refuses a
166
+ * hold to any issuer but the sealed operator directory's. */
167
+ | { command: "drain"; on: boolean; for_min?: number; hold?: true }
168
+ | { command: "retry-thread-decision"; comment_id: number }
169
+ /** The EOD curator's answer to the day's outcomes packet — per
170
+ * candidate, adopted / declined (with a reason) / refused (the write
171
+ * failed). Schema in src/learner/receipt.ts. */
172
+ | ({ command: "eod-curation-receipt" } & CurationReceipt)
173
+ /** The operator's lesson kill switch (`OPERATOR_COMMANDS`):
174
+ * never valid in a session's file. */
175
+ | { command: "lessons-off" }
176
+ | { command: "lessons-on" }
177
+ | { command: "suppress-lesson"; uri: string }
178
+ | { command: "unsuppress-lesson"; uri: string }
179
+ /** The operator's archive verbs (`OPERATOR_COMMANDS`): never
180
+ * valid in a session's file. */
181
+ | { command: "archive-run" }
182
+ | { command: "archive-restore"; pr: number }
183
+ /** The operator's round controls (`CONTROL_COMMANDS`): never valid in a
184
+ * session's file. */
185
+ | { command: "steer"; pr: number; text: string }
186
+ | { command: "follow-up"; pr: number; text: string }
187
+ | { command: "abort-round"; pr: number }
188
+ | { command: "message"; pr: number; text: string; may_post?: boolean }
189
+ | { command: "takeover"; pr: number }
190
+ | { command: "release"; pr: number }
191
+ | { command: "debug-round"; pr: number };
192
+
193
+ /** A round control as the daemon's `operatorControl` handler receives it. */
194
+ export type ControlCommand = Extract<Command, { command: ControlCommandName }>;
195
+
196
+ /** Who a command directory's writer is. Decided by which directory a file
197
+ * arrived in and whether the operator seal held — never by a field in the
198
+ * file. `session` is the default everywhere: it is the bounded side. */
199
+ export type CommandIssuer = LeverIssuer;
200
+
201
+ /** Context a command file carries with it (its own name, for idempotence,
202
+ * and the issuer the watcher stamped from the channel). */
203
+ export interface CommandContext {
204
+ /** The command file's name without `.json` — `<scope>/<name>` when the
205
+ * watcher serving it has a `scope` — or "" for a direct executeCommand
206
+ * call. It names one delivery, stable across that file's retries. */
207
+ file: string;
208
+ /** Exact parsed file version; replacements are fresh deliveries. */
209
+ delivery_key?: string;
210
+ issuer: CommandIssuer;
211
+ }
212
+
213
+ /** Handlers the daemon wires; each may reject by returning {ok:false, reason}. */
214
+ export interface CommandDeps {
215
+ rerunRound(pr: number): Promise<FileOutcome | void> | FileOutcome | void;
216
+ restartSession(
217
+ target: number,
218
+ deliveryKey?: string,
219
+ ): Promise<FileOutcome | void> | FileOutcome | void;
220
+ resolveTerminalFailure(
221
+ payload: TerminalFailurePayload,
222
+ ): Promise<FileOutcome | void> | FileOutcome | void;
223
+ /** The two bounded levers receive the issuer the watcher stamped
224
+ * from the channel, plus the optional asked-for duration. */
225
+ pausePr(
226
+ pr: number,
227
+ reason: string,
228
+ issuer: CommandIssuer,
229
+ forMin: number | undefined,
230
+ deliveryKey?: string,
231
+ ): Promise<FileOutcome | void> | FileOutcome | void;
232
+ resumePr(pr: number, issuer: CommandIssuer): Promise<FileOutcome | void> | FileOutcome | void;
233
+ flagOwner(message: string): Promise<FileOutcome | void> | FileOutcome | void;
234
+ fileImprovementPr(
235
+ payload: ImprovementPayload,
236
+ deliveryKey?: string,
237
+ ): Promise<FileOutcome | void> | FileOutcome | void;
238
+ fileIssue(
239
+ payload: IssuePayload,
240
+ deliveryKey?: string,
241
+ ): Promise<FileOutcome | void> | FileOutcome | void;
242
+ /** Deploy-window drain valve. Optional like `slackReply`: a daemon wired
243
+ * without it rejects the command with a reason instead of pretending. */
244
+ drain?(
245
+ on: boolean,
246
+ issuer: CommandIssuer,
247
+ forMin: number | undefined,
248
+ deliveryKey?: string,
249
+ hold?: boolean,
250
+ ): Promise<FileOutcome | void> | FileOutcome | void;
251
+ retryThreadDecision?(commentId: number): Promise<FileOutcome | void> | FileOutcome | void;
252
+ /** The learner's receipt handler. Optional like `drain`: a
253
+ * daemon wired without the learner rejects the command with a reason. */
254
+ curationReceipt?(receipt: CurationReceipt): Promise<FileOutcome | void> | FileOutcome | void;
255
+ /** The operator's lesson kill switch (state.db). Reached only for an
256
+ * operator-issued command; optional like `drain`. */
257
+ lessonSwitch?(
258
+ change: LessonSwitchChange,
259
+ file: string,
260
+ ): Promise<FileOutcome | void> | FileOutcome | void;
261
+ /** The operator's archive verbs — one immediate bounded tick of the
262
+ * archiver, and the restore of one PR's archive to its live paths. Reached
263
+ * only for an operator-issued command; optional like `drain`. */
264
+ archiveRun?(file: string): Promise<FileOutcome | void> | FileOutcome | void;
265
+ archiveRestore?(pr: number, file: string): Promise<FileOutcome | void> | FileOutcome | void;
266
+ /** The operator's round controls (`CONTROL_COMMANDS`), forwarded to the PR's
267
+ * host. Reached only for an operator-issued command; optional like `drain`,
268
+ * so a daemon with no worker host refuses them with a reason. */
269
+ operatorControl?(
270
+ command: ControlCommand,
271
+ file: string,
272
+ ): Promise<FileOutcome | void> | FileOutcome | void;
273
+ /** Optional: absent when Slack is disabled, in which case `slack_reply` is
274
+ * rejected with a reason instead of silently succeeding. */
275
+ slackReply?(payload: {
276
+ thread: string;
277
+ text: string;
278
+ file: string;
279
+ }): Promise<FileOutcome | void> | FileOutcome | void;
280
+ }
281
+
282
+ export type ValidationResult = { ok: true; command: Command } | { ok: false; reason: string };
283
+
284
+ // ---- hand-rolled schema validation ----
285
+
286
+ function fieldError(obj: Record<string, unknown>, allowed: string[]): string | null {
287
+ const unknown = Object.keys(obj).filter((k) => k !== "command" && !allowed.includes(k));
288
+ return unknown.length > 0 ? `unknown field(s): ${unknown.join(", ")}` : null;
289
+ }
290
+
291
+ function isPrNumber(v: unknown): v is number {
292
+ return typeof v === "number" && Number.isInteger(v) && v > 0;
293
+ }
294
+
295
+ function isGitHubId(v: unknown): v is number {
296
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
297
+ }
298
+
299
+ function isNonEmptyString(v: unknown): v is string {
300
+ return typeof v === "string" && v.trim().length > 0;
301
+ }
302
+
303
+ /** The optional `for_min` of a lever command: absent, or a positive integer.
304
+ * The CAP is not checked here — the validator does not know the channel; the
305
+ * lever handler applies it with the issuer in hand. */
306
+ function forMinField(
307
+ obj: Record<string, unknown>,
308
+ ): { ok: true; value: number | undefined } | { ok: false; reason: string } {
309
+ const v = obj["for_min"];
310
+ if (v === undefined) return { ok: true, value: undefined };
311
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
312
+ return {
313
+ ok: false,
314
+ reason: "'for_min' must be a positive integer number of minutes when present",
315
+ };
316
+ }
317
+ return { ok: true, value: v };
318
+ }
319
+
320
+ /** Where a command file came from, as far as validation needs to know: the
321
+ * operator's commands (`OPERATOR_COMMANDS`) validate only from the sealed
322
+ * operator channel. The default is the bounded side. */
323
+ export interface ValidateOptions {
324
+ issuer?: CommandIssuer;
325
+ }
326
+
327
+ /** Validate a parsed command file against the whitelist + per-command schema. */
328
+ export function validateCommand(raw: unknown, opts: ValidateOptions = {}): ValidationResult {
329
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
330
+ return { ok: false, reason: "command file must be a JSON object" };
331
+ }
332
+ const obj = raw as Record<string, unknown>;
333
+ const name = obj["command"];
334
+ if (typeof name !== "string") return { ok: false, reason: "missing 'command' field" };
335
+ if (isOperatorCommand(name)) {
336
+ if (opts.issuer !== "operator")
337
+ return {
338
+ ok: false,
339
+ reason:
340
+ `command '${name}' is the OPERATOR's alone: it is accepted only from the root-sealed commands/operator/ ` +
341
+ "channel (`qcr lessons off|on|suppress|unsuppress`, `qcr archive run|restore`), never from a session's command file",
342
+ };
343
+ return validateOperatorCommand(name, obj);
344
+ }
345
+ // Legacy-name fold FIRST: an aliased spelling validates against — and parses
346
+ // into — its current name, so downstream (tier table, dispatch, result
347
+ // files) never sees the retired spelling.
348
+ const canonical = LEGACY_COMMAND_NAMES[name] ?? name;
349
+ if (!(COMMAND_WHITELIST as readonly string[]).includes(canonical)) {
350
+ // A retired command gets its own reason: a session spawned on an older
351
+ // prompt can still emit it and should learn why, not just "unknown". The
352
+ // hint is read by a model, so it must not describe a NEEDS_HUMAN park
353
+ // either — no such park exists.
354
+ if (name === "mark-escalation-worthy") {
355
+ return {
356
+ ok: false,
357
+ reason:
358
+ "command 'mark-escalation-worthy' was RETIRED on 2026-08-18: escalation-worthiness no longer exists, " +
359
+ "and the review is never handed to a person — every round decides APPROVE or REQUEST_CHANGES on the merits. " +
360
+ "Use pause-pr to hold one, or flag-owner (formerly flag-owner-legacy — the old name is still accepted) to raise it.",
361
+ };
362
+ }
363
+ return {
364
+ ok: false,
365
+ reason: `command '${name}' is not whitelisted (allowed: ${COMMAND_WHITELIST.join(", ")})`,
366
+ };
367
+ }
368
+
369
+ switch (canonical as CommandName) {
370
+ case "rerun-round": {
371
+ const err = fieldError(obj, ["pr"]);
372
+ if (err) return { ok: false, reason: err };
373
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
374
+ return { ok: true, command: { command: "rerun-round", pr: obj["pr"] } };
375
+ }
376
+ case "restart-session": {
377
+ const err = fieldError(obj, ["pr"]);
378
+ if (err) return { ok: false, reason: err };
379
+ const pr = obj["pr"];
380
+ if (!isPrNumber(pr)) {
381
+ return { ok: false, reason: "'pr' must be a positive integer" };
382
+ }
383
+ return { ok: true, command: { command: "restart-session", pr: pr } };
384
+ }
385
+ case "resolve-terminal-failure": {
386
+ const err = fieldError(obj, ["pr", "incident_id", "command_id", "head_sha"]);
387
+ if (err) return { ok: false, reason: err };
388
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
389
+ for (const field of ["incident_id", "command_id"]) {
390
+ if (!isNonEmptyString(obj[field]))
391
+ return { ok: false, reason: `'${field}' must be a non-empty string` };
392
+ }
393
+ // An authoritative GitHub 404 can legitimately correspond to a legacy
394
+ // synthetic debt whose stored head is the empty string. Closed PRs are
395
+ // still forced to a non-empty exact match by resolveTerminalFailure().
396
+ if (typeof obj["head_sha"] !== "string")
397
+ return { ok: false, reason: "'head_sha' must be a string" };
398
+ if (obj["incident_id"] !== `review-failure:${obj["command_id"] as string}`) {
399
+ return {
400
+ ok: false,
401
+ reason: "'incident_id' must exactly equal review-failure:<command_id>",
402
+ };
403
+ }
404
+ return {
405
+ ok: true,
406
+ command: {
407
+ command: "resolve-terminal-failure",
408
+ pr: obj["pr"],
409
+ incident_id: obj["incident_id"],
410
+ command_id: obj["command_id"] as string,
411
+ head_sha: obj["head_sha"],
412
+ },
413
+ };
414
+ }
415
+ case "pause-pr": {
416
+ const err = fieldError(obj, ["pr", "reason", "for_min"]);
417
+ if (err) return { ok: false, reason: err };
418
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
419
+ if (!isNonEmptyString(obj["reason"]))
420
+ return { ok: false, reason: "'reason' must be a non-empty string" };
421
+ const forMin = forMinField(obj);
422
+ if (!forMin.ok) return forMin;
423
+ return {
424
+ ok: true,
425
+ command: {
426
+ command: "pause-pr",
427
+ pr: obj["pr"],
428
+ reason: obj["reason"],
429
+ ...(forMin.value === undefined ? {} : { for_min: forMin.value }),
430
+ },
431
+ };
432
+ }
433
+ case "resume-pr": {
434
+ const err = fieldError(obj, ["pr"]);
435
+ if (err) return { ok: false, reason: err };
436
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
437
+ return { ok: true, command: { command: "resume-pr", pr: obj["pr"] } };
438
+ }
439
+ case "flag-owner": {
440
+ const err = fieldError(obj, ["message"]);
441
+ if (err) return { ok: false, reason: err };
442
+ if (!isNonEmptyString(obj["message"]))
443
+ return { ok: false, reason: "'message' must be a non-empty string" };
444
+ return { ok: true, command: { command: "flag-owner", message: obj["message"] } };
445
+ }
446
+ case "file-improvement-pr": {
447
+ const err = fieldError(obj, ["branch", "title", "body", "slug"]);
448
+ if (err) return { ok: false, reason: err };
449
+ for (const f of ["branch", "title", "body", "slug"]) {
450
+ if (!isNonEmptyString(obj[f]))
451
+ return { ok: false, reason: `'${f}' must be a non-empty string` };
452
+ }
453
+ const slug = obj["slug"] as string;
454
+ if (!SLUG_RE.test(slug)) {
455
+ return {
456
+ ok: false,
457
+ reason: `'slug' must match ${SLUG_RE.source} (it becomes bot/improve-<slug>)`,
458
+ };
459
+ }
460
+ // The session writes `branch` and the daemon hands it to git: only a
461
+ // plain local branch name passes — never an option (`--quiet`), a force
462
+ // refspec (`+wip/x`), a refspec (`a:b`) or a revision expression (`a..b`).
463
+ const branch = obj["branch"] as string;
464
+ if (!isBranchName(branch)) {
465
+ return {
466
+ ok: false,
467
+ reason: `'branch' must be a plain local branch name matching ${BRANCH_RE.source} (at most ${BRANCH_MAX} chars)`,
468
+ };
469
+ }
470
+ return {
471
+ ok: true,
472
+ command: {
473
+ command: "file-improvement-pr",
474
+ branch,
475
+ title: obj["title"] as string,
476
+ body: obj["body"] as string,
477
+ slug,
478
+ },
479
+ };
480
+ }
481
+ case "file-issue": {
482
+ // `pr` is optional provenance — the PR a deferral's ticket was filed
483
+ // from. The review session files the ticket itself, in the round that
484
+ // deferred; `pr` attributes the filing — and a cap refusal, which then
485
+ // leaves a `deferral_ticket_refused` anomaly — to that PR, so the refusal
486
+ // is visible where the deferral happened. It grants no rights and lifts
487
+ // no cap.
488
+ const err = fieldError(obj, ["title", "body", "pr"]);
489
+ if (err) return { ok: false, reason: err };
490
+ for (const f of ["title", "body"]) {
491
+ if (!isNonEmptyString(obj[f]))
492
+ return { ok: false, reason: `'${f}' must be a non-empty string` };
493
+ }
494
+ if (obj["pr"] !== undefined && !isPrNumber(obj["pr"])) {
495
+ return { ok: false, reason: "'pr' must be a positive integer when present" };
496
+ }
497
+ return {
498
+ ok: true,
499
+ command: {
500
+ command: "file-issue",
501
+ title: obj["title"] as string,
502
+ body: obj["body"] as string,
503
+ ...(obj["pr"] === undefined ? {} : { pr: obj["pr"] }),
504
+ },
505
+ };
506
+ }
507
+ case "drain": {
508
+ const err = fieldError(obj, ["on", "for_min", "hold"]);
509
+ if (err) return { ok: false, reason: err };
510
+ if (typeof obj["on"] !== "boolean") {
511
+ return {
512
+ ok: false,
513
+ reason: "'on' must be a boolean (true = engage the drain, false = lift it)",
514
+ };
515
+ }
516
+ const forMin = forMinField(obj);
517
+ if (!forMin.ok) return forMin;
518
+ if (forMin.value !== undefined && !obj["on"])
519
+ return { ok: false, reason: "'for_min' only applies with on = true" };
520
+ const hold = obj["hold"];
521
+ if (hold !== undefined && typeof hold !== "boolean")
522
+ return { ok: false, reason: "'hold' must be a boolean" };
523
+ if (hold === true && !obj["on"])
524
+ return { ok: false, reason: "'hold' only applies with on = true (drain off lifts a hold)" };
525
+ if (hold === true && forMin.value !== undefined)
526
+ return {
527
+ ok: false,
528
+ reason: "'hold' has no expiry; 'for_min' and 'hold' exclude each other",
529
+ };
530
+ return {
531
+ ok: true,
532
+ command: {
533
+ command: "drain",
534
+ on: obj["on"],
535
+ ...(forMin.value === undefined ? {} : { for_min: forMin.value }),
536
+ ...(hold === true ? { hold: true as const } : {}),
537
+ },
538
+ };
539
+ }
540
+ case "retry-thread-decision": {
541
+ const err = fieldError(obj, ["comment_id"]);
542
+ if (err) return { ok: false, reason: err };
543
+ if (!isGitHubId(obj["comment_id"]))
544
+ return { ok: false, reason: "'comment_id' must be a positive safe integer" };
545
+ return {
546
+ ok: true,
547
+ command: { command: "retry-thread-decision", comment_id: obj["comment_id"] },
548
+ };
549
+ }
550
+ case "eod-curation-receipt": {
551
+ const res = validateReceipt(obj);
552
+ if (!res.ok) return res;
553
+ return { ok: true, command: { command: "eod-curation-receipt", ...res.receipt } };
554
+ }
555
+ case "slack_reply": {
556
+ const err = fieldError(obj, ["thread", "text"]);
557
+ if (err) return { ok: false, reason: err };
558
+ // No `channel` field, by construction: the daemon binds every reply to
559
+ // cfg.slack.channel, so posting in the wrong place is unrepresentable.
560
+ if (!isNonEmptyString(obj["thread"]) || !SLACK_TS_RE.test(obj["thread"])) {
561
+ return {
562
+ ok: false,
563
+ reason: "'thread' must be a Slack message ts like \"1712345678.123456\"",
564
+ };
565
+ }
566
+ if (!isNonEmptyString(obj["text"]))
567
+ return { ok: false, reason: "'text' must be a non-empty string" };
568
+ return {
569
+ ok: true,
570
+ command: { command: "slack_reply", thread: obj["thread"], text: obj["text"] },
571
+ };
572
+ }
573
+ }
574
+ }
575
+
576
+ /** The operator commands' strict schemas (the issuer was checked first). */
577
+ function validateOperatorCommand(
578
+ name: OperatorCommandName,
579
+ obj: Record<string, unknown>,
580
+ ): ValidationResult {
581
+ switch (name) {
582
+ case "lessons-off":
583
+ case "lessons-on": {
584
+ const err = fieldError(obj, []);
585
+ if (err) return { ok: false, reason: err };
586
+ return { ok: true, command: { command: name } };
587
+ }
588
+ case "suppress-lesson":
589
+ case "unsuppress-lesson": {
590
+ const err = fieldError(obj, ["uri"]);
591
+ if (err) return { ok: false, reason: err };
592
+ const uri = obj["uri"];
593
+ if (typeof uri !== "string" || !LESSON_URI_RE.test(uri) || uri.includes(".."))
594
+ return {
595
+ ok: false,
596
+ reason:
597
+ "'uri' must be one memory entry's full URI (viking://user/<user>/memories/<area>/<file>.md)",
598
+ };
599
+ return { ok: true, command: { command: name, uri } };
600
+ }
601
+ case "archive-run": {
602
+ const err = fieldError(obj, []);
603
+ if (err) return { ok: false, reason: err };
604
+ return { ok: true, command: { command: "archive-run" } };
605
+ }
606
+ case "archive-restore": {
607
+ const err = fieldError(obj, ["pr"]);
608
+ if (err) return { ok: false, reason: err };
609
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
610
+ return { ok: true, command: { command: "archive-restore", pr: obj["pr"] } };
611
+ }
612
+ case "steer":
613
+ case "follow-up":
614
+ case "message": {
615
+ const err = fieldError(obj, name === "message" ? ["pr", "text", "may_post"] : ["pr", "text"]);
616
+ if (err) return { ok: false, reason: err };
617
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
618
+ const text = obj["text"];
619
+ if (!isNonEmptyString(text) || text.length > CONTROL_TEXT_MAX)
620
+ return {
621
+ ok: false,
622
+ reason: `'text' must be a non-empty string of at most ${CONTROL_TEXT_MAX} characters`,
623
+ };
624
+ if (name !== "message") return { ok: true, command: { command: name, pr: obj["pr"], text } };
625
+ const mayPost = obj["may_post"];
626
+ if (mayPost !== undefined && typeof mayPost !== "boolean")
627
+ return { ok: false, reason: "'may_post' must be a boolean when present" };
628
+ return {
629
+ ok: true,
630
+ command: {
631
+ command: "message",
632
+ pr: obj["pr"],
633
+ text,
634
+ ...(mayPost === undefined ? {} : { may_post: mayPost }),
635
+ },
636
+ };
637
+ }
638
+ case "abort-round":
639
+ case "takeover":
640
+ case "release":
641
+ case "debug-round": {
642
+ const err = fieldError(obj, ["pr"]);
643
+ if (err) return { ok: false, reason: err };
644
+ if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
645
+ return { ok: true, command: { command: name, pr: obj["pr"] } };
646
+ }
647
+ }
648
+ }
649
+
650
+ // ---- tier enforcement ----
651
+
652
+ /**
653
+ * Commands a Slack turn may run, by tier. The table is data on purpose: adding
654
+ * a command without deciding its tier is then impossible to do by accident.
655
+ *
656
+ * `own` = allowed for tier `author` only when the target PR is in the origin's
657
+ * `authorized_prs` (recomputed by the router from `pr_state.author`, NEVER read
658
+ * out of the inject body).
659
+ *
660
+ * `owner` is the owner's tier (legacy persisted records with the retired tier
661
+ * spelling fold to it on read). The `companion` tier is not a column: it is the
662
+ * owner's column read again (`tierAllows`), so the two are one shared set and a
663
+ * verb added for the owner is the companion's in the same edit (by ruling:
664
+ * the companion can do everything the owner can). `system` is the
665
+ * daemon's own unattended turns (`OriginStore.writeLocal`): it is `true` on
666
+ * every row but the operator's round controls (`CONTROL_COMMANDS`, which a
667
+ * daemon turn has no standing to send) because a
668
+ * daemon turn with NO origin record already runs unrestricted —
669
+ * `authorizeCommand(_, null)` ⇒ ok, and that null-origin rule is unchanged.
670
+ * The column is the stamped, auditable form of that same
671
+ * standing authority; it never widens anything, and a Slack turn can never
672
+ * reach it (`computeTier` never returns `system`).
673
+ *
674
+ * Nothing here can reach the dry_run flag, a deploy, an approval or anything
675
+ * review-dismissing: those are not commands at all, and this feature adds no
676
+ * GitHub mutation — every write still goes through the MutationGate.
677
+ */
678
+ const TIER_TABLE: Record<
679
+ CommandName | ControlCommandName,
680
+ { anyone: boolean; author: "own" | boolean; owner: boolean; system: boolean }
681
+ > = {
682
+ "rerun-round": { anyone: false, author: "own", owner: true, system: true },
683
+ "pause-pr": { anyone: false, author: "own", owner: true, system: true },
684
+ "resume-pr": { anyone: false, author: "own", owner: true, system: true },
685
+ // The safety valve: anyone may always reach a human.
686
+ "flag-owner": { anyone: true, author: true, owner: true, system: true },
687
+ "restart-session": { anyone: false, author: false, owner: true, system: true },
688
+ // Terminal debt cleanup is destructive and never author-reachable. Its
689
+ // handler additionally requires exact identity + authoritative terminality.
690
+ "resolve-terminal-failure": { anyone: false, author: false, owner: true, system: true },
691
+ // improvements.ts caps still apply ON TOP — a tier never lifts a cap.
692
+ "file-improvement-pr": { anyone: false, author: false, owner: true, system: true },
693
+ // The review session files a deferral's ticket itself, in the round that
694
+ // deferred — there is no author confirmation step. Its command files land
695
+ // in the `commands/` dir with no Slack origin, which already runs at full
696
+ // (system-column) autonomy, so the deferral path needs no widening here. On
697
+ // Slack, `file-issue` is the owner's alone. The day cap, the dedup and the
698
+ // gate all still apply downstream — a tier never lifts a cap.
699
+ "file-issue": { anyone: false, author: false, owner: true, system: true },
700
+ // Not user-tiered: gated by channel binding, thread, caps, cooldown, length.
701
+ slack_reply: { anyone: true, author: true, owner: true, system: true },
702
+ // Deploy-window drain: an operator lever (same standing as restart-session)
703
+ // — it stops the whole board, so it is never author-reachable.
704
+ drain: { anyone: false, author: false, owner: true, system: true },
705
+ "retry-thread-decision": { anyone: false, author: false, owner: true, system: true },
706
+ // The EOD curator's receipt — a daemon (system) turn's, or the
707
+ // owner's; never a Slack author's or anyone's.
708
+ "eod-curation-receipt": { anyone: false, author: false, owner: true, system: true },
709
+ // The operator's round controls reach the daemon only from the sealed
710
+ // operator channel (`validateCommand`), so this row never admits a Slack
711
+ // turn; it is the verb set the owner and the companion share. A daemon turn
712
+ // (`system`) never steers, takes over or releases a PR.
713
+ steer: { anyone: false, author: false, owner: true, system: false },
714
+ "follow-up": { anyone: false, author: false, owner: true, system: false },
715
+ "abort-round": { anyone: false, author: false, owner: true, system: false },
716
+ message: { anyone: false, author: false, owner: true, system: false },
717
+ takeover: { anyone: false, author: false, owner: true, system: false },
718
+ release: { anyone: false, author: false, owner: true, system: false },
719
+ "debug-round": { anyone: false, author: false, owner: true, system: false },
720
+ };
721
+
722
+ /** A tier the table is read for besides a Slack turn's: the companion, whose
723
+ * every verb is the owner's. */
724
+ export type CommandTier = "owner" | "companion" | "system";
725
+
726
+ /** May `tier` send `name`? `companion` reads the owner's column: one set. */
727
+ export function tierAllows(tier: CommandTier, name: string): boolean {
728
+ const row = (TIER_TABLE as Record<string, (typeof TIER_TABLE)[CommandName] | undefined>)[name];
729
+ if (!row) return false;
730
+ return tier === "system" ? row.system : row.owner;
731
+ }
732
+
733
+ /** Every verb `tier` may send, in table order. */
734
+ export function tierVerbs(tier: CommandTier): string[] {
735
+ return Object.keys(TIER_TABLE).filter((name) => tierAllows(tier, name));
736
+ }
737
+
738
+ /** Target PR of a command, when it has one. */
739
+ function targetPr(command: Command): number | null {
740
+ if (command.command === "restart-session")
741
+ return typeof command.pr === "number" ? command.pr : null;
742
+ return "pr" in command && typeof command.pr === "number" ? command.pr : null;
743
+ }
744
+
745
+ export type AuthorizationResult = { ok: true } | { ok: false; reason: string };
746
+
747
+ /**
748
+ * May this command run on behalf of `origin`?
749
+ *
750
+ * `origin === null` means "no Slack turn is live" — the command came from the
751
+ * daemon itself (EOD sweep, anomaly wake) or from the owner at the tmux pane,
752
+ * and keeps full autonomy (the same rights the `system` tier stamps
753
+ * explicitly). A caller that must fail closed (the Slack command directory)
754
+ * checks for a missing origin BEFORE calling this.
755
+ *
756
+ * Every rejection reason starts with `tier: ` so the overseer — and the
757
+ * `command_rejected` event stream — can tell a rights refusal from a cap.
758
+ */
759
+ export function authorizeCommand(
760
+ command: Command,
761
+ origin: OriginRecord | null,
762
+ ): AuthorizationResult {
763
+ if (isOperatorCommand(command.command))
764
+ return {
765
+ ok: false,
766
+ reason: `tier: '${command.command}' is the operator's alone (commands/operator/), never a Slack turn's`,
767
+ };
768
+ if (origin === null) return { ok: true };
769
+ const row = TIER_TABLE[command.command];
770
+ const pr = targetPr(command);
771
+ switch (origin.tier) {
772
+ case "owner":
773
+ return row.owner
774
+ ? { ok: true }
775
+ : { ok: false, reason: `tier: '${command.command}' is not available` };
776
+ case "system":
777
+ return row.system
778
+ ? { ok: true }
779
+ : { ok: false, reason: `tier: '${command.command}' is not available` };
780
+ case "author": {
781
+ if (row.author === true) return { ok: true };
782
+ if (row.author === false) {
783
+ return {
784
+ ok: false,
785
+ reason: `tier: '${command.command}' requires tier owner (this turn is tier author)`,
786
+ };
787
+ }
788
+ if (pr === null)
789
+ return {
790
+ ok: false,
791
+ reason: `tier: '${command.command}' needs a PR number to check ownership`,
792
+ };
793
+ if (origin.authorized_prs.includes(pr)) return { ok: true };
794
+ return {
795
+ ok: false,
796
+ reason: `tier: PR #${pr} is not authored by this Slack user (authorized: ${
797
+ origin.authorized_prs.length ? origin.authorized_prs.join(", ") : "none"
798
+ })`,
799
+ };
800
+ }
801
+ default:
802
+ // `anyone` and `ignored` alike: read-only plus the two ungated commands.
803
+ return row.anyone
804
+ ? { ok: true }
805
+ : {
806
+ ok: false,
807
+ reason: `tier: '${command.command}' is not available at tier ${origin.tier}`,
808
+ };
809
+ }
810
+ }
811
+
812
+ /**
813
+ * May the companion send this command? Not a Slack origin: the TUI forwards a
814
+ * companion's action as an operator command, and checks it here first.
815
+ * The verbs are the owner's, no more and no fewer (`tierAllows`).
816
+ */
817
+ export function authorizeCompanionCommand(command: Command): AuthorizationResult {
818
+ return tierAllows("companion", command.command)
819
+ ? { ok: true }
820
+ : { ok: false, reason: `tier: '${command.command}' is not available to the companion` };
821
+ }
822
+
823
+ function normalize(outcome: FileOutcome | void): FileOutcome {
824
+ return outcome === undefined || outcome === null ? { ok: true } : outcome;
825
+ }
826
+
827
+ /** Dispatch a validated command to the injected handlers. */
828
+ export async function executeCommand(
829
+ command: Command,
830
+ deps: CommandDeps,
831
+ // A direct call with no context is a SESSION's: the bounded side is the
832
+ // default, and only the operator watcher ever stamps the other one.
833
+ ctx: CommandContext = { file: "", issuer: "session" },
834
+ ): Promise<FileOutcome> {
835
+ switch (command.command) {
836
+ case "rerun-round":
837
+ return normalize(await deps.rerunRound(command.pr));
838
+ case "restart-session":
839
+ return normalize(
840
+ await deps.restartSession(
841
+ command.pr,
842
+ ...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
843
+ ),
844
+ );
845
+ case "resolve-terminal-failure":
846
+ return normalize(
847
+ await deps.resolveTerminalFailure({
848
+ pr: command.pr,
849
+ incident_id: command.incident_id,
850
+ command_id: command.command_id,
851
+ head_sha: command.head_sha,
852
+ }),
853
+ );
854
+ case "pause-pr":
855
+ return normalize(
856
+ await deps.pausePr(
857
+ command.pr,
858
+ command.reason,
859
+ ctx.issuer,
860
+ command.for_min,
861
+ ...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
862
+ ),
863
+ );
864
+ case "resume-pr":
865
+ return normalize(await deps.resumePr(command.pr, ctx.issuer));
866
+ case "flag-owner":
867
+ return normalize(await deps.flagOwner(command.message));
868
+ case "file-improvement-pr":
869
+ return normalize(
870
+ await deps.fileImprovementPr(
871
+ {
872
+ branch: command.branch,
873
+ title: command.title,
874
+ body: command.body,
875
+ slug: command.slug,
876
+ },
877
+ ...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
878
+ ),
879
+ );
880
+ case "file-issue":
881
+ return normalize(
882
+ await deps.fileIssue(
883
+ {
884
+ title: command.title,
885
+ body: command.body,
886
+ ...(command.pr === undefined ? {} : { pr: command.pr }),
887
+ },
888
+ ...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
889
+ ),
890
+ );
891
+ case "slack_reply": {
892
+ if (!deps.slackReply)
893
+ return { ok: false, reason: "slack_reply is not wired (Slack is disabled)" };
894
+ return normalize(
895
+ await deps.slackReply({
896
+ thread: command.thread,
897
+ text: command.text,
898
+ // Session filenames are stable replay keys, even after archival.
899
+ // Sealed operator rewrites are separate, versioned deliveries.
900
+ file: ctx.issuer === "operator" ? (ctx.delivery_key ?? ctx.file) : ctx.file,
901
+ }),
902
+ );
903
+ }
904
+ case "drain": {
905
+ if (!deps.drain) return { ok: false, reason: "drain is not wired on this daemon" };
906
+ if (command.hold === true)
907
+ return normalize(
908
+ await deps.drain(command.on, ctx.issuer, command.for_min, ctx.delivery_key, true),
909
+ );
910
+ return normalize(
911
+ await deps.drain(
912
+ command.on,
913
+ ctx.issuer,
914
+ command.for_min,
915
+ ...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
916
+ ),
917
+ );
918
+ }
919
+ case "retry-thread-decision": {
920
+ if (!deps.retryThreadDecision)
921
+ return { ok: false, reason: "retry-thread-decision is not wired on this daemon" };
922
+ return normalize(await deps.retryThreadDecision(command.comment_id));
923
+ }
924
+ case "eod-curation-receipt": {
925
+ if (!deps.curationReceipt)
926
+ return {
927
+ ok: false,
928
+ reason: "eod-curation-receipt is not wired on this daemon (no learner)",
929
+ };
930
+ return normalize(
931
+ await deps.curationReceipt({ day: command.day, decisions: command.decisions }),
932
+ );
933
+ }
934
+ case "lessons-off":
935
+ case "lessons-on":
936
+ case "suppress-lesson":
937
+ case "unsuppress-lesson": {
938
+ // The belt behind validation's brace: whoever reached this function,
939
+ // only an operator-stamped context may flip the kill switch.
940
+ if (ctx.issuer !== "operator")
941
+ return {
942
+ ok: false,
943
+ reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
944
+ };
945
+ if (!deps.lessonSwitch)
946
+ return { ok: false, reason: `${command.command} is not wired on this daemon` };
947
+ const change: LessonSwitchChange =
948
+ command.command === "lessons-off"
949
+ ? { action: "off" }
950
+ : command.command === "lessons-on"
951
+ ? { action: "on" }
952
+ : command.command === "suppress-lesson"
953
+ ? { action: "suppress", uri: command.uri }
954
+ : { action: "unsuppress", uri: command.uri };
955
+ return normalize(await deps.lessonSwitch(change, ctx.delivery_key ?? ctx.file));
956
+ }
957
+ case "archive-run":
958
+ case "archive-restore": {
959
+ // The belt behind validation's brace, as for the kill switch.
960
+ if (ctx.issuer !== "operator")
961
+ return {
962
+ ok: false,
963
+ reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
964
+ };
965
+ if (command.command === "archive-run") {
966
+ if (!deps.archiveRun)
967
+ return { ok: false, reason: "archive-run is not wired on this daemon" };
968
+ return normalize(await deps.archiveRun(ctx.delivery_key ?? ctx.file));
969
+ }
970
+ if (!deps.archiveRestore)
971
+ return { ok: false, reason: "archive-restore is not wired on this daemon" };
972
+ return normalize(await deps.archiveRestore(command.pr, ctx.delivery_key ?? ctx.file));
973
+ }
974
+ case "steer":
975
+ case "follow-up":
976
+ case "abort-round":
977
+ case "message":
978
+ case "takeover":
979
+ case "release":
980
+ case "debug-round": {
981
+ // The belt behind validation's brace, as for the kill switch.
982
+ if (ctx.issuer !== "operator")
983
+ return {
984
+ ok: false,
985
+ reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
986
+ };
987
+ if (!deps.operatorControl)
988
+ return { ok: false, reason: `${command.command} is not wired on this daemon` };
989
+ return normalize(await deps.operatorControl(command, ctx.delivery_key ?? ctx.file));
990
+ }
991
+ }
992
+ }
993
+
994
+ // ---- the watcher ----
995
+
996
+ /**
997
+ * Which command directory this watcher serves, and how to find the live Slack
998
+ * origin — two directories, one gate.
999
+ *
1000
+ * `local` — the `commands/` dir. Untiered, so EOD sweeps, anomaly wakes and
1001
+ * the owner at the tmux pane keep full autonomy. The one BELT:
1002
+ * when the turn being served was CAUSED by a non-owner Slack
1003
+ * message, this dir refuses — the overseer
1004
+ * wrote to the wrong place mid-turn, and a tier-checked command
1005
+ * must not slip past by choosing a filename.
1006
+ * `slack` — the tier-checked `commands/slack/` dir. No live origin ⇒ DENY.
1007
+ *
1008
+ * Omit the gate entirely and the directory is served with no tier check.
1009
+ */
1010
+ export interface CommandTierGate {
1011
+ dir: "local" | "slack";
1012
+ /**
1013
+ * What this gate reads, and it differs by dir — both directions fail closed:
1014
+ *
1015
+ * `slack` — the origin of the turn being served, while it is live
1016
+ * (`OriginStore.latest()`; never a queued wake's rights).
1017
+ * Null ⇒ deny: rights need a live turn.
1018
+ * `local` — the same origin REGARDLESS OF AGE
1019
+ * (`OriginStore.latestAny()`). Expiry is not an escalation: a
1020
+ * Slack turn that simply ran long must not gain owner rights
1021
+ * here, and only a daemon wake (a `local` record) pi has
1022
+ * accepted clears the belt.
1023
+ */
1024
+ currentOrigin(): OriginRecord | null;
1025
+ }
1026
+
1027
+ // ---- the operator seal ----
1028
+
1029
+ /** What a path IS, as `lstat` sees it — a symlink is reported as itself,
1030
+ * never as what it points at. */
1031
+ export type PathKind = "dir" | "file" | "symlink" | "other";
1032
+
1033
+ export interface PathIdentity {
1034
+ uid: number;
1035
+ gid: number;
1036
+ /** Permission bits only (`& 0o7777`). */
1037
+ mode: number;
1038
+ kind: PathKind;
1039
+ }
1040
+
1041
+ /** What the watcher can learn about a path's owner, mode and kind. Injected
1042
+ * so the sealed case — a ROOT-owned directory — is testable by a process that
1043
+ * is not root; the real one is `processOperatorSeal()`. */
1044
+ export interface OperatorSeal {
1045
+ /** The daemon's effective uid — the uid every pi session runs as too. */
1046
+ euid: number;
1047
+ /** The daemon's supplementary groups (for the group-write bit). */
1048
+ gids: readonly number[];
1049
+ /** `null` when the path does not exist. */
1050
+ stat(path: string): PathIdentity | null;
1051
+ }
1052
+
1053
+ export function processOperatorSeal(): OperatorSeal {
1054
+ return {
1055
+ euid: process.geteuid?.() ?? -1,
1056
+ gids: process.getgroups?.() ?? [],
1057
+ stat(path) {
1058
+ try {
1059
+ // lstat, not stat: the seal is a claim about the path the operator's
1060
+ // tools wrote, and a symlink planted at that path must show as one.
1061
+ const s = lstatSync(path);
1062
+ const kind: PathKind = s.isSymbolicLink()
1063
+ ? "symlink"
1064
+ : s.isDirectory()
1065
+ ? "dir"
1066
+ : s.isFile()
1067
+ ? "file"
1068
+ : "other";
1069
+ return { uid: s.uid, gid: s.gid, mode: s.mode & 0o7777, kind };
1070
+ } catch {
1071
+ return null;
1072
+ }
1073
+ },
1074
+ };
1075
+ }
1076
+
1077
+ /** Could a process with `seal`'s identity write this path? */
1078
+ function writableBy(seal: OperatorSeal, s: PathIdentity): boolean {
1079
+ if (s.uid === seal.euid) return true;
1080
+ if ((s.mode & 0o002) !== 0) return true;
1081
+ return seal.gids.includes(s.gid) && (s.mode & 0o020) !== 0;
1082
+ }
1083
+
1084
+ export type SealVerdict = { sealed: true } | { sealed: false; reason: string };
1085
+
1086
+ /**
1087
+ * Is `file` in `dir` the OPERATOR's? It is exactly when the directory is a
1088
+ * real directory and the file a real file, and neither could have been
1089
+ * written by the daemon's own identity — the identity every session shares.
1090
+ * Anything else (a symlink standing in for the directory — the parent
1091
+ * `commands/` is the daemon's, so a session can plant one — the directory
1092
+ * made by the daemon's uid, a group- or world-writable mode, a file the
1093
+ * daemon could edit) is not a seal, and the verdict says which.
1094
+ */
1095
+ export function operatorSealVerdict(seal: OperatorSeal, dir: string, file: string): SealVerdict {
1096
+ const d = seal.stat(dir);
1097
+ if (!d) return { sealed: false, reason: "operator directory missing" };
1098
+ if (d.kind !== "dir")
1099
+ return { sealed: false, reason: `operator directory is a ${d.kind}, not a directory` };
1100
+ if (writableBy(seal, d))
1101
+ return {
1102
+ sealed: false,
1103
+ reason: `operator directory is writable by uid ${seal.euid} (owner ${d.uid}, mode ${d.mode.toString(8)})`,
1104
+ };
1105
+ const f = seal.stat(file);
1106
+ if (!f) return { sealed: false, reason: "command file vanished" };
1107
+ if (f.kind !== "file")
1108
+ return { sealed: false, reason: `command file is a ${f.kind}, not a regular file` };
1109
+ if (writableBy(seal, f))
1110
+ return {
1111
+ sealed: false,
1112
+ reason: `command file is writable by uid ${seal.euid} (owner ${f.uid}, mode ${f.mode.toString(8)})`,
1113
+ };
1114
+ return { sealed: true };
1115
+ }
1116
+
1117
+ /**
1118
+ * The identity of one version of an operator command file — the key the
1119
+ * handled-ledger is written under. The operator's directory is not the
1120
+ * daemon's to archive from, so "handled" has to be remembered; remembering the
1121
+ * name alone would make every re-used name a one-shot, silently ignoring a
1122
+ * second write under the same name. Inode + mtime + size change on every
1123
+ * rewrite that matters — the operator's tools land files by tmp+mv, a new
1124
+ * inode each time — and stay put across the sweeps that re-see an unchanged
1125
+ * file. `null` when the file is gone.
1126
+ */
1127
+ export function operatorCommandVersion(path: string): string | null {
1128
+ try {
1129
+ return commandVersionOf(path, statSync(path, { bigint: true }));
1130
+ } catch {
1131
+ return null;
1132
+ }
1133
+ }
1134
+
1135
+ function commandVersionOf(path: string, s: { ino: bigint; mtimeNs: bigint; size: bigint }): string {
1136
+ return `${basename(path)}:${s.ino}:${s.mtimeNs}:${s.size}`;
1137
+ }
1138
+
1139
+ /** `dev:ino` of the object AT `path` (a symlink as itself), `null` when absent. */
1140
+ function fileIdentityAt(path: string): string | null {
1141
+ try {
1142
+ const s = lstatSync(path, { bigint: true });
1143
+ return `${s.dev}:${s.ino}`;
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+
1149
+ /** The inode at `path` — what `fs.watch` binds to — or `null` when nothing is there. */
1150
+ function inodeAt(path: string): bigint | null {
1151
+ try {
1152
+ return statSync(path, { bigint: true }).ino;
1153
+ } catch {
1154
+ return null;
1155
+ }
1156
+ }
1157
+
1158
+ export interface CommandWatcherOptions {
1159
+ /** The directory served. Session dirs: results land in <dir>/results and
1160
+ * handled files move to <dir>/processed. */
1161
+ dir: string;
1162
+ /** Tier enforcement for this directory. Absent ⇒ no tier check. */
1163
+ tier?: CommandTierGate;
1164
+ /**
1165
+ * Serve `dir` as the operator's directory. The watcher then never
1166
+ * creates it (a daemon-made directory would carry the daemon's uid and be
1167
+ * no seal), never renames out of it (it cannot), records handled files in
1168
+ * the store instead, stamps `issuer: "operator"` only on files the seal
1169
+ * vouches for, and writes results beside the session dir's — into
1170
+ * `<parent>/results` — so the operator's clients read them from one place.
1171
+ */
1172
+ operator?: OperatorSeal;
1173
+ /** Names this directory in each command's context `file`
1174
+ * (`<scope>/<name>`), so a same-named file in another served directory is a
1175
+ * different delivery to a handler's idempotence key. Absent ⇒ the bare name. */
1176
+ scope?: string;
1177
+ deps: CommandDeps;
1178
+ store: StateStore;
1179
+ now?: () => Date;
1180
+ /** Partial-write tolerance: re-read a file that fails to parse. */
1181
+ parseAttempts?: number;
1182
+ parseRetryMs?: number;
1183
+ /** Operator dir only: re-sweep (and attach to a directory that appeared
1184
+ * after start) this often. 0 disables the timer. Default 15 s. */
1185
+ sweepIntervalMs?: number;
1186
+ log?: (line: string) => void;
1187
+ }
1188
+
1189
+ export interface CommandResultFile extends FileOutcome {
1190
+ command: string | null;
1191
+ file: string;
1192
+ handled_at: string;
1193
+ /** Who the watcher decided the writer was. */
1194
+ issuer: CommandIssuer;
1195
+ }
1196
+
1197
+ // These handlers own richer durable recovery (filing reconciles exact GitHub
1198
+ // markers; lever/process handlers journal their effects; the remaining handlers
1199
+ // use their own delivery key). Pending recovery must reach those handlers.
1200
+ const HANDLER_RECOVERY_COMMANDS = new Set<Command["command"]>([
1201
+ "file-improvement-pr",
1202
+ "file-issue",
1203
+ "pause-pr",
1204
+ "drain",
1205
+ "restart-session",
1206
+ "slack_reply",
1207
+ "lessons-off",
1208
+ "lessons-on",
1209
+ "suppress-lesson",
1210
+ "unsuppress-lesson",
1211
+ ]);
1212
+
1213
+ export class CommandWatcher {
1214
+ private readonly dir: string;
1215
+ private readonly resultsDir: string;
1216
+ private readonly processedDir: string;
1217
+ private readonly now: () => Date;
1218
+ private readonly log: (line: string) => void;
1219
+ private watcher: FSWatcher | null = null;
1220
+ /** The inode `watcher` is bound to — `fs.watch` follows the inode, not the path. */
1221
+ private watchedIno: bigint | null = null;
1222
+ private parentWatcher: FSWatcher | null = null;
1223
+ private sweepTimer: NodeJS.Timeout | null = null;
1224
+ private readonly inFlight = new Set<string>();
1225
+ /** Diagnostics only: every sweep still reads/parses the source anew. */
1226
+ private readonly parseFailures = new Map<string, string>();
1227
+ /** Serialises handling so two file events can't race the same caps. */
1228
+ private queue: Promise<void> = Promise.resolve();
1229
+
1230
+ constructor(private readonly opts: CommandWatcherOptions) {
1231
+ this.dir = opts.dir;
1232
+ const resultsRoot = opts.operator ? dirname(opts.dir) : opts.dir;
1233
+ this.resultsDir = join(resultsRoot, "results");
1234
+ this.processedDir = join(opts.dir, "processed");
1235
+ this.now = opts.now ?? (() => new Date());
1236
+ this.log = opts.log ?? ((line) => console.error(line));
1237
+ }
1238
+
1239
+ private ensureDirs(): void {
1240
+ mkdirSync(this.resultsDir, { recursive: true });
1241
+ if (this.opts.operator) return; // never ours to create — see `operator`
1242
+ mkdirSync(this.dir, { recursive: true });
1243
+ mkdirSync(this.processedDir, { recursive: true });
1244
+ }
1245
+
1246
+ /** Startup sweep + fs.watch. Returns once the initial sweep has completed.
1247
+ * The operator directory may not exist yet (the operator's first write
1248
+ * creates it): then the parent is watched for it to appear, and the sweep
1249
+ * timer re-tries the attach as a belt. */
1250
+ async start(): Promise<void> {
1251
+ this.ensureDirs();
1252
+ // Watch FIRST, then sweep on the same queue the watch feeds: the sweep
1253
+ // snapshots the directory and then awaits each handler, so a file a live
1254
+ // session writes after the snapshot must already have a watcher to see
1255
+ // it — a session dir has no periodic sweep, so a file missed here would
1256
+ // wait for a restart. A file seen by both is handled once: the second
1257
+ // visit finds it archived.
1258
+ if (!this.opts.operator) this.attachDirWatch();
1259
+ const initial = this.queue.then(() => this.sweep());
1260
+ this.queue = initial.catch((err) => this.log(`[commands] sweep failed: ${String(err)}`));
1261
+ await initial;
1262
+ const interval = this.opts.sweepIntervalMs ?? 15_000;
1263
+ if (this.opts.operator && interval > 0) {
1264
+ this.sweepTimer = setInterval(() => void this.enqueueSweep(), interval);
1265
+ this.sweepTimer.unref?.();
1266
+ }
1267
+ }
1268
+
1269
+ /** Watch the served directory when it exists. True once watching. */
1270
+ private attachDirWatch(): boolean {
1271
+ if (this.watcher) return true;
1272
+ const ino = inodeAt(this.dir);
1273
+ if (ino === null) return false;
1274
+ this.watcher = watch(this.dir, (_event, filename) => {
1275
+ if (!filename) {
1276
+ void this.enqueueSweep();
1277
+ return;
1278
+ }
1279
+ const name = filename.toString();
1280
+ if (!name.endsWith(".json")) return;
1281
+ void this.enqueue(join(this.dir, name));
1282
+ });
1283
+ this.watchedIno = ino;
1284
+ this.parentWatcher?.close();
1285
+ this.parentWatcher = null;
1286
+ return true;
1287
+ }
1288
+
1289
+ private detachDirWatch(): void {
1290
+ this.watcher?.close();
1291
+ this.watcher = null;
1292
+ this.watchedIno = null;
1293
+ }
1294
+
1295
+ /**
1296
+ * Operator dir only: keep the watch on whatever directory is AT the path
1297
+ * now. `fs.watch` binds to an inode, and the parent `commands/` is the
1298
+ * daemon's — so a session can rename the sealed directory away and put
1299
+ * another in its place, and a watcher left on the old inode would see none
1300
+ * of the new one's files. Every sweep re-checks; a directory that is gone
1301
+ * or replaced drops the old watch, a present one is attached, and an absent
1302
+ * one leaves the parent watched for it to appear.
1303
+ */
1304
+ private syncDirWatch(): void {
1305
+ if (this.watcher && inodeAt(this.dir) !== this.watchedIno) this.detachDirWatch();
1306
+ if (!this.attachDirWatch() && !this.parentWatcher) {
1307
+ this.parentWatcher = watch(dirname(this.dir), (_event, filename) => {
1308
+ if (filename?.toString() === basename(this.dir)) void this.enqueueSweep();
1309
+ });
1310
+ }
1311
+ }
1312
+
1313
+ stop(): void {
1314
+ this.detachDirWatch();
1315
+ this.parentWatcher?.close();
1316
+ this.parentWatcher = null;
1317
+ if (this.sweepTimer) clearInterval(this.sweepTimer);
1318
+ this.sweepTimer = null;
1319
+ }
1320
+
1321
+ /** Handle every pending <dir>/*.json once. */
1322
+ async sweep(): Promise<void> {
1323
+ this.ensureDirs();
1324
+ if (this.opts.operator) this.syncDirWatch();
1325
+ if (!existsSync(this.dir)) return;
1326
+ const names = readdirSync(this.dir, { withFileTypes: true })
1327
+ .filter((e) => e.isFile() && e.name.endsWith(".json"))
1328
+ .map((e) => e.name)
1329
+ .sort();
1330
+ for (const name of names) await this.handleFile(join(this.dir, name));
1331
+ }
1332
+
1333
+ private enqueueSweep(): Promise<void> {
1334
+ this.queue = this.queue
1335
+ .then(() => this.sweep())
1336
+ .catch((err) => this.log(`[commands] sweep failed: ${String(err)}`));
1337
+ return this.queue;
1338
+ }
1339
+
1340
+ private enqueue(path: string): Promise<void> {
1341
+ this.queue = this.queue
1342
+ .then(async () => {
1343
+ await this.handleFile(path);
1344
+ })
1345
+ .catch((err) => this.log(`[commands] ${path}: ${String(err)}`));
1346
+ return this.queue;
1347
+ }
1348
+
1349
+ /**
1350
+ * Read and parse one command file. `identity` names the file object the
1351
+ * parsed bytes came from (`dev:ino` of the open descriptor) so the operator
1352
+ * seal can be checked against THOSE bytes rather than against whatever
1353
+ * stands at the path a moment earlier or later (the parent `commands/` is
1354
+ * the daemon's, so a session can swap the directory between two syscalls).
1355
+ * `version` is the handled-ledger key of those same bytes
1356
+ * (`operatorCommandVersion`'s), so a file replaced during a parse retry is
1357
+ * recorded as the version that ran, not the one first seen. The descriptor
1358
+ * is stat'ed before AND after the read, and a read the two disagree on (a
1359
+ * writer's write landed in between) is retried like a partial one — so the
1360
+ * version always names the bytes parsed. `text` is those bytes; a failed
1361
+ * read still returns the last text it read, when it read one. The
1362
+ * operator directory is opened without following a symlink.
1363
+ */
1364
+ private async readCommandFile(
1365
+ path: string,
1366
+ ): Promise<
1367
+ | { ok: true; value: unknown; identity: string; version: string; text: string }
1368
+ | { ok: false; reason: string; version: string | null; text: string | null }
1369
+ > {
1370
+ const attempts = this.opts.parseAttempts ?? 3;
1371
+ const delay = this.opts.parseRetryMs ?? 40;
1372
+ const flags = this.opts.operator
1373
+ ? fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW
1374
+ : fsConstants.O_RDONLY;
1375
+ let lastError = "unreadable";
1376
+ let lastVersion: string | null = null;
1377
+ let lastText: string | null = null;
1378
+ for (let i = 0; i < attempts; i++) {
1379
+ if (!existsSync(path))
1380
+ return { ok: false, reason: "file disappeared", version: null, text: null };
1381
+ try {
1382
+ const fd = openSync(path, flags);
1383
+ let text: string;
1384
+ let identity: string;
1385
+ let version: string;
1386
+ let stable: boolean;
1387
+ try {
1388
+ const st = fstatSync(fd, { bigint: true });
1389
+ identity = `${st.dev}:${st.ino}`;
1390
+ text = readFileSync(fd, "utf8");
1391
+ version = commandVersionOf(path, fstatSync(fd, { bigint: true }));
1392
+ stable = version === commandVersionOf(path, st);
1393
+ } finally {
1394
+ closeSync(fd);
1395
+ }
1396
+ lastVersion = version;
1397
+ lastText = text;
1398
+ if (!stable) {
1399
+ lastError = "changed while it was read";
1400
+ } else if (text.trim().length === 0) {
1401
+ lastError = "empty file";
1402
+ } else {
1403
+ return { ok: true, value: JSON.parse(text) as unknown, identity, version, text };
1404
+ }
1405
+ } catch (err) {
1406
+ // A partially-written file is expected transiently — retry before rejecting.
1407
+ lastError = (err as Error).message;
1408
+ }
1409
+ if (i < attempts - 1) await new Promise((r) => setTimeout(r, delay));
1410
+ }
1411
+ return {
1412
+ ok: false,
1413
+ reason: `malformed JSON: ${lastError}`,
1414
+ version: lastVersion,
1415
+ text: lastText,
1416
+ };
1417
+ }
1418
+
1419
+ /**
1420
+ * Who wrote this file, as far as the daemon can PROVE it. A session
1421
+ * directory's writer is a session. The operator directory's writer is the
1422
+ * operator only when the seal holds; a file there without it is served as a
1423
+ * session's — loudly, with the reason in the ledger — so an unsealed channel
1424
+ * degrades to the bounded side and never to the unbounded one.
1425
+ */
1426
+ private issuerFor(path: string, readIdentity: string | null): CommandIssuer {
1427
+ const seal = this.opts.operator;
1428
+ if (!seal) return "session";
1429
+ // Checked AFTER the read, and then bound to it: the file at the path must
1430
+ // still be the very object whose bytes were parsed. Otherwise a directory
1431
+ // swapped in for the read and swapped back for the seal check would lend
1432
+ // its bytes the operator's authority.
1433
+ let verdict = operatorSealVerdict(seal, this.dir, path);
1434
+ if (verdict.sealed && readIdentity !== null && fileIdentityAt(path) !== readIdentity)
1435
+ verdict = { sealed: false, reason: "command file replaced while it was read" };
1436
+ if (verdict.sealed) return "operator";
1437
+ this.opts.store.logEvent("operator_channel_unsealed", null, {
1438
+ file: basename(path),
1439
+ reason: verdict.reason,
1440
+ });
1441
+ this.log(
1442
+ `[commands] ${basename(path)}: operator channel NOT sealed (${verdict.reason}) — served as a session's command`,
1443
+ );
1444
+ return "session";
1445
+ }
1446
+
1447
+ /** Validate + execute one command file, writing its result file. */
1448
+ async handleFile(path: string): Promise<CommandResultFile | null> {
1449
+ const name = basename(path, ".json");
1450
+ if (this.inFlight.has(path)) return null;
1451
+ if (!existsSync(path)) return null;
1452
+ // An operator file cannot be archived away (the directory is not ours to
1453
+ // write), so the store remembers what was handled across restarts — by
1454
+ // file VERSION, so a name written again with new content is a new command
1455
+ // and an unchanged file re-seen by the sweep is not.
1456
+ const seen = this.opts.operator ? operatorCommandVersion(path) : null;
1457
+ if (this.opts.operator) {
1458
+ if (seen === null) return null;
1459
+ if (this.opts.store.operatorCommandHandled(seen)) return null;
1460
+ }
1461
+ this.inFlight.add(path);
1462
+ try {
1463
+ this.ensureDirs();
1464
+ const read = await this.readCommandFile(path);
1465
+ if (!read.ok && read.reason === "file disappeared") return null;
1466
+ // The ledger records the version whose bytes were parsed — a file
1467
+ // replaced during a parse retry is that new version.
1468
+ const version = this.opts.operator ? (read.version ?? seen) : null;
1469
+ if (version !== null && version !== seen && this.opts.store.operatorCommandHandled(version))
1470
+ return null;
1471
+ if (!read.ok) {
1472
+ const fingerprint = createHash("sha256")
1473
+ .update(JSON.stringify([read.version, read.text, read.reason]))
1474
+ .digest("hex");
1475
+ if (this.parseFailures.get(path) === fingerprint) return null;
1476
+ const result = this.finish(
1477
+ path,
1478
+ name,
1479
+ version,
1480
+ read.text,
1481
+ null,
1482
+ this.issuerFor(path, null),
1483
+ { ok: false, reason: `${read.reason}; file retained for a later completed write` },
1484
+ true,
1485
+ true,
1486
+ );
1487
+ this.parseFailures.set(path, fingerprint);
1488
+ if (this.parseFailures.size > 256) {
1489
+ const oldest = this.parseFailures.keys().next().value;
1490
+ if (oldest !== undefined) this.parseFailures.delete(oldest);
1491
+ }
1492
+ return result;
1493
+ }
1494
+ this.parseFailures.delete(path);
1495
+ const issuer = this.issuerFor(path, read.identity);
1496
+ const validation = validateCommand(read.value, { issuer });
1497
+ if (!validation.ok) {
1498
+ const attempted = (read.value as { command?: unknown } | null)?.command;
1499
+ return this.finish(
1500
+ path,
1501
+ name,
1502
+ version,
1503
+ read.text,
1504
+ typeof attempted === "string" ? attempted : null,
1505
+ issuer,
1506
+ { ok: false, reason: validation.reason },
1507
+ true,
1508
+ );
1509
+ }
1510
+ // The Slack belt stops a SESSION mis-filing; a sealed operator file is
1511
+ // the human at the terminal and never rides a Slack turn. An UNSEALED
1512
+ // operator file is a session's and gets the belt like any other.
1513
+ const denial = issuer === "operator" ? null : this.tierCheck(validation.command);
1514
+ if (denial)
1515
+ return this.finish(
1516
+ path,
1517
+ name,
1518
+ version,
1519
+ read.text,
1520
+ validation.command.command,
1521
+ issuer,
1522
+ { ok: false, reason: denial },
1523
+ true,
1524
+ );
1525
+ const deliveryKey = `${this.opts.scope ?? "local"}/${read.identity}/${read.version}`;
1526
+ const receiptKey = createHash("sha256")
1527
+ .update(JSON.stringify([deliveryKey, validation.command]))
1528
+ .digest("hex");
1529
+ const journal = !HANDLER_RECOVERY_COMMANDS.has(validation.command.command);
1530
+ const reserved = !journal || this.opts.store.reserveCommandDelivery(receiptKey);
1531
+ let outcome: FileOutcome;
1532
+ if (!reserved) {
1533
+ const receipt = this.opts.store.commandDeliveryReceipt(receiptKey)!;
1534
+ outcome =
1535
+ receipt.result === null
1536
+ ? {
1537
+ ok: false,
1538
+ reason:
1539
+ "command outcome unknown after interrupted delivery; submit a fresh command to retry",
1540
+ }
1541
+ : (JSON.parse(receipt.result) as FileOutcome);
1542
+ } else {
1543
+ try {
1544
+ outcome = await executeCommand(validation.command, this.opts.deps, {
1545
+ file: this.opts.scope ? `${this.opts.scope}/${name}` : name,
1546
+ delivery_key: deliveryKey,
1547
+ issuer,
1548
+ });
1549
+ if (journal) this.opts.store.settleCommandDelivery(receiptKey, JSON.stringify(outcome));
1550
+ } catch (err) {
1551
+ // The effect may already have happened. Leave its pending receipt
1552
+ // unknown rather than allowing a restart to repeat it.
1553
+ outcome = { ok: false, reason: `handler threw: ${(err as Error).message}` };
1554
+ }
1555
+ }
1556
+ return this.finish(
1557
+ path,
1558
+ name,
1559
+ version,
1560
+ read.text,
1561
+ validation.command.command,
1562
+ issuer,
1563
+ outcome,
1564
+ !outcome.ok,
1565
+ );
1566
+ } finally {
1567
+ this.inFlight.delete(path);
1568
+ }
1569
+ }
1570
+
1571
+ /** Rights check between validation and execution. Returns a reason to deny. */
1572
+ private tierCheck(command: Command): string | null {
1573
+ const gate = this.opts.tier;
1574
+ if (!gate) return null;
1575
+ let origin: OriginRecord | null = null;
1576
+ try {
1577
+ origin = gate.currentOrigin();
1578
+ } catch (err) {
1579
+ // A gate that cannot answer denies — never fails open.
1580
+ return `tier: origin lookup failed (${(err as Error).message})`;
1581
+ }
1582
+ if (gate.dir === "slack") {
1583
+ if (!origin) {
1584
+ return "tier: no live Slack origin for this turn — a tier-checked command needs one (fail closed)";
1585
+ }
1586
+ const auth = authorizeCommand(command, origin);
1587
+ return auth.ok ? null : auth.reason;
1588
+ }
1589
+ // Local dir: only the belt applies. It fires on CAUSE, not on recency —
1590
+ // a daemon-caused turn (`source: "local"`) always passes, and a Slack-caused
1591
+ // one from a non-owner user always refuses, however old the record is.
1592
+ // (`parseOriginRecord` folds the retired tier spelling to "owner", so this
1593
+ // comparison only ever sees the current vocabulary; a Slack origin can
1594
+ // never be "system" — only `writeLocal` stamps that, with source "local".)
1595
+ if (origin && beltRestricts(origin)) {
1596
+ return (
1597
+ `tier: this turn was caused by a Slack message from ${origin.slack_user} (tier ${origin.tier}) — ` +
1598
+ `write '${command.command}' to commands/slack/ so it can be tier-checked`
1599
+ );
1600
+ }
1601
+ return null;
1602
+ }
1603
+
1604
+ private finish(
1605
+ path: string,
1606
+ name: string,
1607
+ /** The operator file's version key (`operatorCommandVersion`); null for a session dir. */
1608
+ version: string | null,
1609
+ /** The bytes that were read; null when no file was read. */
1610
+ readText: string | null,
1611
+ command: string | null,
1612
+ issuer: CommandIssuer,
1613
+ outcome: FileOutcome,
1614
+ loud: boolean,
1615
+ unsettled = false,
1616
+ ): CommandResultFile {
1617
+ const result: CommandResultFile = {
1618
+ ...outcome,
1619
+ command,
1620
+ file: basename(path),
1621
+ handled_at: this.now().toISOString(),
1622
+ issuer,
1623
+ };
1624
+ writeFileSync(join(this.resultsDir, `${name}.result.json`), JSON.stringify(result, null, 2));
1625
+ if (outcome.ok) {
1626
+ this.opts.store.logEvent("command_executed", null, { command, file: result.file, issuer });
1627
+ } else {
1628
+ this.opts.store.logEvent("command_rejected", null, {
1629
+ command,
1630
+ file: result.file,
1631
+ issuer,
1632
+ reason: outcome.reason,
1633
+ });
1634
+ }
1635
+ if (loud)
1636
+ this.log(
1637
+ `[commands] REJECTED ${result.file} (${command ?? "?"}): ${outcome.reason ?? "unknown reason"}`,
1638
+ );
1639
+ // A parse failure is an observation, not a settled delivery: a slow
1640
+ // writer may complete after eager retries. Keep both session and operator
1641
+ // files eligible, even when metadata did not advance between observations.
1642
+ if (unsettled) return result;
1643
+ // Make sure a daemon restart cannot re-execute the file: session dirs
1644
+ // archive it into processed/; the operator dir is not writable by the
1645
+ // daemon, so the store keeps the ledger instead, keyed by this version.
1646
+ if (version !== null) {
1647
+ this.opts.store.markOperatorCommandHandled(version, result.file);
1648
+ } else {
1649
+ let atPath: string | null = null;
1650
+ try {
1651
+ atPath = readFileSync(path, "utf8");
1652
+ } catch {
1653
+ /* gone — nothing to archive or to run */
1654
+ }
1655
+ if (readText !== null && atPath !== null && atPath !== readText) {
1656
+ // A different command took this name while this one ran. It is not
1657
+ // the file this result describes, so it is not archived under it: it
1658
+ // is queued to run next (the watch event it raised found this one in
1659
+ // flight, and a session dir has no periodic sweep to find it later).
1660
+ // The same bytes written again are the same command, already run.
1661
+ this.log(`[commands] ${result.file} was replaced while it ran — running the new one next`);
1662
+ void this.enqueue(path);
1663
+ } else {
1664
+ try {
1665
+ renameSync(path, join(this.processedDir, `${name}.json`));
1666
+ } catch (err) {
1667
+ // A vanished file is fine; any other failure leaves the source
1668
+ // pending, so it is logged with the path and cause.
1669
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT")
1670
+ this.log(
1671
+ `[commands] could not archive ${path}: ${(err as NodeJS.ErrnoException).code ?? (err as Error).message}`,
1672
+ );
1673
+ }
1674
+ }
1675
+ }
1676
+ return result;
1677
+ }
1678
+ }