approval-md 0.1.0 → 0.3.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 (416) hide show
  1. package/README.md +629 -559
  2. package/SPEC.md +99 -24
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +656 -0
  27. package/dist/src/channels/contract.js +200 -7
  28. package/dist/src/channels/contract.js.map +1 -1
  29. package/dist/src/channels/payload-view.d.ts +35 -0
  30. package/dist/src/channels/render-queue.d.ts +149 -0
  31. package/dist/src/channels/tagging.d.ts +196 -0
  32. package/dist/src/channels/telegram.d.ts +1944 -0
  33. package/dist/src/channels/telegram.js +218 -23
  34. package/dist/src/channels/telegram.js.map +1 -1
  35. package/dist/src/channels/web.d.ts +350 -0
  36. package/dist/src/channels/web.js +17 -0
  37. package/dist/src/channels/web.js.map +1 -1
  38. package/dist/src/cli/adapter.d.ts +90 -0
  39. package/dist/src/cli/adapter.js +25 -15
  40. package/dist/src/cli/adapter.js.map +1 -1
  41. package/dist/src/cli/amend.d.ts +59 -0
  42. package/dist/src/cli/amend.js +214 -30
  43. package/dist/src/cli/amend.js.map +1 -1
  44. package/dist/src/cli/args.d.ts +43 -0
  45. package/dist/src/cli/attest.d.ts +50 -0
  46. package/dist/src/cli/attest.js +134 -7
  47. package/dist/src/cli/attest.js.map +1 -1
  48. package/dist/src/cli/audit-card.d.ts +62 -0
  49. package/dist/src/cli/audit.d.ts +59 -0
  50. package/dist/src/cli/channel-telegram.d.ts +879 -0
  51. package/dist/src/cli/channel-telegram.js +311 -13
  52. package/dist/src/cli/channel-telegram.js.map +1 -1
  53. package/dist/src/cli/channel-web.d.ts +131 -0
  54. package/dist/src/cli/channel.d.ts +80 -0
  55. package/dist/src/cli/channel.js +9 -0
  56. package/dist/src/cli/channel.js.map +1 -1
  57. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  58. package/dist/src/cli/codex-bridge.d.ts +819 -0
  59. package/dist/src/cli/codex-bridge.js +1607 -0
  60. package/dist/src/cli/codex-bridge.js.map +1 -0
  61. package/dist/src/cli/codex.d.ts +2 -0
  62. package/dist/src/cli/codex.js +469 -0
  63. package/dist/src/cli/codex.js.map +1 -0
  64. package/dist/src/cli/coverage.d.ts +61 -0
  65. package/dist/src/cli/daemon.d.ts +120 -0
  66. package/dist/src/cli/daemon.js +4 -1
  67. package/dist/src/cli/daemon.js.map +1 -1
  68. package/dist/src/cli/doctor.d.ts +129 -0
  69. package/dist/src/cli/doctor.js +586 -17
  70. package/dist/src/cli/doctor.js.map +1 -1
  71. package/dist/src/cli/env.d.ts +65 -0
  72. package/dist/src/cli/execute.d.ts +202 -0
  73. package/dist/src/cli/execute.js +25 -2
  74. package/dist/src/cli/execute.js.map +1 -1
  75. package/dist/src/cli/exit-codes.d.ts +73 -0
  76. package/dist/src/cli/feedback.d.ts +60 -0
  77. package/dist/src/cli/gate-window.d.ts +40 -0
  78. package/dist/src/cli/gate.d.ts +68 -0
  79. package/dist/src/cli/git-scope.d.ts +190 -0
  80. package/dist/src/cli/gloss-attach.d.ts +85 -0
  81. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  82. package/dist/src/cli/gloss-codex.d.ts +24 -0
  83. package/dist/src/cli/gloss-options.d.ts +42 -0
  84. package/dist/src/cli/gloss.d.ts +265 -0
  85. package/dist/src/cli/help.d.ts +107 -0
  86. package/dist/src/cli/help.js +320 -93
  87. package/dist/src/cli/help.js.map +1 -1
  88. package/dist/src/cli/hook-codex.d.ts +126 -0
  89. package/dist/src/cli/hook-codex.js +226 -0
  90. package/dist/src/cli/hook-codex.js.map +1 -0
  91. package/dist/src/cli/hook.d.ts +787 -0
  92. package/dist/src/cli/hook.js +1235 -181
  93. package/dist/src/cli/hook.js.map +1 -1
  94. package/dist/src/cli/import.d.ts +35 -0
  95. package/dist/src/cli/import.js +1 -1
  96. package/dist/src/cli/import.js.map +1 -1
  97. package/dist/src/cli/init.d.ts +84 -0
  98. package/dist/src/cli/init.js +2 -2
  99. package/dist/src/cli/init.js.map +1 -1
  100. package/dist/src/cli/instructions.d.ts +23 -0
  101. package/dist/src/cli/journal.d.ts +41 -0
  102. package/dist/src/cli/log-advance.d.ts +287 -0
  103. package/dist/src/cli/log-advance.js +102 -11
  104. package/dist/src/cli/log-advance.js.map +1 -1
  105. package/dist/src/cli/log-anchor.d.ts +176 -0
  106. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  107. package/dist/src/cli/log-sync.d.ts +243 -0
  108. package/dist/src/cli/log-verbs.d.ts +16 -0
  109. package/dist/src/cli/log-verbs.js +7 -1
  110. package/dist/src/cli/log-verbs.js.map +1 -1
  111. package/dist/src/cli/long-help.d.ts +70 -0
  112. package/dist/src/cli/main.d.ts +77 -0
  113. package/dist/src/cli/main.js +159 -7
  114. package/dist/src/cli/main.js.map +1 -1
  115. package/dist/src/cli/mcp.d.ts +52 -0
  116. package/dist/src/cli/paths.d.ts +56 -0
  117. package/dist/src/cli/payload.d.ts +58 -0
  118. package/dist/src/cli/policy-apply.d.ts +195 -0
  119. package/dist/src/cli/policy-apply.js +573 -0
  120. package/dist/src/cli/policy-apply.js.map +1 -0
  121. package/dist/src/cli/policy.d.ts +43 -0
  122. package/dist/src/cli/policy.js +14 -1
  123. package/dist/src/cli/policy.js.map +1 -1
  124. package/dist/src/cli/preflight.d.ts +501 -0
  125. package/dist/src/cli/preflight.js +689 -45
  126. package/dist/src/cli/preflight.js.map +1 -1
  127. package/dist/src/cli/progress.d.ts +78 -0
  128. package/dist/src/cli/prompt.d.ts +209 -0
  129. package/dist/src/cli/quickstart.d.ts +46 -0
  130. package/dist/src/cli/quickstart.js +297 -0
  131. package/dist/src/cli/quickstart.js.map +1 -0
  132. package/dist/src/cli/records.d.ts +34 -0
  133. package/dist/src/cli/render.d.ts +22 -0
  134. package/dist/src/cli/sandbox.d.ts +51 -0
  135. package/dist/src/cli/sandbox.js +17 -1
  136. package/dist/src/cli/sandbox.js.map +1 -1
  137. package/dist/src/cli/scaffold.d.ts +79 -0
  138. package/dist/src/cli/scaffold.js +1 -1
  139. package/dist/src/cli/setup-adapter.d.ts +137 -0
  140. package/dist/src/cli/setup-adapter.js +38 -4
  141. package/dist/src/cli/setup-adapter.js.map +1 -1
  142. package/dist/src/cli/setup-channel.d.ts +126 -0
  143. package/dist/src/cli/setup-channel.js +28 -1
  144. package/dist/src/cli/setup-channel.js.map +1 -1
  145. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  146. package/dist/src/cli/setup-common.d.ts +277 -0
  147. package/dist/src/cli/setup-common.js +3 -2
  148. package/dist/src/cli/setup-common.js.map +1 -1
  149. package/dist/src/cli/setup-flow.d.ts +287 -0
  150. package/dist/src/cli/setup-service.d.ts +96 -0
  151. package/dist/src/cli/setup.d.ts +204 -0
  152. package/dist/src/cli/setup.js +94 -2
  153. package/dist/src/cli/setup.js.map +1 -1
  154. package/dist/src/cli/style.d.ts +320 -0
  155. package/dist/src/cli/token.d.ts +39 -0
  156. package/dist/src/cli/up.d.ts +155 -0
  157. package/dist/src/cli/up.js +119 -53
  158. package/dist/src/cli/up.js.map +1 -1
  159. package/dist/src/cli/usage.d.ts +37 -0
  160. package/dist/src/cli/values.d.ts +40 -0
  161. package/dist/src/cli/values.js +3 -4
  162. package/dist/src/cli/values.js.map +1 -1
  163. package/dist/src/cli/vault.d.ts +59 -0
  164. package/dist/src/cli/vault.js +2 -2
  165. package/dist/src/cli/vault.js.map +1 -1
  166. package/dist/src/cli/verb-registry.d.ts +76 -0
  167. package/dist/src/cli/verb-registry.js +344 -11
  168. package/dist/src/cli/verb-registry.js.map +1 -1
  169. package/dist/src/cli/wordmark.d.ts +31 -0
  170. package/dist/src/cli/wordmark.js +2 -2
  171. package/dist/src/codex/broker.d.ts +229 -0
  172. package/dist/src/codex/broker.js +548 -0
  173. package/dist/src/codex/broker.js.map +1 -0
  174. package/dist/src/codex/doctor.d.ts +13 -0
  175. package/dist/src/codex/doctor.js +41 -0
  176. package/dist/src/codex/doctor.js.map +1 -0
  177. package/dist/src/codex/manifest.d.ts +49 -0
  178. package/dist/src/codex/manifest.js +103 -0
  179. package/dist/src/codex/manifest.js.map +1 -0
  180. package/dist/src/codex/runner.d.ts +178 -0
  181. package/dist/src/codex/runner.js +231 -0
  182. package/dist/src/codex/runner.js.map +1 -0
  183. package/dist/src/codex/serve.d.ts +56 -0
  184. package/dist/src/codex/serve.js +98 -0
  185. package/dist/src/codex/serve.js.map +1 -0
  186. package/dist/src/codex/templates.d.ts +41 -0
  187. package/dist/src/codex/templates.js +319 -0
  188. package/dist/src/codex/templates.js.map +1 -0
  189. package/dist/src/codex/trust.d.ts +19 -0
  190. package/dist/src/codex/trust.js +183 -0
  191. package/dist/src/codex/trust.js.map +1 -0
  192. package/dist/src/codex/workspace-commit.d.ts +219 -0
  193. package/dist/src/codex/workspace-commit.js +549 -0
  194. package/dist/src/codex/workspace-commit.js.map +1 -0
  195. package/dist/src/codex/workspace-plan.d.ts +131 -0
  196. package/dist/src/codex/workspace-plan.js +561 -0
  197. package/dist/src/codex/workspace-plan.js.map +1 -0
  198. package/dist/src/core/actor.d.ts +2 -0
  199. package/dist/src/core/actor.js +5 -0
  200. package/dist/src/core/actor.js.map +1 -0
  201. package/dist/src/core/advance-cycle.d.ts +221 -0
  202. package/dist/src/core/advance-cycle.js +66 -2
  203. package/dist/src/core/advance-cycle.js.map +1 -1
  204. package/dist/src/core/agents-md.d.ts +278 -0
  205. package/dist/src/core/agents-md.js +33 -31
  206. package/dist/src/core/agents-md.js.map +1 -1
  207. package/dist/src/core/apply-patch.d.ts +49 -0
  208. package/dist/src/core/apply-patch.js +266 -0
  209. package/dist/src/core/apply-patch.js.map +1 -0
  210. package/dist/src/core/attest.d.ts +635 -0
  211. package/dist/src/core/attest.js +326 -4
  212. package/dist/src/core/attest.js.map +1 -1
  213. package/dist/src/core/audit.d.ts +510 -0
  214. package/dist/src/core/audit.js +13 -0
  215. package/dist/src/core/audit.js.map +1 -1
  216. package/dist/src/core/budgets.d.ts +238 -0
  217. package/dist/src/core/channel-owner.d.ts +213 -0
  218. package/dist/src/core/channel-owner.js +358 -0
  219. package/dist/src/core/channel-owner.js.map +1 -0
  220. package/dist/src/core/checkpoint.d.ts +500 -0
  221. package/dist/src/core/child-env.d.ts +88 -0
  222. package/dist/src/core/clock.d.ts +52 -0
  223. package/dist/src/core/command-class.d.ts +697 -0
  224. package/dist/src/core/command-class.js +713 -25
  225. package/dist/src/core/command-class.js.map +1 -1
  226. package/dist/src/core/commit-guard.d.ts +272 -0
  227. package/dist/src/core/commit-guard.js +424 -0
  228. package/dist/src/core/commit-guard.js.map +1 -0
  229. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  230. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  231. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  232. package/dist/src/core/coverage.d.ts +217 -0
  233. package/dist/src/core/credential-spec.d.ts +72 -0
  234. package/dist/src/core/daemon-actor.d.ts +45 -0
  235. package/dist/src/core/daemon-actor.js +54 -0
  236. package/dist/src/core/daemon-actor.js.map +1 -0
  237. package/dist/src/core/dark-session.d.ts +432 -0
  238. package/dist/src/core/dark-session.js +266 -82
  239. package/dist/src/core/dark-session.js.map +1 -1
  240. package/dist/src/core/decision-refusal.d.ts +206 -0
  241. package/dist/src/core/decision-refusal.js +24 -2
  242. package/dist/src/core/decision-refusal.js.map +1 -1
  243. package/dist/src/core/env-file.d.ts +455 -0
  244. package/dist/src/core/env-file.js +60 -1
  245. package/dist/src/core/env-file.js.map +1 -1
  246. package/dist/src/core/execute.d.ts +871 -0
  247. package/dist/src/core/execute.js +59 -8
  248. package/dist/src/core/execute.js.map +1 -1
  249. package/dist/src/core/frontmatter.d.ts +78 -0
  250. package/dist/src/core/gate-window.d.ts +312 -0
  251. package/dist/src/core/gate.d.ts +1449 -0
  252. package/dist/src/core/gate.js +149 -14
  253. package/dist/src/core/gate.js.map +1 -1
  254. package/dist/src/core/gesture-refusal.d.ts +166 -0
  255. package/dist/src/core/gesture-refusal.js +188 -0
  256. package/dist/src/core/gesture-refusal.js.map +1 -0
  257. package/dist/src/core/git-run.d.ts +73 -0
  258. package/dist/src/core/harness-version.d.ts +157 -0
  259. package/dist/src/core/harness-version.js +4 -1
  260. package/dist/src/core/harness-version.js.map +1 -1
  261. package/dist/src/core/harness-wait.d.ts +55 -0
  262. package/dist/src/core/head-retry.d.ts +107 -0
  263. package/dist/src/core/instance.d.ts +310 -0
  264. package/dist/src/core/instance.js +113 -0
  265. package/dist/src/core/instance.js.map +1 -1
  266. package/dist/src/core/intake-limits.d.ts +247 -0
  267. package/dist/src/core/jcs.d.ts +52 -0
  268. package/dist/src/core/journal.d.ts +144 -0
  269. package/dist/src/core/live-draw.d.ts +436 -0
  270. package/dist/src/core/log-reconcile.d.ts +89 -0
  271. package/dist/src/core/log-subscribe.d.ts +36 -0
  272. package/dist/src/core/log-subscribe.js +162 -0
  273. package/dist/src/core/log-subscribe.js.map +1 -0
  274. package/dist/src/core/log.d.ts +316 -0
  275. package/dist/src/core/log.js.map +1 -1
  276. package/dist/src/core/loop.d.ts +274 -0
  277. package/dist/src/core/loop.js +11 -0
  278. package/dist/src/core/loop.js.map +1 -1
  279. package/dist/src/core/md-fence.d.ts +41 -0
  280. package/dist/src/core/money.d.ts +147 -0
  281. package/dist/src/core/payload-census.d.ts +74 -0
  282. package/dist/src/core/payload-store.d.ts +175 -0
  283. package/dist/src/core/payload.d.ts +71 -0
  284. package/dist/src/core/policy-diff.d.ts +292 -0
  285. package/dist/src/core/policy-diff.js +27 -4
  286. package/dist/src/core/policy-diff.js.map +1 -1
  287. package/dist/src/core/policy-expectations.d.ts +199 -0
  288. package/dist/src/core/policy-explain.d.ts +160 -0
  289. package/dist/src/core/policy-explain.js +63 -3
  290. package/dist/src/core/policy-explain.js.map +1 -1
  291. package/dist/src/core/policy-load.d.ts +567 -0
  292. package/dist/src/core/policy-load.js +36 -6
  293. package/dist/src/core/policy-load.js.map +1 -1
  294. package/dist/src/core/policy-match.d.ts +324 -0
  295. package/dist/src/core/policy-match.js +72 -9
  296. package/dist/src/core/policy-match.js.map +1 -1
  297. package/dist/src/core/policy-proposal.d.ts +317 -0
  298. package/dist/src/core/policy-proposal.js +102 -2
  299. package/dist/src/core/policy-proposal.js.map +1 -1
  300. package/dist/src/core/prompt-layout.d.ts +221 -0
  301. package/dist/src/core/protected-path-guard.d.ts +566 -0
  302. package/dist/src/core/protected-path-guard.js +848 -55
  303. package/dist/src/core/protected-path-guard.js.map +1 -1
  304. package/dist/src/core/question-preempted.d.ts +141 -0
  305. package/dist/src/core/question-preempted.js +152 -0
  306. package/dist/src/core/question-preempted.js.map +1 -0
  307. package/dist/src/core/read-scope.d.ts +172 -0
  308. package/dist/src/core/read-scope.js +252 -0
  309. package/dist/src/core/read-scope.js.map +1 -0
  310. package/dist/src/core/registration.d.ts +25 -0
  311. package/dist/src/core/reindex.d.ts +99 -0
  312. package/dist/src/core/sampler.d.ts +313 -0
  313. package/dist/src/core/sandbox.d.ts +371 -0
  314. package/dist/src/core/sandbox.js +190 -1
  315. package/dist/src/core/sandbox.js.map +1 -1
  316. package/dist/src/core/seal.d.ts +165 -0
  317. package/dist/src/core/sender-identity.d.ts +476 -0
  318. package/dist/src/core/sender-identity.js +572 -0
  319. package/dist/src/core/sender-identity.js.map +1 -0
  320. package/dist/src/core/shlex.d.ts +102 -0
  321. package/dist/src/core/shlex.js +159 -0
  322. package/dist/src/core/shlex.js.map +1 -0
  323. package/dist/src/core/state.d.ts +505 -0
  324. package/dist/src/core/task-file.d.ts +185 -0
  325. package/dist/src/core/telegram-config.d.ts +93 -0
  326. package/dist/src/core/token.d.ts +409 -0
  327. package/dist/src/core/token.js +21 -38
  328. package/dist/src/core/token.js.map +1 -1
  329. package/dist/src/core/validate.d.ts +138 -0
  330. package/dist/src/core/values.d.ts +147 -0
  331. package/dist/src/core/values.js +36 -1
  332. package/dist/src/core/values.js.map +1 -1
  333. package/dist/src/core/vault.d.ts +291 -0
  334. package/dist/src/core/verified-snapshot.d.ts +204 -0
  335. package/dist/src/core/verify.d.ts +336 -0
  336. package/dist/src/core/version.d.ts +8 -0
  337. package/dist/src/core/wysiwys.d.ts +370 -0
  338. package/dist/src/daemon/advance-child.d.ts +39 -0
  339. package/dist/src/daemon/advance.d.ts +476 -0
  340. package/dist/src/daemon/advance.js +25 -4
  341. package/dist/src/daemon/advance.js.map +1 -1
  342. package/dist/src/daemon/audit.d.ts +87 -0
  343. package/dist/src/daemon/daemon.d.ts +1180 -0
  344. package/dist/src/daemon/daemon.js +9 -0
  345. package/dist/src/daemon/daemon.js.map +1 -1
  346. package/dist/src/daemon/dark-session.d.ts +64 -0
  347. package/dist/src/daemon/draw-child.d.ts +36 -0
  348. package/dist/src/daemon/draw.d.ts +154 -0
  349. package/dist/src/daemon/git-evidence.d.ts +173 -0
  350. package/dist/src/daemon/git-evidence.js +1 -1
  351. package/dist/src/daemon/projection.d.ts +180 -0
  352. package/dist/src/daemon/prune.d.ts +207 -0
  353. package/dist/src/mcp/http.d.ts +113 -0
  354. package/dist/src/mcp/server.d.ts +265 -0
  355. package/dist/src/mcp/server.js +17 -1
  356. package/dist/src/mcp/server.js.map +1 -1
  357. package/docs/adapter-api.md +106 -0
  358. package/docs/cli-reference.md +1316 -63
  359. package/docs/codex-enforced-session.md +103 -0
  360. package/docs/codex-workspace-broker.md +118 -0
  361. package/package.json +14 -2
  362. package/schema/codex-instance.schema.json +82 -0
  363. package/schema/event.schema.json +539 -9
  364. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  365. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  366. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  367. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  368. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  370. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  371. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  372. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  373. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  374. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  375. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  376. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  377. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  378. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  379. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  380. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  381. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  382. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  383. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  384. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  385. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  386. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  387. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  388. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  389. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  390. package/schema/fixtures/policy/valid/canonical.json +1 -1
  391. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  392. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  393. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  394. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  395. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  396. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  397. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  398. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  399. package/schema/fixtures/values/invalid/version-float.json +1 -0
  400. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  401. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  402. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  403. package/schema/fixtures/values/valid/full.json +5 -7
  404. package/schema/fixtures/values/valid/minimal.json +1 -1
  405. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  406. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  407. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  408. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  409. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  410. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  411. package/schema/fixtures/values-md/valid/absent.md +1 -1
  412. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  413. package/schema/policy.schema.json +75 -3
  414. package/schema/values.schema.json +7 -11
  415. package/templates/codex/README.md +9 -0
  416. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -0,0 +1,879 @@
1
+ /**
2
+ * `approval channel telegram listen | health` — the runtime half of the
3
+ * Telegram channel (SPEC.md §10.3, APRV-26).
4
+ *
5
+ * As everywhere else in this CLI, **no logic lives here**. The rendering and
6
+ * the Bot API are `channels/telegram.ts`; turning a button press into an event
7
+ * is `channels/contract.ts`'s `recordChannelDecision`, which calls the
8
+ * human-only `decide()` in `core/gate.ts`. This file resolves configuration,
9
+ * builds the pending queue, wires the two together, and chooses an exit code.
10
+ *
11
+ * Three things it does that the channel deliberately cannot:
12
+ *
13
+ * 1. **It reads the environment.** `APPROVAL_TG_TOKEN` and `APPROVAL_TG_CHAT`
14
+ * are read here and passed to the channel as values (SPEC.md §5.1: policy
15
+ * carries the env-var *names*, never the secrets). Nothing in `channels/`
16
+ * touches `process.env`.
17
+ * 2. **It declares the identity the process itself acts under.** `--as` /
18
+ * `APPROVAL_HUMAN` is what this listener is, and it is what a decision is
19
+ * recorded as whenever the policy maps no sender for this channel: SPEC.md
20
+ * §11's config-declared identity, where the trust boundary is the local
21
+ * machine and everyone who can reach the configured chat can approve as that
22
+ * actor. Since APRV-324 a policy MAY map Telegram account ids to approvers,
23
+ * and then the decision is recorded against the person the operator attested
24
+ * the tapping account to, and an unmapped account is refused rather than
25
+ * recorded as this listener. Either way the choosing happens in
26
+ * `channels/contract.ts` against the attested policy, never here and never
27
+ * in the channel. Both settings are stated in `--help` because an operator
28
+ * has to be able to see which one they are in without reading the source.
29
+ * 3. **It holds the token.** A grant mints a single-use execution token;
30
+ * `recordChannelDecision` returns it to *this* handler, which prints it on
31
+ * **stdout** and never hands it back to the channel. It is never sent to
32
+ * Telegram — see the module doc of `channels/telegram.ts` for why a chat
33
+ * transcript is not a credential store, and for the flag on that decision.
34
+ *
35
+ * ## Payload material — the store, and why `--payloads` still exists
36
+ *
37
+ * SPEC.md §6.2 records a `payload_hash` in the log and never the bytes, and
38
+ * §10.4 requires a channel to present the full payload for a manual action. So
39
+ * the bytes must come from somewhere the runtime can reach. Since APRV-28 that
40
+ * somewhere is the payload store beside the log (`.approval/payloads/`, written
41
+ * by `approval request --payload`), and a listener ordinarily needs no payload
42
+ * flag at all. `--payloads` remains an override for bytes an operator holds
43
+ * elsewhere: a JSON file mapping action key to that action's payload value,
44
+ * consulted before the store. The tagger
45
+ * (`channels/tagging.ts`) re-hashes whatever it is given and refuses anything
46
+ * that does not match the recorded binding, so a wrong or stale file cannot put
47
+ * different bytes in front of an approver than the token will execute — it
48
+ * produces a visible skip instead. Requests whose material is missing are
49
+ * reported on stderr and NOT delivered: a manual request rendered without its
50
+ * payload would be exactly the §10.4 violation the contract refuses.
51
+ *
52
+ * ## Dispatch: where it lives, and why it lives here (APRV-55) — flagged
53
+ *
54
+ * SPEC.md §10.2 lists "dispatches channel notifications" among the daemon's
55
+ * jobs. At v0.1 the reference runtime performs that dispatch **in this
56
+ * listener**, on every poll cycle, and the placement is deliberate:
57
+ *
58
+ * 1. The listener already holds the channel connection (the bot token, the
59
+ * chat id) and the approver identity. The daemon holds neither, and giving
60
+ * it either would put a credential and a human identity into a process
61
+ * whose job is to read files and append events.
62
+ * 2. The daemon is the sole writer of the log; dispatch appends nothing. Moving
63
+ * a read-and-send out of the daemon costs the single-writer stance nothing,
64
+ * because dispatch was never a write.
65
+ * 3. A network round-trip inside the daemon's tick couples the projection loop
66
+ * to Telegram's availability. A slow Bot API would delay TTL expiry and
67
+ * write-back, which are the daemon's actual obligations.
68
+ *
69
+ * So this is an implementation placement, not a change to the daemon's stated
70
+ * role: a later build MAY move dispatch into the daemon (or a supervisor) with
71
+ * no change to the log or to any event shape. SPEC.md §10.3 records the same.
72
+ *
73
+ * ### The cycle
74
+ *
75
+ * {@link dispatchPending} runs before every `getUpdates` — the startup send and
76
+ * every later cycle are the same call with the same state, the startup one
77
+ * merely finding an empty delivered set. Each call **re-derives** the pending
78
+ * queue from the verified log ({@link buildPendingQueue}), so which requests
79
+ * are pending is always the log's answer and never this process's memory. A
80
+ * request appended while the listener is running is therefore delivered on the
81
+ * next cycle, without a restart; a request that was decided or whose TTL lapsed
82
+ * simply stops appearing in the derivation and is never sent.
83
+ *
84
+ * What *is* remembered, and only in {@link DispatchState} for this process's
85
+ * lifetime, is which action keys this listener has already put on the phone.
86
+ * Losing that memory (a restart, a crash) re-sends everything still pending:
87
+ * a duplicate on the phone, never silence. That direction is the whole design
88
+ * (SPEC.md §10.3: channels hold no state that is a source of truth).
89
+ *
90
+ * APRV-196 made that re-send legible rather than rarer. The first batch a
91
+ * process sends is preceded by one banner naming how many are coming, the
92
+ * copies already in the chat keep working (`actionRefOf` in
93
+ * `channels/telegram.ts` resolves their buttons to the same request), and the
94
+ * bookkeeping above is pruned as requests settle and age out instead of growing
95
+ * for the life of a listener that `approval up` keeps running for weeks.
96
+ *
97
+ * ### Send failures
98
+ *
99
+ * A key that fails to send stays undelivered, so the next cycle retries it.
100
+ * There is **no attempt limit**: giving up would turn a transient outage into a
101
+ * pending request no human ever sees, which is the one failure this project
102
+ * exists to prevent. The retry rate is bounded by the poll cycle itself (the
103
+ * long-poll timeout, or the channel's doubling backoff after a poll error), and
104
+ * the stderr warnings are throttled after {@link DISPATCH_LOUD_ATTEMPTS}
105
+ * consecutive failures for one key so a long outage cannot bury the terminal.
106
+ * The one exception is the **startup** dispatch, which still exits non-zero on
107
+ * a send failure: an operator who has just mistyped a chat id or a token should
108
+ * learn it immediately rather than watch a listener retry forever.
109
+ */
110
+ import type { DecideOptions } from "../core/gate.js";
111
+ import { type ChannelRequest, type DeliveryId } from "../channels/contract.js";
112
+ import { type ChannelTagRefusalCode, type TagOptions } from "../channels/tagging.js";
113
+ import { type CheckpointTap } from "./checkpoint-tap.js";
114
+ import { TelegramChannel, type CheckpointTapResponse, type ReviewCard, type ReviewTapResponse, type TelegramCommand, type TelegramTerminalState } from "../channels/telegram.js";
115
+ import { type TelegramDelivery } from "../core/telegram-config.js";
116
+ import { type InstanceFinding } from "../core/instance.js";
117
+ import { type ChannelSender } from "../core/sender-identity.js";
118
+ import { type ParsedFlags } from "./args.js";
119
+ import { type GlossRunner } from "./gloss.js";
120
+ import { type GlossRunnerFactoryOptions } from "./gloss-options.js";
121
+ import type { Streams } from "./main.js";
122
+ /**
123
+ * The gloss runner this listener will use, as a spreadable fragment (APRV-197).
124
+ *
125
+ * ON unless `--no-gloss`. Two flags rather than one because the pair reads
126
+ * honestly next to `channel cli`, where the default is the other way round:
127
+ * `--gloss` is accepted here (and is simply the default restated) so that one
128
+ * command line works on both verbs, and `--no-gloss` wins a tie, because the
129
+ * flag that removes a language model from the path should never lose one.
130
+ *
131
+ * A fragment rather than a value so that "no runner" is the ABSENCE of the
132
+ * key. {@link ListenSetup.gloss} being optional is what lets every
133
+ * programmatic caller of `dispatchPending` spawn nothing without saying so.
134
+ *
135
+ * `passphraseEnv` is the name this policy's `vault.passphrase_env` gives, and
136
+ * the only thing the APRV-207 scrub needs from a policy: the subprocess is
137
+ * spawned starved either way, and naming the variable covers the deployment
138
+ * that renamed it out from under the credential prefixes.
139
+ */
140
+ export declare function glossWiring(flags: ParsedFlags, passphraseEnv?: string | null, factories?: Omit<GlossRunnerFactoryOptions, "passphraseEnv">): {
141
+ gloss?: GlossRunner;
142
+ };
143
+ export interface ListenSetup {
144
+ channel: TelegramChannel;
145
+ logPath: string;
146
+ actor: string;
147
+ json: boolean;
148
+ once: boolean;
149
+ gateOptions: DecideOptions;
150
+ tagOptions: TagOptions;
151
+ /**
152
+ * How this listener puts the pending set in front of the approver (APRV-216):
153
+ * `paced`, one question at a time, or `burst`, everything not yet sent on
154
+ * every cycle.
155
+ *
156
+ * REQUIRED rather than defaulted, so that every construction site states
157
+ * which of the two it means. The policy's answer is resolved once, in
158
+ * {@link prepareListen}, and the default that answer falls back to lives in
159
+ * `core/telegram-config.ts` beside the other Telegram policy readings.
160
+ */
161
+ delivery: TelegramDelivery;
162
+ /**
163
+ * Cross-instance findings `--allow-cross-instance` let through (APRV-390).
164
+ *
165
+ * Empty on every ordinary start, because a finding without the flag is a
166
+ * refusal. Carried on the setup rather than printed inside `prepareListen`
167
+ * for the reason that function prints nothing at all: `approval up` and
168
+ * `approval channel telegram listen` choose their own stream, and this is
169
+ * the one thing an overridden refusal still owes the operator.
170
+ */
171
+ crossInstance: readonly InstanceFinding[];
172
+ /**
173
+ * `--allow-cross-instance` as passed (APRV-390).
174
+ *
175
+ * Carried as well as applied, because the flag overrides TWO refusals and
176
+ * only one of them can be decided synchronously. The credential check is
177
+ * `prepareListen`'s; the bot-ownership claim needs a `getMe`, so it happens
178
+ * later and has to be able to ask whether the operator already said yes.
179
+ */
180
+ allowCrossInstance: boolean;
181
+ /**
182
+ * The Bot API base this listener talks to (APRV-390).
183
+ *
184
+ * Part of the bot's identity in the ownership registry, because a bot id is
185
+ * unique within one Bot API deployment and nothing more.
186
+ */
187
+ apiBase: string;
188
+ /**
189
+ * How the one-sentence model gloss is obtained (APRV-144).
190
+ *
191
+ * OPT-IN, and absent by default. The verb wires in the production runner
192
+ * (`claude -p --model haiku`, hard timeout, failing toward absence); every
193
+ * other caller — the test suite above all — gets no gloss unless it hands
194
+ * over a runner. A default that spawned would make a subprocess an implicit
195
+ * dependency of anything that drives a dispatch cycle, and would put a model
196
+ * inside a test suite that must never invoke one.
197
+ *
198
+ * Injectable so the tests drive both branches, answered and absent, against
199
+ * a stub.
200
+ */
201
+ gloss?: GlossRunner;
202
+ /**
203
+ * Where a checkpoint key may come from, and where the cadence is read
204
+ * (APRV-257).
205
+ *
206
+ * Present at every construction site, because every one of them knows a log
207
+ * path and a policy. Whether a checkpoint is ever OFFERED is the policy's
208
+ * answer — `audit.checkpoint_every` plus `audit.checkpoint_keys` — and
209
+ * {@link checkpointOfferFor} gives up before it walks a log when the policy
210
+ * names neither, so a gate that has not turned checkpoints on pays a policy
211
+ * load per cycle and nothing else.
212
+ */
213
+ checkpoint: CheckpointTap;
214
+ }
215
+ /**
216
+ * Claim this listener's bot before it polls (APRV-390).
217
+ *
218
+ * ONE `getMe`, once per process, before the first `getUpdates`. Two things
219
+ * come out of it: the bot's identity, which is the only thing that can tell
220
+ * two instances holding two differently-named copies of one token apart; and
221
+ * a claim in the per-machine registry, which is what makes the SECOND such
222
+ * listener refuse instead of joining a 409 loop.
223
+ *
224
+ * ## Why an unreachable Bot API is not a refusal
225
+ *
226
+ * A `getMe` that fails says nothing about ownership. The machine may be behind
227
+ * a captive portal, the API may be rate-limiting, the network may be down for
228
+ * four seconds. Refusing to start on that would mean a transient network fault
229
+ * took the phone channel down until somebody noticed, in exchange for no
230
+ * safety: an unreachable Bot API is also a `getUpdates` that cannot conflict
231
+ * with anything. So the preflight says what happened and lets the listener
232
+ * start, where the existing retry loop is already the right behaviour. The
233
+ * refusal is reserved for the case this task is about, which is a bot that is
234
+ * demonstrably somebody else's.
235
+ */
236
+ export declare function claimListenerBot(setup: ListenSetup, report: (message: string) => void): Promise<{
237
+ ok: true;
238
+ } | {
239
+ ok: false;
240
+ code: ListenRefusalCode;
241
+ message: string;
242
+ }>;
243
+ /**
244
+ * What to append to a runtime 409, naming the other instance if one is known.
245
+ *
246
+ * Read at the moment of the conflict rather than captured at start-up, because
247
+ * the other gate may have claimed the bot in between: the registry is the only
248
+ * thing that knows, and it is cheap to re-read.
249
+ */
250
+ export declare function conflictAdviceFor(setup: ListenSetup): () => string | null;
251
+ /**
252
+ * Why a listener could not be built. A closed union, because more than one
253
+ * caller now branches on it: the verb turns each into an exit code, and
254
+ * `approval up` turns each into a part it will not start (SPEC.md §11.1
255
+ * invariant 6 — a refusal is machine-readable and distinct).
256
+ */
257
+ export declare const LISTEN_REFUSAL_CODES: readonly [
258
+ /** A credential variable the policy names is unset or empty. */
259
+ "not-configured",
260
+ /** No `human:<id>` was declared, so nothing could be recorded against one. */
261
+ "no-identity",
262
+ /** `--poll-timeout` was not a whole number of seconds. */
263
+ "poll-timeout",
264
+ /** The log could not be read (or its directory does not exist). */
265
+ "log-unreadable",
266
+ /** `--payloads` did not hold a JSON object of action key -> payload. */
267
+ "payloads-unreadable",
268
+ /**
269
+ * A credential variable holds a value this instance did not configure
270
+ * (APRV-390): it was exported before the process started and is not this
271
+ * instance's own `approval env` export, or `.approval/env` names another
272
+ * instance's keystore item. Refused rather than warned since APRV-390 — the
273
+ * warning is what let a demo gate spend an evening on the production bot.
274
+ * `--allow-cross-instance` is the deliberate case.
275
+ */
276
+ "cross-instance-credential",
277
+ /**
278
+ * `getMe` named a bot another instance on this machine has already claimed
279
+ * (APRV-390). Refused BEFORE the first `getUpdates`, so the operator reads
280
+ * which two gates are involved instead of an HTTP 409 loop.
281
+ */
282
+ "bot-owned-elsewhere"];
283
+ export type ListenRefusalCode = (typeof LISTEN_REFUSAL_CODES)[number];
284
+ /** Everything {@link prepareListen} needs, already resolved to absolute paths. */
285
+ export interface ListenRequest {
286
+ /** The log to derive the queue from and append decisions to. */
287
+ logPath: string;
288
+ /** Policy location, with `loadPolicy`'s semantics. */
289
+ policy: {
290
+ dir?: string;
291
+ file?: string;
292
+ };
293
+ /** `--as` as typed, or `null` to fall back to `APPROVAL_HUMAN`. */
294
+ as: string | null;
295
+ /** `--payloads`, already absolute, or `null` for the payload store alone. */
296
+ payloads: string | null;
297
+ /** `--api-base`, or `null` for the Bot API. */
298
+ apiBase: string | null;
299
+ /** `--poll-timeout` as typed, or `null` for the channel's own default. */
300
+ pollTimeout: string | null;
301
+ once: boolean;
302
+ json: boolean;
303
+ /**
304
+ * `--allow-cross-instance` (APRV-390): start on a credential this instance
305
+ * did not configure, saying so, instead of refusing.
306
+ */
307
+ allowCrossInstance?: boolean;
308
+ /** Where the channel's operational complaints go. Ordinarily stderr. */
309
+ log(message: string): void;
310
+ /** The gloss runner, if the caller wants one. See {@link ListenSetup.gloss}. */
311
+ gloss?: GlossRunner;
312
+ }
313
+ export type ListenPreparation = {
314
+ ok: true;
315
+ setup: ListenSetup;
316
+ } | {
317
+ ok: false;
318
+ code: ListenRefusalCode;
319
+ message: string;
320
+ };
321
+ /**
322
+ * Everything that can fail without touching the network, in order.
323
+ *
324
+ * Deliberately sequential and deliberately synchronous: an operator who typed
325
+ * the wrong thing learns it before a bot message is sent, and the async half
326
+ * below can then assume its configuration is whole.
327
+ *
328
+ * It PRINTS NOTHING and CHOOSES NO EXIT CODE (APRV-110). The verb below turns
329
+ * each refusal into the usage or I/O error it always was; `approval up` turns
330
+ * the same refusal into a channel it declines to start, reported in doctor's
331
+ * vocabulary while the other parts carry on. Two callers, one set of checks,
332
+ * one set of sentences — which is the only way the two surfaces can agree about
333
+ * what "telegram is not configured" means.
334
+ */
335
+ export declare function prepareListen(request: ListenRequest): ListenPreparation;
336
+ /**
337
+ * What to tell a human who tapped a button for an action this listener is not
338
+ * holding open (APRV-196).
339
+ *
340
+ * The one place a stale tap gets a real answer instead of a shrug. It reads the
341
+ * VERIFIED log (SPEC.md §11.1(1): a sentence a human reads about what the log
342
+ * says is derived from a log that verified, or it is not derived at all) and
343
+ * answers from `requestState`, the same derivation the gate and the pending
344
+ * queue use. Nothing here decides anything, nothing is appended, and nothing is
345
+ * remembered between calls: an unreadable log answers `null`, which the channel
346
+ * renders as its "not open here" toast.
347
+ *
348
+ * The argument is an action REFERENCE and never a key. The string came off the
349
+ * network, so this hashes the keys the log actually carries and looks for a
350
+ * match; a caller cannot make it describe a request by naming one, and a ref
351
+ * matching nothing simply answers `null`.
352
+ *
353
+ * The walk is over `approval.requested` records, which is the set of things
354
+ * that could ever have had a button. Run only on a stale tap, which is rare by
355
+ * construction.
356
+ */
357
+ export declare function describeActionFor(logPath: string): (actionRef: string) => string | null;
358
+ /**
359
+ * Consecutive failures for one action key after which stderr warnings thin out.
360
+ *
361
+ * Not an attempt limit: the send is retried on every cycle forever (see the
362
+ * module doc). Only the complaining is throttled, to every tenth attempt.
363
+ */
364
+ export declare const DISPATCH_LOUD_ATTEMPTS = 3;
365
+ /**
366
+ * How long an unannotated delivery stays in the bookkeeping before it is
367
+ * dropped (APRV-196). Twenty-four hours, matching the channel's own
368
+ * `TELEGRAM_DEFAULT_RETENTION_MS`.
369
+ *
370
+ * It is a floor on forgetting and not a deadline for anything: a request that
371
+ * is still pending is never dropped however old it is, because the pending
372
+ * queue is checked first. What this bounds is the memory a long-lived listener
373
+ * holds for questions the log has finished with.
374
+ */
375
+ export declare const DISPATCH_RETENTION_MS: number;
376
+ /**
377
+ * The line that introduces the first batch a listener process sends (APRV-196).
378
+ *
379
+ * **Why a banner and not an edit of the earlier copies.** The incident was a
380
+ * restart re-sending five pending requests with no warning, on top of five
381
+ * copies whose buttons had quietly stopped working. Editing those earlier
382
+ * copies to say "superseded" would read better — and it is not a design that
383
+ * can be relied on, because it requires this process to know their message ids,
384
+ * which a restart by definition does not: SPEC.md §10.3 forbids channel state
385
+ * that is a source of truth, and a crash loses a cache whether or not one is
386
+ * allowed. A design that only works when the crash was gentle is a design that
387
+ * fails on the day it is needed. So the banner is unconditional, and the
388
+ * earlier copies are made harmless instead of tidy: their buttons resolve by
389
+ * action reference to the request this process has just re-delivered
390
+ * (`actionRefOf` in `channels/telegram.ts`), so a human who taps the copy they
391
+ * can see decides the request they meant.
392
+ *
393
+ * It says "started" rather than "restarted" because a listener cannot tell the
394
+ * two apart, having deliberately kept nothing that would let it, and a first
395
+ * start that claimed to be a restart would be this channel's own text lying
396
+ * about the system's history.
397
+ */
398
+ export declare function bannerLines(pending: number): string[];
399
+ /**
400
+ * The summary line that precedes a paced send, and the body of `/queue`.
401
+ *
402
+ * One message, and everything in it is arithmetic on the verified log at the
403
+ * instant it is written: how many requests are pending, how long the oldest has
404
+ * waited, and which classes they are. Nothing is remembered between calls, so
405
+ * two summaries a minute apart can disagree only because the log moved.
406
+ *
407
+ * The class tally is the part worth the space. The count alone says how much
408
+ * work is waiting; the classes say what KIND of work, which is what tells an
409
+ * approver whether the queue is six identical `network.call`s they can walk
410
+ * through or one `policy.edit` they should read carefully.
411
+ */
412
+ export declare function summaryLines(requests: ChannelRequest[], now: string): string[];
413
+ /**
414
+ * `/queue`'s reply: the summary, then one numbered line per pending request.
415
+ *
416
+ * Derived, like the summary, from the verified log at reply time and not from
417
+ * anything this process is holding: the numbering is positional and names no
418
+ * button, so a stale copy of this list cannot be used to decide anything. The
419
+ * marker says which one this listener has selected and once delivered, because
420
+ * the question `/queue` is usually asked to answer is "what else is there
421
+ * besides the one I am looking at" — and, since APRV-256, its unhappy twin,
422
+ * "where is the one I am supposed to be looking at".
423
+ *
424
+ * The footer answers that second question the only honest way available to a
425
+ * process whose knowledge of the chat ends at "a send returned success": it
426
+ * says what was sent, says it cannot tell whether the card survived, and then
427
+ * spends its remaining words on recovery rather than reassurance.
428
+ */
429
+ export declare function queueLines(requests: ChannelRequest[], now: string, shown: readonly string[]): string[];
430
+ /**
431
+ * What this process is showing, and in what order (APRV-216). **In memory
432
+ * only**, like every other field of {@link DispatchState} and for the same
433
+ * reason (SPEC.md §10.3).
434
+ *
435
+ * None of this is truth, and the check that proves it is what happens when it
436
+ * is lost: a restarted listener re-derives the pending set from the verified
437
+ * log, rebuilds the order from log order, and shows the oldest — which is
438
+ * exactly what a fresh start does anyway. What a crash costs is the human's
439
+ * place in a walkthrough, never a request that stays pending in the log and is
440
+ * never shown.
441
+ */
442
+ export interface PacedState {
443
+ /**
444
+ * Every pending action key, in the order this process will show them.
445
+ *
446
+ * Seeded from log order (oldest first, which is what `buildPendingQueue`
447
+ * returns) and rearranged by `/skip` alone. Keys the log no longer calls
448
+ * pending are dropped on every cycle, and newly pending ones join the back.
449
+ */
450
+ order: string[];
451
+ /**
452
+ * The action keys of the unit in front of the approver, or `null` when
453
+ * nothing is.
454
+ *
455
+ * A unit rather than a key because a digest (APRV-115) is one thing to read
456
+ * and several things to decide. It is released when the log says none of its
457
+ * members is pending any more, which is what makes a decision — at any
458
+ * surface, on any copy — advance the walkthrough.
459
+ */
460
+ current: string[] | null;
461
+ /** Whether any summary has been sent yet, i.e. whether this is the start. */
462
+ summarySent: boolean;
463
+ /** The pending count the last summary named, so growth can be recognised. */
464
+ announced: number;
465
+ }
466
+ /**
467
+ * The retrospective walkthrough this process is running (APRV-299). **In memory
468
+ * only**, exactly like {@link PacedState} and under the same rule (SPEC.md
469
+ * §10.3).
470
+ *
471
+ * Its loss is the check that it is not truth: a restarted listener re-derives
472
+ * the open samples from the verified log, rebuilds the order from log order,
473
+ * and offers the oldest — which is what a fresh start does anyway. A card that
474
+ * never arrives, or that a human scrolls past, leaves the sample OPEN: it stays
475
+ * in `approval audit list`, in `.approval/QUEUE.md`, and reviewable with
476
+ * `approval audit review <seq>`. Nothing here can empty the backlog, which is
477
+ * the property a sampled-audit backlog exists to have.
478
+ */
479
+ export interface ReviewWalkthrough {
480
+ /** Every open sample's seq, in the order this process will show them. */
481
+ order: number[];
482
+ /** The sample whose card is in front of the approver, or `null`. */
483
+ current: number | null;
484
+ /** Sample seq -> the message this process sent the card as. */
485
+ readonly delivered: Map<number, DeliveryId>;
486
+ /** Whether any review summary has been sent yet. */
487
+ summarySent: boolean;
488
+ /** The open count the last summary named, so growth can be recognised. */
489
+ announced: number;
490
+ /**
491
+ * The log's size in bytes when this pass last derived the backlog, or `null`
492
+ * before the first one.
493
+ *
494
+ * A cost guard and nothing else, and it is sound for exactly one reason: the
495
+ * log is APPEND-ONLY, so a size that has not changed is a record set that has
496
+ * not changed. It is read only to skip a full verified walk on the cycle
497
+ * where a card is already in front of the approver and nothing has been
498
+ * written — which, with a 25-second poll and a human who answers in minutes,
499
+ * is most cycles. Every other cycle re-derives from the log as usual, and a
500
+ * lost or stale value costs one extra read rather than a wrong answer.
501
+ */
502
+ logSize: number | null;
503
+ }
504
+ /**
505
+ * The summary line that precedes a review card, and `/queue`'s review footer.
506
+ *
507
+ * Arithmetic on the verified log at the instant it is written, exactly as
508
+ * {@link summaryLines} is: how many samples are awaiting review, how old the
509
+ * oldest is, and which classes they are. The last clause is the one that stops
510
+ * a reader treating this like the pending queue: nothing here is waiting on
511
+ * them, because all of it has already happened.
512
+ */
513
+ export declare function reviewSummaryLines(cards: ReviewCard[], now: string): string[];
514
+ /**
515
+ * What one listener process remembers between cycles. **In memory only.**
516
+ *
517
+ * SPEC.md §10.3: channels hold no state that is a source of truth. Nothing here
518
+ * is truth — the pending set is re-derived from the verified log every cycle,
519
+ * and this only prevents a second copy of a message this process already sent.
520
+ * Its loss (restart, crash) degrades to a re-send, never to a request that is
521
+ * pending in the log and absent from the approver's phone.
522
+ */
523
+ export interface DispatchState {
524
+ /**
525
+ * action key -> the delivery id this process sent it under.
526
+ *
527
+ * Pruned (APRV-196): an entry goes when the request reaches a terminal state
528
+ * and its message has been annotated, and a straggler goes when it is older
529
+ * than {@link DISPATCH_RETENTION_MS} and the pending queue no longer carries
530
+ * it. Neither prune can cost a re-send, because `buildPendingQueue` only ever
531
+ * returns requests the verified log says are pending — the same reason losing
532
+ * the whole map to a restart is safe.
533
+ */
534
+ readonly delivered: Map<string, DeliveryId>;
535
+ /** action key -> when this process sent it, ms since epoch (APRV-196). */
536
+ readonly sentAtMs: Map<string, number>;
537
+ /**
538
+ * Whether the re-delivery banner has been sent (APRV-196).
539
+ *
540
+ * A box rather than a field because {@link DispatchState} is `readonly`
541
+ * everywhere else, and for the same reason: a cycle may write what it did,
542
+ * and nothing may swap the state out from under one.
543
+ */
544
+ readonly banner: {
545
+ sent: boolean;
546
+ };
547
+ /** action key -> consecutive failed send attempts. Cleared on success. */
548
+ readonly attempts: Map<string, number>;
549
+ /** `<action key>:<code>` skips already reported, so cycles do not repeat them. */
550
+ readonly warned: Set<string>;
551
+ /**
552
+ * Keys this process has already annotated on the approver's phone (APRV-106
553
+ * for withdrawal, APRV-113 for every other terminal state). In memory, like
554
+ * `delivered`, and for the same reason: it stops a second edit of the same
555
+ * message, and its loss costs a duplicate edit at worst.
556
+ *
557
+ * The memory that matters here is `delivered`, and losing it degrades to
558
+ * un-annotated messages, NEVER to wrong annotations: a process that does not
559
+ * remember sending a message cannot edit it, and one that does re-reads the
560
+ * outcome from the verified log every cycle. A fresh listener also never
561
+ * sends a settled request in the first place — `buildPendingQueue` re-derives
562
+ * from the verified log and only `requested` is pending — so a restart leaves
563
+ * stale text on old messages whose buttons the gate refuses anyway, and
564
+ * nothing worse.
565
+ */
566
+ readonly annotated: Set<string>;
567
+ /**
568
+ * The walkthrough this process is running under `delivery: paced`
569
+ * (APRV-216). Present under `burst` too and simply never read there, so that
570
+ * one state shape serves both modes and a policy change between two runs
571
+ * needs no different bookkeeping.
572
+ */
573
+ readonly paced: PacedState;
574
+ /**
575
+ * The checkpoint prompt this process has outstanding (APRV-257). **In memory
576
+ * only**, like everything else here.
577
+ *
578
+ * `offeredSince` is the newest checkpoint's seq at the moment a prompt went
579
+ * out (`null` for a log that had never been checkpointed), and it is what
580
+ * makes "at most one outstanding, and never a nag" a single condition: a
581
+ * cadence that has lapsed keeps producing an offer on every cycle, and this
582
+ * process asks once per lapse. The value changes only when a checkpoint
583
+ * actually lands, which is also the moment due-ness goes false — so the next
584
+ * prompt comes from the next lapse and never from this one repeating.
585
+ *
586
+ * `offered: false` means nothing is outstanding. Losing the box to a restart
587
+ * costs one duplicate prompt for a checkpoint that is genuinely owed, which
588
+ * is the same direction every other piece of this bookkeeping degrades in.
589
+ */
590
+ readonly checkpoint: {
591
+ offered: boolean;
592
+ offeredSince: number | null;
593
+ };
594
+ /**
595
+ * The retrospective walkthrough (APRV-299), paced in both delivery modes.
596
+ *
597
+ * Always paced, and deliberately so even under `burst`: a review is never
598
+ * urgent, nobody is blocked on one, and a restart that put sixty of them on a
599
+ * phone at once would be the flood APRV-287 collapsed in the other direction.
600
+ * One card at a time, behind a summary, is the whole of the design.
601
+ */
602
+ readonly review: ReviewWalkthrough;
603
+ }
604
+ export declare function newDispatchState(): DispatchState;
605
+ /** What one {@link dispatchPending} call did. Total: it never throws. */
606
+ export interface DispatchResult {
607
+ /** Requests put in front of the approver by this cycle. */
608
+ delivered: {
609
+ action_key: string;
610
+ delivery_id: DeliveryId;
611
+ }[];
612
+ /** Sends that failed and will be retried on the next cycle. */
613
+ failed: {
614
+ action_key: string;
615
+ attempts: number;
616
+ message: string;
617
+ }[];
618
+ /**
619
+ * The queue could not be derived at all: the log is unreadable or does not
620
+ * verify. Nothing was sent. Fatal at startup, retried on later cycles.
621
+ */
622
+ queueError?: {
623
+ code: ChannelTagRefusalCode;
624
+ message: string;
625
+ };
626
+ /**
627
+ * Deliveries annotated with their terminal outcome and disarmed this cycle
628
+ * (APRV-106 for `withdrawn`, APRV-113 for the rest).
629
+ */
630
+ annotated: {
631
+ action_key: string;
632
+ delivery_id: DeliveryId;
633
+ outcome: TelegramTerminalState;
634
+ }[];
635
+ /**
636
+ * Digests sent this cycle (APRV-115): the message that carries the buttons,
637
+ * the batch delivery id every member's event will carry, and the members.
638
+ * A group that fell back to one message per member produces no entry here.
639
+ */
640
+ digests: {
641
+ delivery_id: DeliveryId;
642
+ batch_delivery_id: DeliveryId;
643
+ action_keys: string[];
644
+ }[];
645
+ /**
646
+ * The re-delivery banner, when this cycle sent one (APRV-196): the message
647
+ * that precedes a startup batch and says how many requests are coming.
648
+ */
649
+ banner?: {
650
+ delivery_id: DeliveryId;
651
+ pending: number;
652
+ };
653
+ /**
654
+ * The paced summary this cycle sent, when it sent one (APRV-216): the line
655
+ * that precedes the request being shown and says how many are waiting behind
656
+ * it. Sent by the first paced cycle that has something to show, and again
657
+ * whenever the pending set has grown while nothing was in front of the
658
+ * approver. Never both this and {@link banner}: they are the two modes'
659
+ * openings, and a process runs one mode.
660
+ */
661
+ summary?: {
662
+ delivery_id: DeliveryId;
663
+ pending: number;
664
+ };
665
+ /**
666
+ * Action keys dropped from the delivery bookkeeping this cycle (APRV-196),
667
+ * with why. Neither kind can cost a re-send: the pending queue is the log's
668
+ * answer, and a dropped key that is still pending is simply re-delivered.
669
+ */
670
+ pruned: {
671
+ action_key: string;
672
+ reason: "settled" | "stale";
673
+ }[];
674
+ /**
675
+ * The collapsed re-delivery this cycle sent, when it sent one (APRV-287):
676
+ * the one message that stood in for a batch of requests nobody is waiting on
677
+ * any more, and the keys it covers. At most one, on a process's first cycle.
678
+ */
679
+ collapsed?: {
680
+ delivery_id: DeliveryId;
681
+ action_keys: string[];
682
+ oldest_ms: number;
683
+ };
684
+ /**
685
+ * The `CHECKPOINT DUE` prompt this cycle sent, when it sent one (APRV-257):
686
+ * the message it is on and the head it asks about. At most one per lapse.
687
+ */
688
+ checkpoint?: {
689
+ delivery_id: DeliveryId;
690
+ seq: number;
691
+ hash: string;
692
+ };
693
+ /**
694
+ * The review card this cycle sent, when it sent one (APRV-299): the message
695
+ * it is on, the `audit.sampled` seq it is drawn for, and the action it is
696
+ * about. At most one per cycle, and none while a request is in front of the
697
+ * approver.
698
+ */
699
+ reviewCard?: {
700
+ delivery_id: DeliveryId;
701
+ sample_seq: number;
702
+ action_key: string;
703
+ };
704
+ /**
705
+ * The review summary this cycle sent, when it sent one (APRV-299): the line
706
+ * saying how many samples are awaiting review and how old the oldest is.
707
+ */
708
+ reviewSummary?: {
709
+ delivery_id: DeliveryId;
710
+ open: number;
711
+ };
712
+ /**
713
+ * The review backlog could not be derived: the log is unreadable or does not
714
+ * verify. No card was sent. Never fatal — a review is not a decision anyone
715
+ * is blocked on — and retried on the next cycle.
716
+ */
717
+ reviewError?: {
718
+ code: string;
719
+ message: string;
720
+ };
721
+ }
722
+ /**
723
+ * One dispatch cycle: re-derive the pending queue from the verified log, send
724
+ * whatever this process has not already sent.
725
+ *
726
+ * `now` is a parameter, not a clock read: TTL judgment inside
727
+ * {@link buildPendingQueue} is deterministic and the tests drive it at chosen
728
+ * instants. Requests that are decided, expired, or not yet requested are absent
729
+ * from the derivation and so are never sent.
730
+ */
731
+ export declare function dispatchPending(setup: ListenSetup, streams: Streams, state: DispatchState, now: string): Promise<DispatchResult>;
732
+ /**
733
+ * How old a pending request must be, on a listener's first cycle, to be one
734
+ * nobody is waiting on (APRV-287).
735
+ *
736
+ * The hook's own wait PLUS its retry grace, read from the same module the hook
737
+ * reads (`core/harness-wait.ts`), because two numbers would be two answers to
738
+ * "is anybody still holding this". Past the wait alone a hook process has
739
+ * stopped blocking and a retry can still adopt the question, so those are
740
+ * ordinary pending requests. Past the wait and the grace together nothing will
741
+ * adopt it: that is the moment the hook itself takes such a request back, and a
742
+ * request still pending here is one whose session never came back at all —
743
+ * exactly the dozen that arrived on a phone behind a dead daemon on
744
+ * 2026-09-06.
745
+ *
746
+ * Collapsing is not deciding. These stay pending, listable by `/queue`, and
747
+ * decidable from any copy already delivered; what changes is how many messages
748
+ * it takes to say they are there.
749
+ */
750
+ export declare const COLLAPSE_STALE_AFTER_MS: number;
751
+ /** The computed lines a collapsed re-delivery leads with (APRV-287). */
752
+ export declare function staleLines(requests: ChannelRequest[], now: string): string[];
753
+ export declare function checkpointHandlerFor(setup: ListenSetup, streams: Streams): (tap: {
754
+ sign: boolean;
755
+ head: {
756
+ seq: number;
757
+ hash: string;
758
+ };
759
+ sender?: ChannelSender;
760
+ }) => CheckpointTapResponse;
761
+ /**
762
+ * What a review tap does: the human-only `reviewSample`, and nothing else
763
+ * (APRV-299).
764
+ *
765
+ * The one path from a button on a phone to an `audit.reviewed`, and it is the
766
+ * SAME path `approval audit review` takes — same function, same refusals, same
767
+ * record shape — so a reaction given on a card and one given at a terminal are
768
+ * indistinguishable to `approval feedback`, which is the whole of AC3.
769
+ *
770
+ * The actor is `setup.actor`, the human identity this listener was configured
771
+ * with (`--as` / `APPROVAL_HUMAN`), exactly as a grant's is. It is never read
772
+ * off the tap, never off the callback, and never out of a payload field: this
773
+ * channel does not authenticate the person who pressed the button, and SPEC.md
774
+ * §11's config-declared identity is what a review is recorded against. Anyone
775
+ * who can reach the configured chat reviews as that actor, which is the same
776
+ * trust boundary a tapped grant already stands on.
777
+ *
778
+ * The reaction is passed through untouched and NOTHING here reads it (SPEC.md
779
+ * §11.1 invariant 10): it is a field on a record, chosen by a human, on its way
780
+ * to the log.
781
+ */
782
+ export declare function reviewHandlerFor(setup: ListenSetup, streams: Streams): (tap: {
783
+ sampleSeq: number;
784
+ verdict: "ok" | "denied";
785
+ reaction?: "disliked" | "indifferent" | "liked" | "loved";
786
+ note?: string;
787
+ sender?: ChannelSender;
788
+ }) => ReviewTapResponse;
789
+ /**
790
+ * `/queue`, `/skip`, `/next` — the paced walkthrough's three verbs (APRV-216).
791
+ *
792
+ * **None of them appends anything**, and the reason is structural rather than
793
+ * careful: this function never touches `recordChannelDecision`, so there is no
794
+ * path from a typed word to the log. A decision is a button, always, because a
795
+ * button carries the nonce and the action reference that bind an answer to the
796
+ * bytes an approver was shown, and a word typed into a chat carries neither.
797
+ *
798
+ * What they do move is process memory:
799
+ *
800
+ * - `/queue` reads the verified log and replies with the summary and a numbered
801
+ * list. It changes nothing, and it works while a request is selected, because
802
+ * the list is derived and not held. The reply says outright that it carries no
803
+ * buttons and that it cannot vouch for a card it once sent (APRV-256).
804
+ * - `/skip` sends the shown unit to the BACK of this process's order and
805
+ * forgets having delivered it, so the next cycle shows the next question and
806
+ * this one comes round again after the rest. The copy already in the chat
807
+ * keeps its buttons, and they still decide the same request by action
808
+ * reference (APRV-196), so a skip is "later", never "gone".
809
+ * - `/next` releases the shown unit without reordering, so this process moves
810
+ * past it and does not show it again. The same copy stays live in the chat:
811
+ * the approver has kept the question and given up their place in the queue,
812
+ * which is the opposite trade from `/skip`.
813
+ *
814
+ * A command that finds nothing to do says so, because silence in a chat window
815
+ * is indistinguishable from a listener that has died.
816
+ */
817
+ export declare function commandHandlerFor(setup: ListenSetup, streams: Streams, state: DispatchState,
818
+ /**
819
+ * When the command arrived. A clock read in production, because a command is
820
+ * answered when a human types it; injectable for the same reason
821
+ * {@link dispatchPending} takes `now` as a parameter, since the ages a reply
822
+ * states are arithmetic against it and a suite must be able to choose them.
823
+ */
824
+ clock?: () => string): (command: TelegramCommand) => Promise<void>;
825
+ /**
826
+ * How one run of the listen loop ended (APRV-110).
827
+ *
828
+ * `stopped` is the ordinary ending: a signal, or `--once` completing. The two
829
+ * failures are the ones the startup cycle has always treated as fatal, hoisted
830
+ * out of the verb so that a supervisor can treat them as a part that fell over
831
+ * rather than as a process that must exit.
832
+ */
833
+ export type ListenerOutcome = {
834
+ kind: "stopped";
835
+ } | {
836
+ kind: "queue-error";
837
+ code: ChannelTagRefusalCode;
838
+ message: string;
839
+ } | {
840
+ kind: "send-failed";
841
+ message: string;
842
+ };
843
+ /** A listen loop that is already running. {@link stop} ends it cleanly. */
844
+ export interface RunningListener {
845
+ /** Settles when the loop ends. Never rejects for a listener-shaped failure. */
846
+ readonly done: Promise<ListenerOutcome>;
847
+ /** Stop the loop, now or as soon as it reaches its first poll. */
848
+ stop(): void;
849
+ }
850
+ /**
851
+ * Start the dispatch-and-poll loop. **Installs no signal handler** and chooses
852
+ * no exit code (APRV-110): both are the caller's, because `approval up` runs
853
+ * this beside a daemon loop and a web server under one set of handlers.
854
+ *
855
+ * A FRESH {@link DispatchState} per call, which is the whole of the restart
856
+ * story: a supervisor that restarts a fallen listener re-derives the pending
857
+ * queue from the verified log and re-sends everything still pending, exactly as
858
+ * a restarted process would. A duplicate on the phone, never a silence.
859
+ */
860
+ export declare function startListener(setup: ListenSetup, streams: Streams): RunningListener;
861
+ /**
862
+ * The listener verb. Returns a promise, which is why `main` treats `channel`
863
+ * specially: it is the only long-lived command in the CLI.
864
+ */
865
+ export declare function commandTelegramListen(argv: string[], streams: Streams, cwd: string): number | Promise<number>;
866
+ /**
867
+ * Configuration health, offline.
868
+ *
869
+ * It answers one question — "is this runtime configured to talk to Telegram?"
870
+ * — and deliberately makes no network call: a health check that contacted the
871
+ * Bot API would leak the existence of the bot from any shell, and would fail
872
+ * for reasons (a captive portal, a rate limit) that say nothing about whether
873
+ * the operator's configuration is right. The *live* counters (deliveries,
874
+ * decisions, ignored callbacks, recovered poll errors) belong to a running
875
+ * listener and are surfaced by `TelegramChannel.health()` / `stats()` in
876
+ * process, and on the listener's stderr as they happen.
877
+ */
878
+ export declare function commandTelegramHealth(argv: string[], streams: Streams, cwd: string): number;
879
+ export declare function commandTelegram(argv: string[], streams: Streams, cwd: string): number | Promise<number>;