approval-md 0.0.1 → 0.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 (522) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +909 -4
  4. package/SPEC.md +445 -32
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.js +1200 -0
  7. package/dist/src/adapters/agentmail.js.map +1 -0
  8. package/dist/src/adapters/conformance.js +461 -0
  9. package/dist/src/adapters/conformance.js.map +1 -0
  10. package/dist/src/adapters/contract.js +941 -0
  11. package/dist/src/adapters/contract.js.map +1 -0
  12. package/dist/src/adapters/email.js +749 -0
  13. package/dist/src/adapters/email.js.map +1 -0
  14. package/dist/src/adapters/env-passphrase.js +132 -0
  15. package/dist/src/adapters/env-passphrase.js.map +1 -0
  16. package/dist/src/adapters/registry.js +76 -0
  17. package/dist/src/adapters/registry.js.map +1 -0
  18. package/dist/src/adapters/smtp.js +499 -0
  19. package/dist/src/adapters/smtp.js.map +1 -0
  20. package/dist/src/adapters/vault-provider.js +161 -0
  21. package/dist/src/adapters/vault-provider.js.map +1 -0
  22. package/dist/src/channels/batch.js +121 -0
  23. package/dist/src/channels/batch.js.map +1 -0
  24. package/dist/src/channels/cli.js +468 -0
  25. package/dist/src/channels/cli.js.map +1 -0
  26. package/dist/src/channels/conformance.js +445 -0
  27. package/dist/src/channels/conformance.js.map +1 -0
  28. package/dist/src/channels/contract.js +494 -0
  29. package/dist/src/channels/contract.js.map +1 -0
  30. package/dist/src/channels/payload-view.js +43 -0
  31. package/dist/src/channels/payload-view.js.map +1 -0
  32. package/dist/src/channels/render-queue.js +564 -0
  33. package/dist/src/channels/render-queue.js.map +1 -0
  34. package/dist/src/channels/tagging.js +723 -0
  35. package/dist/src/channels/tagging.js.map +1 -0
  36. package/dist/src/channels/telegram.js +3190 -0
  37. package/dist/src/channels/telegram.js.map +1 -0
  38. package/dist/src/channels/web.js +903 -0
  39. package/dist/src/channels/web.js.map +1 -0
  40. package/dist/src/cli/adapter.js +278 -0
  41. package/dist/src/cli/adapter.js.map +1 -0
  42. package/dist/src/cli/amend.js +2171 -0
  43. package/dist/src/cli/amend.js.map +1 -0
  44. package/dist/src/cli/args.js +86 -0
  45. package/dist/src/cli/args.js.map +1 -0
  46. package/dist/src/cli/attest.js +307 -0
  47. package/dist/src/cli/attest.js.map +1 -0
  48. package/dist/src/cli/audit-card.js +201 -0
  49. package/dist/src/cli/audit-card.js.map +1 -0
  50. package/dist/src/cli/audit.js +460 -0
  51. package/dist/src/cli/audit.js.map +1 -0
  52. package/dist/src/cli/channel-telegram.js +2063 -0
  53. package/dist/src/cli/channel-telegram.js.map +1 -0
  54. package/dist/src/cli/channel-web.js +357 -0
  55. package/dist/src/cli/channel-web.js.map +1 -0
  56. package/dist/src/cli/channel.js +438 -0
  57. package/dist/src/cli/channel.js.map +1 -0
  58. package/dist/src/cli/checkpoint-tap.js +238 -0
  59. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  60. package/dist/src/cli/coverage.js +343 -0
  61. package/dist/src/cli/coverage.js.map +1 -0
  62. package/dist/src/cli/daemon.js +631 -0
  63. package/dist/src/cli/daemon.js.map +1 -0
  64. package/dist/src/cli/doctor.js +2648 -0
  65. package/dist/src/cli/doctor.js.map +1 -0
  66. package/dist/src/cli/env.js +302 -0
  67. package/dist/src/cli/env.js.map +1 -0
  68. package/dist/src/cli/execute.js +1682 -0
  69. package/dist/src/cli/execute.js.map +1 -0
  70. package/dist/src/cli/exit-codes.js +82 -0
  71. package/dist/src/cli/exit-codes.js.map +1 -0
  72. package/dist/src/cli/feedback.js +205 -0
  73. package/dist/src/cli/feedback.js.map +1 -0
  74. package/dist/src/cli/gate-window.js +294 -0
  75. package/dist/src/cli/gate-window.js.map +1 -0
  76. package/dist/src/cli/gate.js +557 -0
  77. package/dist/src/cli/gate.js.map +1 -0
  78. package/dist/src/cli/git-scope.js +295 -0
  79. package/dist/src/cli/git-scope.js.map +1 -0
  80. package/dist/src/cli/gloss-attach.js +107 -0
  81. package/dist/src/cli/gloss-attach.js.map +1 -0
  82. package/dist/src/cli/gloss-codex-child.js +149 -0
  83. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  84. package/dist/src/cli/gloss-codex.js +255 -0
  85. package/dist/src/cli/gloss-codex.js.map +1 -0
  86. package/dist/src/cli/gloss-options.js +79 -0
  87. package/dist/src/cli/gloss-options.js.map +1 -0
  88. package/dist/src/cli/gloss.js +362 -0
  89. package/dist/src/cli/gloss.js.map +1 -0
  90. package/dist/src/cli/help.js +2217 -0
  91. package/dist/src/cli/help.js.map +1 -0
  92. package/dist/src/cli/hook.js +2743 -0
  93. package/dist/src/cli/hook.js.map +1 -0
  94. package/dist/src/cli/import.js +175 -0
  95. package/dist/src/cli/import.js.map +1 -0
  96. package/dist/src/cli/init.js +336 -0
  97. package/dist/src/cli/init.js.map +1 -0
  98. package/dist/src/cli/instructions.js +262 -0
  99. package/dist/src/cli/instructions.js.map +1 -0
  100. package/dist/src/cli/journal.js +238 -0
  101. package/dist/src/cli/journal.js.map +1 -0
  102. package/dist/src/cli/log-advance.js +749 -0
  103. package/dist/src/cli/log-advance.js.map +1 -0
  104. package/dist/src/cli/log-anchor.js +387 -0
  105. package/dist/src/cli/log-anchor.js.map +1 -0
  106. package/dist/src/cli/log-checkpoint.js +128 -0
  107. package/dist/src/cli/log-checkpoint.js.map +1 -0
  108. package/dist/src/cli/log-sync.js +849 -0
  109. package/dist/src/cli/log-sync.js.map +1 -0
  110. package/dist/src/cli/log-verbs.js +354 -0
  111. package/dist/src/cli/log-verbs.js.map +1 -0
  112. package/dist/src/cli/long-help.js +148 -0
  113. package/dist/src/cli/long-help.js.map +1 -0
  114. package/dist/src/cli/main.js +1056 -0
  115. package/dist/src/cli/main.js.map +1 -0
  116. package/dist/src/cli/mcp.js +306 -0
  117. package/dist/src/cli/mcp.js.map +1 -0
  118. package/dist/src/cli/paths.js +79 -0
  119. package/dist/src/cli/paths.js.map +1 -0
  120. package/dist/src/cli/payload.js +253 -0
  121. package/dist/src/cli/payload.js.map +1 -0
  122. package/dist/src/cli/policy.js +229 -0
  123. package/dist/src/cli/policy.js.map +1 -0
  124. package/dist/src/cli/preflight.js +888 -0
  125. package/dist/src/cli/preflight.js.map +1 -0
  126. package/dist/src/cli/progress.js +112 -0
  127. package/dist/src/cli/progress.js.map +1 -0
  128. package/dist/src/cli/prompt.js +312 -0
  129. package/dist/src/cli/prompt.js.map +1 -0
  130. package/dist/src/cli/records.js +66 -0
  131. package/dist/src/cli/records.js.map +1 -0
  132. package/dist/src/cli/render.js +132 -0
  133. package/dist/src/cli/render.js.map +1 -0
  134. package/dist/src/cli/sandbox.js +150 -0
  135. package/dist/src/cli/sandbox.js.map +1 -0
  136. package/dist/src/cli/scaffold.js +137 -0
  137. package/dist/src/cli/scaffold.js.map +1 -0
  138. package/dist/src/cli/setup-adapter.js +475 -0
  139. package/dist/src/cli/setup-adapter.js.map +1 -0
  140. package/dist/src/cli/setup-channel.js +635 -0
  141. package/dist/src/cli/setup-channel.js.map +1 -0
  142. package/dist/src/cli/setup-checkpoint.js +196 -0
  143. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  144. package/dist/src/cli/setup-common.js +376 -0
  145. package/dist/src/cli/setup-common.js.map +1 -0
  146. package/dist/src/cli/setup-flow.js +476 -0
  147. package/dist/src/cli/setup-flow.js.map +1 -0
  148. package/dist/src/cli/setup-service.js +308 -0
  149. package/dist/src/cli/setup-service.js.map +1 -0
  150. package/dist/src/cli/setup.js +473 -0
  151. package/dist/src/cli/setup.js.map +1 -0
  152. package/dist/src/cli/style.js +469 -0
  153. package/dist/src/cli/style.js.map +1 -0
  154. package/dist/src/cli/token.js +274 -0
  155. package/dist/src/cli/token.js.map +1 -0
  156. package/dist/src/cli/up.js +847 -0
  157. package/dist/src/cli/up.js.map +1 -0
  158. package/dist/src/cli/usage.js +91 -0
  159. package/dist/src/cli/usage.js.map +1 -0
  160. package/dist/src/cli/values.js +189 -0
  161. package/dist/src/cli/values.js.map +1 -0
  162. package/dist/src/cli/vault.js +362 -0
  163. package/dist/src/cli/vault.js.map +1 -0
  164. package/dist/src/cli/verb-registry.js +2173 -0
  165. package/dist/src/cli/verb-registry.js.map +1 -0
  166. package/dist/src/cli/wordmark.js +52 -0
  167. package/dist/src/cli/wordmark.js.map +1 -0
  168. package/dist/src/core/advance-cycle.js +200 -0
  169. package/dist/src/core/advance-cycle.js.map +1 -0
  170. package/dist/src/core/agents-md.js +747 -0
  171. package/dist/src/core/agents-md.js.map +1 -0
  172. package/dist/src/core/attest.js +577 -0
  173. package/dist/src/core/attest.js.map +1 -0
  174. package/dist/src/core/audit.js +882 -0
  175. package/dist/src/core/audit.js.map +1 -0
  176. package/dist/src/core/budgets.js +449 -0
  177. package/dist/src/core/budgets.js.map +1 -0
  178. package/dist/src/core/checkpoint.js +738 -0
  179. package/dist/src/core/checkpoint.js.map +1 -0
  180. package/dist/src/core/child-env.js +86 -0
  181. package/dist/src/core/child-env.js.map +1 -0
  182. package/dist/src/core/clock.js +43 -0
  183. package/dist/src/core/clock.js.map +1 -0
  184. package/dist/src/core/command-class.js +2321 -0
  185. package/dist/src/core/command-class.js.map +1 -0
  186. package/dist/src/core/coverage-sources/adapter.js +71 -0
  187. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  188. package/dist/src/core/coverage-sources/gh.js +136 -0
  189. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  190. package/dist/src/core/coverage-sources/git.js +269 -0
  191. package/dist/src/core/coverage-sources/git.js.map +1 -0
  192. package/dist/src/core/coverage.js +337 -0
  193. package/dist/src/core/coverage.js.map +1 -0
  194. package/dist/src/core/credential-spec.js +23 -0
  195. package/dist/src/core/credential-spec.js.map +1 -0
  196. package/dist/src/core/dark-session.js +714 -0
  197. package/dist/src/core/dark-session.js.map +1 -0
  198. package/dist/src/core/decision-refusal.js +265 -0
  199. package/dist/src/core/decision-refusal.js.map +1 -0
  200. package/dist/src/core/env-file.js +837 -0
  201. package/dist/src/core/env-file.js.map +1 -0
  202. package/dist/src/core/execute.js +1233 -0
  203. package/dist/src/core/execute.js.map +1 -0
  204. package/dist/src/core/frontmatter.js +100 -0
  205. package/dist/src/core/frontmatter.js.map +1 -0
  206. package/dist/src/core/gate-window.js +506 -0
  207. package/dist/src/core/gate-window.js.map +1 -0
  208. package/dist/src/core/gate.js +2947 -0
  209. package/dist/src/core/gate.js.map +1 -0
  210. package/dist/src/core/git-run.js +93 -0
  211. package/dist/src/core/git-run.js.map +1 -0
  212. package/dist/src/core/harness-version.js +210 -0
  213. package/dist/src/core/harness-version.js.map +1 -0
  214. package/dist/src/core/harness-wait.js +58 -0
  215. package/dist/src/core/harness-wait.js.map +1 -0
  216. package/dist/src/core/head-retry.js +121 -0
  217. package/dist/src/core/head-retry.js.map +1 -0
  218. package/dist/src/core/instance.js +319 -0
  219. package/dist/src/core/instance.js.map +1 -0
  220. package/dist/src/core/intake-limits.js +350 -0
  221. package/dist/src/core/intake-limits.js.map +1 -0
  222. package/dist/src/core/jcs.js +132 -0
  223. package/dist/src/core/jcs.js.map +1 -0
  224. package/dist/src/core/journal.js +200 -0
  225. package/dist/src/core/journal.js.map +1 -0
  226. package/dist/src/core/live-draw.js +703 -0
  227. package/dist/src/core/live-draw.js.map +1 -0
  228. package/dist/src/core/log-reconcile.js +136 -0
  229. package/dist/src/core/log-reconcile.js.map +1 -0
  230. package/dist/src/core/log.js +546 -0
  231. package/dist/src/core/log.js.map +1 -0
  232. package/dist/src/core/loop.js +476 -0
  233. package/dist/src/core/loop.js.map +1 -0
  234. package/dist/src/core/md-fence.js +74 -0
  235. package/dist/src/core/md-fence.js.map +1 -0
  236. package/dist/src/core/money.js +195 -0
  237. package/dist/src/core/money.js.map +1 -0
  238. package/dist/src/core/payload-census.js +146 -0
  239. package/dist/src/core/payload-census.js.map +1 -0
  240. package/dist/src/core/payload-store.js +340 -0
  241. package/dist/src/core/payload-store.js.map +1 -0
  242. package/dist/src/core/payload.js +80 -0
  243. package/dist/src/core/payload.js.map +1 -0
  244. package/dist/src/core/policy-diff.js +565 -0
  245. package/dist/src/core/policy-diff.js.map +1 -0
  246. package/dist/src/core/policy-expectations.js +394 -0
  247. package/dist/src/core/policy-expectations.js.map +1 -0
  248. package/dist/src/core/policy-explain.js +230 -0
  249. package/dist/src/core/policy-explain.js.map +1 -0
  250. package/dist/src/core/policy-load.js +524 -0
  251. package/dist/src/core/policy-load.js.map +1 -0
  252. package/dist/src/core/policy-match.js +467 -0
  253. package/dist/src/core/policy-match.js.map +1 -0
  254. package/dist/src/core/policy-proposal.js +458 -0
  255. package/dist/src/core/policy-proposal.js.map +1 -0
  256. package/dist/src/core/prompt-layout.js +422 -0
  257. package/dist/src/core/prompt-layout.js.map +1 -0
  258. package/dist/src/core/protected-path-guard.js +1087 -0
  259. package/dist/src/core/protected-path-guard.js.map +1 -0
  260. package/dist/src/core/registration.js +39 -0
  261. package/dist/src/core/registration.js.map +1 -0
  262. package/dist/src/core/reindex.js +336 -0
  263. package/dist/src/core/reindex.js.map +1 -0
  264. package/dist/src/core/sampler.js +388 -0
  265. package/dist/src/core/sampler.js.map +1 -0
  266. package/dist/src/core/sandbox.js +424 -0
  267. package/dist/src/core/sandbox.js.map +1 -0
  268. package/dist/src/core/seal.js +290 -0
  269. package/dist/src/core/seal.js.map +1 -0
  270. package/dist/src/core/state.js +1009 -0
  271. package/dist/src/core/state.js.map +1 -0
  272. package/dist/src/core/task-file.js +464 -0
  273. package/dist/src/core/task-file.js.map +1 -0
  274. package/dist/src/core/telegram-config.js +114 -0
  275. package/dist/src/core/telegram-config.js.map +1 -0
  276. package/dist/src/core/token.js +578 -0
  277. package/dist/src/core/token.js.map +1 -0
  278. package/dist/src/core/validate.js +0 -0
  279. package/dist/src/core/validate.js.map +1 -0
  280. package/dist/src/core/values.js +153 -0
  281. package/dist/src/core/values.js.map +1 -0
  282. package/dist/src/core/vault.js +612 -0
  283. package/dist/src/core/vault.js.map +1 -0
  284. package/dist/src/core/verified-snapshot.js +506 -0
  285. package/dist/src/core/verified-snapshot.js.map +1 -0
  286. package/dist/src/core/verify.js +549 -0
  287. package/dist/src/core/verify.js.map +1 -0
  288. package/dist/src/core/version.js +9 -0
  289. package/dist/src/core/version.js.map +1 -0
  290. package/dist/src/core/wysiwys.js +728 -0
  291. package/dist/src/core/wysiwys.js.map +1 -0
  292. package/dist/src/daemon/advance-child.js +78 -0
  293. package/dist/src/daemon/advance-child.js.map +1 -0
  294. package/dist/src/daemon/advance.js +849 -0
  295. package/dist/src/daemon/advance.js.map +1 -0
  296. package/dist/src/daemon/audit.js +90 -0
  297. package/dist/src/daemon/audit.js.map +1 -0
  298. package/dist/src/daemon/daemon.js +1988 -0
  299. package/dist/src/daemon/daemon.js.map +1 -0
  300. package/dist/src/daemon/dark-session.js +119 -0
  301. package/dist/src/daemon/dark-session.js.map +1 -0
  302. package/dist/src/daemon/draw-child.js +132 -0
  303. package/dist/src/daemon/draw-child.js.map +1 -0
  304. package/dist/src/daemon/draw.js +458 -0
  305. package/dist/src/daemon/draw.js.map +1 -0
  306. package/dist/src/daemon/git-evidence.js +345 -0
  307. package/dist/src/daemon/git-evidence.js.map +1 -0
  308. package/dist/src/daemon/projection.js +233 -0
  309. package/dist/src/daemon/projection.js.map +1 -0
  310. package/dist/src/daemon/prune.js +376 -0
  311. package/dist/src/daemon/prune.js.map +1 -0
  312. package/dist/src/mcp/http.js +343 -0
  313. package/dist/src/mcp/http.js.map +1 -0
  314. package/dist/src/mcp/server.js +594 -0
  315. package/dist/src/mcp/server.js.map +1 -0
  316. package/docs/cli-reference.md +5363 -0
  317. package/package.json +43 -4
  318. package/schema/.gitkeep +0 -0
  319. package/schema/LICENSE +117 -0
  320. package/schema/envelope.schema.json +137 -0
  321. package/schema/event.schema.json +1810 -0
  322. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  323. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  324. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  325. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  326. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  327. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  328. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  329. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  330. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  331. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  332. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  333. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  334. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  335. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  336. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  337. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  338. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  339. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  340. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  341. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  342. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  343. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  344. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  345. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  346. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  347. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  348. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  349. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  350. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  351. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  352. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  353. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  354. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  355. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  356. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  357. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  358. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  359. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  360. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  361. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  362. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  363. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  364. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  365. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  366. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  367. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  368. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  370. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  371. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  372. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  373. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  374. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  375. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  376. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  377. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  378. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  379. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  380. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  381. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  382. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  383. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  384. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  385. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  386. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  387. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  388. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  389. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  390. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  391. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  392. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  393. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  394. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  395. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  396. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  397. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  398. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  399. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  400. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  401. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  402. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  403. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  404. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  405. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  406. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  407. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  408. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  409. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  410. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  411. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  412. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  413. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  414. package/schema/fixtures/event/valid/execution-started.json +14 -0
  415. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  416. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  417. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  418. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  419. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  420. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  421. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  422. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  423. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  424. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  425. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  426. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  427. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  428. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  429. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  430. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  431. package/schema/fixtures/event/valid/spec-example.json +15 -0
  432. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  433. package/schema/fixtures/event/valid/task-registered.json +14 -0
  434. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  435. package/schema/fixtures/hash/known-answer.json +74 -0
  436. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  437. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  438. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  439. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  440. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  441. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  442. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  443. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  444. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  445. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  446. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  447. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  448. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  449. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  450. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  451. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  452. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  453. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  454. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  455. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  456. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  457. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  458. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  459. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  460. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  461. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  462. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  463. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  464. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  465. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  466. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  467. package/schema/fixtures/policy/valid/canonical.json +47 -0
  468. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  469. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  470. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  471. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  472. package/schema/fixtures/policy/valid/human-only.json +9 -0
  473. package/schema/fixtures/policy/valid/minimal.json +6 -0
  474. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  475. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  476. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  477. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  478. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  479. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  480. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  481. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  482. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  483. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  484. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  485. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  486. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  487. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  488. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  489. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  490. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  491. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  492. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  493. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  494. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  495. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  496. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  497. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  498. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  499. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  500. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  501. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  502. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  503. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  504. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  505. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  506. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  507. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  508. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  509. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  510. package/schema/fixtures/values/invalid/version-string.json +1 -0
  511. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  512. package/schema/fixtures/values/valid/full.json +20 -0
  513. package/schema/fixtures/values/valid/minimal.json +1 -0
  514. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  515. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  516. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  517. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  518. package/schema/fixtures/values-md/valid/absent.md +50 -0
  519. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  520. package/schema/policy.schema.json +481 -0
  521. package/schema/sample-record.schema.json +26 -0
  522. package/schema/values.schema.json +55 -0
@@ -0,0 +1,1056 @@
1
+ /**
2
+ * `approval` CLI entry point — the commands of SPEC.md §10.1: the log-facing
3
+ * `approval log verify | tail | export` and `approval reindex`, the policy
4
+ * verbs, and the gate verbs `register`, `request`, `grant`, `reject`, `revoke`,
5
+ * and `expire`.
6
+ *
7
+ * **The CLI holds no logic.** Chain verification lives in `core/verify.ts`, the
8
+ * projection in `core/reindex.ts`, and appends in `core/log.ts`. Everything
9
+ * here is argument parsing, path resolution, output formatting, and the mapping
10
+ * from a core result to an exit code. That boundary is deliberate: the rules
11
+ * about what counts as a clean log must have exactly one implementation, and it
12
+ * is not this one.
13
+ *
14
+ * **Two things are frozen public API**, because agents depend on them
15
+ * mechanically: the exit codes (see `exit-codes.ts`) and the `--json` shapes
16
+ * (documented in every `--help`). Both are pinned by tests.
17
+ *
18
+ * **I/O is not integrity.** `verify()` cannot tell an unreadable log from a
19
+ * broken one, so this layer stats and access-checks the path *first* and
20
+ * reports filesystem problems as {@link EXIT_IO} with a message that never uses
21
+ * the word "corrupt". Absent files are exempt: an empty log is clean.
22
+ *
23
+ * Nothing in this file writes to the log, and no command repairs a torn tail.
24
+ * The gate verbs do append — through `core/gate.ts`, which appends through
25
+ * `core/log.ts` — and their exit-code mapping lives in `gate.ts` beside them: a
26
+ * gate refusal is {@link EXIT_INTEGRITY}, because the command was well-formed
27
+ * and the runtime's answer was no.
28
+ */
29
+ import { pathToFileURL } from "node:url";
30
+ import { boolFlag, countFlag, parseFlags, stringFlag } from "./args.js";
31
+ import { EXIT_INTEGRITY, EXIT_IO, EXIT_OK, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
32
+ import { EXPORT_HELP, LOG_HELP, REINDEX_HELP, ROOT_HELP, TAIL_HELP, VERIFY_HELP, } from "./help.js";
33
+ import { DEFAULT_INDEX_PATH, DEFAULT_LOG_PATH, preflightLog, resolvePath, } from "./paths.js";
34
+ import { parseLines, readCompleteLines } from "./records.js";
35
+ import { helpFor, longHelp } from "./long-help.js";
36
+ import { refusal as renderRefusal, resetStyle, style, table, } from "./style.js";
37
+ import { usageErrorText } from "./usage.js";
38
+ import { wordmark } from "./wordmark.js";
39
+ const DEFAULT_TAIL_COUNT = 10;
40
+ const HELP_FLAGS = { "--help": "boolean", "-h": "boolean" };
41
+ function defaultStreams() {
42
+ return {
43
+ out: (text) => void process.stdout.write(text),
44
+ err: (text) => void process.stderr.write(text),
45
+ };
46
+ }
47
+ function emitJson(streams, value) {
48
+ streams.out(`${JSON.stringify(value)}\n`);
49
+ }
50
+ function emitJsonError(streams, code, message) {
51
+ streams.err(`${JSON.stringify({ error: { code, message } })}\n`);
52
+ }
53
+ function usageError(streams, json, message, helpText) {
54
+ if (json)
55
+ emitJsonError(streams, "usage", message);
56
+ else
57
+ streams.err(usageErrorText(message, helpText));
58
+ return EXIT_USAGE;
59
+ }
60
+ function ioError(streams, json, message) {
61
+ if (json)
62
+ emitJsonError(streams, "io", message);
63
+ else
64
+ streams.err(`approval: ${message}\n`);
65
+ return EXIT_IO;
66
+ }
67
+ function integrityError(streams, json, message) {
68
+ if (json)
69
+ emitJsonError(streams, "integrity", message);
70
+ else
71
+ streams.err(`approval: ${message}\n`);
72
+ return EXIT_INTEGRITY;
73
+ }
74
+ /** `--json` as seen before parsing, so parse failures can still answer in JSON. */
75
+ function wantsJson(argv) {
76
+ return argv.includes("--json");
77
+ }
78
+ /** One human-readable line per record: seq, ts, event, actor, task. */
79
+ function formatRecord(record) {
80
+ const fields = (record ?? {});
81
+ const cell = (value) => value === undefined || value === null ? "-" : String(value);
82
+ return [
83
+ cell(fields["seq"]),
84
+ cell(fields["ts"]),
85
+ cell(fields["event"]),
86
+ cell(fields["actor"]),
87
+ cell(fields["task"]),
88
+ ].join("\t");
89
+ }
90
+ /** The role an actor wears in `log tail`: a human decided, a robot did not. */
91
+ function actorRole(actor) {
92
+ if (actor.startsWith("human:"))
93
+ return "ok";
94
+ if (actor.startsWith("system:"))
95
+ return "muted";
96
+ return undefined;
97
+ }
98
+ /**
99
+ * `approval log tail`'s human rendering (APRV-91 #9, APRV-102).
100
+ *
101
+ * TWO SHAPES, DELIBERATELY, and the piped one is unchanged.
102
+ *
103
+ * In a pipe (and under `NO_COLOR`) this is exactly what it always was:
104
+ * tab-separated fields, one record per line. That shape is pinned by
105
+ * `tests/cli.test.ts`, printed in three `examples/*.md` transcripts, and — the
106
+ * reason that matters more than either — it is what `cut -f2` reads. An aligned
107
+ * table is a nicer thing to look at and a worse thing to pipe, because the
108
+ * separator stops being a character and starts being "however many spaces this
109
+ * particular log needed". A log tail is the surface most likely to be on the
110
+ * left of a pipe, so the plain bytes win there.
111
+ *
112
+ * On a terminal, where nothing is parsing the output, the columns are aligned
113
+ * and the brief's roles apply: the seq right-aligned so the digits line up,
114
+ * the event name in `key`, and the actor coloured by kind (human `ok`, agent
115
+ * undressed, system `muted`). Colour is redundant with the actor prefix printed
116
+ * beside it, as everywhere. The TIMESTAMP is left undressed against the brief's
117
+ * `muted`: APRV-102's rule that a copyable value is never painted outranks it,
118
+ * and this is the surface an operator lifts timestamps out of.
119
+ *
120
+ * Both shapes carry the same fields in the same order, so this is a change of
121
+ * spacing and dressing, never of content.
122
+ */
123
+ export function renderTailHuman(records, st = style()) {
124
+ if (!st.enabled)
125
+ return records.map((record) => `${formatRecord(record)}\n`).join("");
126
+ const cellOf = (value) => value === undefined || value === null ? "-" : String(value);
127
+ const rows = records.map((record) => {
128
+ const fields = (record ?? {});
129
+ const actor = cellOf(fields["actor"]);
130
+ const role = actorRole(actor);
131
+ return [
132
+ // Not `value`-roled but genuinely undressed: a seq is the thing an
133
+ // operator retypes into `approval audit review`.
134
+ cellOf(fields["seq"]),
135
+ // The brief marks a timestamp `muted`, and APRV-102's later rule — no
136
+ // colour inside a value a human copies — outranks it. A dim timestamp is
137
+ // exactly as unpasteable as a bold one, and this is the surface an
138
+ // operator lifts timestamps out of. The alignment does the separating.
139
+ cellOf(fields["ts"]),
140
+ { text: cellOf(fields["event"]), role: "key" },
141
+ role === undefined ? actor : { text: actor, role },
142
+ cellOf(fields["task"]),
143
+ ];
144
+ });
145
+ return `${table(st, rows, { align: ["right"] })}\n`;
146
+ }
147
+ function prelude(argv, spec, helpText, streams, cwd) {
148
+ const json = wantsJson(argv);
149
+ const parsed = parseFlags(argv, { ...spec, ...HELP_FLAGS });
150
+ if (!parsed.ok) {
151
+ return { kind: "handled", code: usageError(streams, json, parsed.message, helpText) };
152
+ }
153
+ if (boolFlag(parsed.flags, "--help") || boolFlag(parsed.flags, "-h")) {
154
+ streams.out(`${helpText}\n`);
155
+ return { kind: "handled", code: EXIT_OK };
156
+ }
157
+ const extra = parsed.positionals[0];
158
+ if (extra !== undefined) {
159
+ return {
160
+ kind: "handled",
161
+ code: usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, helpText),
162
+ };
163
+ }
164
+ const logPath = resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd);
165
+ return { kind: "run", flags: parsed.flags, logPath, json };
166
+ }
167
+ /**
168
+ * The `anomalies` field, present only when there is something to report.
169
+ *
170
+ * ADDITIVE, in the strict sense the frozen `--json` shapes require: a consumer
171
+ * written against the pre-APRV-40 shape sees byte-identical output for every log
172
+ * that has no anomaly, and the key appears only when the runtime has something
173
+ * new to say. Omitting the empty case is deliberate rather than lazy — an
174
+ * always-present empty array would change the shape of every existing clean
175
+ * result, and those shapes are what agents parse.
176
+ */
177
+ function anomalyField(anomalies) {
178
+ return anomalies.length === 0 ? {} : { anomalies };
179
+ }
180
+ /**
181
+ * Print anomalies to stderr, one line each.
182
+ *
183
+ * stderr rather than stdout, and after the verdict rather than instead of it:
184
+ * the verdict is the answer to the question asked (does this chain verify?), and
185
+ * an anomaly is a note in the margin. The exit code does not move. A clean log
186
+ * with anomalies exits 0, because the chain verifies and skew is a judgment for
187
+ * a human, not a proof the runtime is entitled to enforce.
188
+ */
189
+ function reportAnomalies(streams, anomalies) {
190
+ if (anomalies.length === 0)
191
+ return;
192
+ streams.err(`approval: ${anomalies.length} timestamp anomaly(ies) — the chain verifies and NOTHING is refused; these are reported for a human to weigh\n`);
193
+ for (const anomaly of anomalies) {
194
+ streams.err(`approval: ${anomaly.kind}: ${anomaly.message}\n`);
195
+ }
196
+ }
197
+ /**
198
+ * The anchor half of `approval log verify` (APRV-219).
199
+ *
200
+ * Runs only behind `--anchor`, and only on a chain that already verified: the
201
+ * committed copy answers a different question from the one the chain walk
202
+ * answers ("does anybody else hold these records?" rather than "is this file
203
+ * self-consistent?"), and asking it about a log that does not verify would be
204
+ * deciding something from an unverified log.
205
+ *
206
+ * A divergence is an integrity refusal and exits where `corrupt` exits. A skip
207
+ * is a skip: a repository with no committed copy has said nothing about this
208
+ * log, and this verb never reports silence as a pass.
209
+ */
210
+ function anchorField(outcome) {
211
+ if (outcome.status === "skip")
212
+ return { anchor: { status: "skip", reason: outcome.reason } };
213
+ return {
214
+ anchor: {
215
+ status: outcome.status,
216
+ rev: outcome.anchor.rev,
217
+ seq: outcome.anchor.head.seq,
218
+ hash: outcome.anchor.head.hash,
219
+ bytes: outcome.anchor.byteLength,
220
+ ...(outcome.status === "diverged" ? { message: outcome.message } : {}),
221
+ },
222
+ };
223
+ }
224
+ /** The anchor line a human reads, after the chain verdict it qualifies. */
225
+ function reportAnchor(outcome, streams) {
226
+ if (outcome.status === "skip") {
227
+ streams.err(`approval: anchor skipped — ${outcome.reason}\n`);
228
+ return;
229
+ }
230
+ if (outcome.status === "diverged")
231
+ return;
232
+ streams.out(`anchor ${outcome.anchor.rev}: ${outcome.detail}\n`);
233
+ }
234
+ /**
235
+ * The anchor block on the checkpoint-refusal path, where there may be none.
236
+ *
237
+ * A separate spelling rather than a nullable {@link anchorField}, so the field
238
+ * stays ADDITIVE in the strict sense the frozen `--json` shapes require: a
239
+ * consumer that never asked for `--anchor` sees no `anchor` key, on this path as
240
+ * on every other.
241
+ */
242
+ function anchorField2(outcome) {
243
+ return outcome === null ? {} : anchorField(outcome);
244
+ }
245
+ /**
246
+ * The checkpoint half of `approval log verify` (APRV-220).
247
+ *
248
+ * The keys come from the policy, which is where the human wrote them, and an
249
+ * unloadable policy is a SKIP naming that rather than a pass: the check has
250
+ * said nothing about this log, and a verb that reported silence as a verified
251
+ * chain would be a verb that stopped verifying.
252
+ */
253
+ async function runCheckpointCheck(records, cwd) {
254
+ const { checkLogCheckpoints, checkpointPolicyOf } = await import("../core/checkpoint.js");
255
+ const configured = checkpointPolicyOf({ dir: cwd });
256
+ return checkLogCheckpoints({
257
+ records,
258
+ publicKeys: configured.publicKeys,
259
+ checkpointEveryMs: configured.checkpointEveryMs,
260
+ keysUnavailable: configured.unloadable,
261
+ });
262
+ }
263
+ function checkpointField(outcome) {
264
+ if (outcome.status === "skip") {
265
+ return { checkpoints: { status: "skip", reason: outcome.reason } };
266
+ }
267
+ if (outcome.status === "refused") {
268
+ return {
269
+ checkpoints: {
270
+ status: "refused",
271
+ code: outcome.code,
272
+ at: outcome.at,
273
+ verified: outcome.checkpoints.length,
274
+ message: outcome.message,
275
+ },
276
+ };
277
+ }
278
+ const newest = outcome.checkpoints[outcome.checkpoints.length - 1] ?? null;
279
+ return {
280
+ checkpoints: {
281
+ status: "pass",
282
+ verified: outcome.checkpoints.length,
283
+ keys: outcome.keys,
284
+ unchecked: outcome.unchecked,
285
+ newest: newest === null ? null : { at: newest.at, seq: newest.seq, hash: newest.hash },
286
+ ...(outcome.warning === null ? {} : { warning: outcome.warning }),
287
+ },
288
+ };
289
+ }
290
+ /** The checkpoint line a human reads, after the chain verdict it qualifies. */
291
+ function reportCheckpoints(outcome, streams) {
292
+ if (outcome.status === "skip") {
293
+ streams.err(`approval: checkpoints skipped — ${outcome.reason}\n`);
294
+ return;
295
+ }
296
+ if (outcome.status === "refused")
297
+ return;
298
+ streams.out(`checkpoints: ${outcome.detail}\n`);
299
+ if (outcome.warning !== null)
300
+ streams.err(`approval: ${outcome.warning}\n`);
301
+ }
302
+ async function commandVerify(argv, streams, cwd) {
303
+ const front = prelude(argv, {
304
+ "--log": "string",
305
+ "--json": "boolean",
306
+ // APRV-219. `--anchor` is the default resolution (the newest committed
307
+ // copy this checkout can see); `--anchor-rev` names one and implies it.
308
+ // Two flags rather than one optional-value flag, because this CLI's
309
+ // parser has no optional-value form and inventing one to save a word
310
+ // would make every other flag's shape a special case.
311
+ "--anchor": "boolean",
312
+ "--anchor-rev": "string",
313
+ // APRV-220. The second witness, and a separate flag from `--anchor`
314
+ // because they answer different questions and fail in different
315
+ // directions: the anchor asks whether anybody else holds these bytes, a
316
+ // checkpoint asks whether a key no agent holds signed this head. Asking
317
+ // for one has never implied the other, and neither may be weakened to
318
+ // make the other pass.
319
+ "--checkpoints": "boolean",
320
+ }, VERIFY_HELP, streams, cwd);
321
+ if (front.kind === "handled")
322
+ return front.code;
323
+ const { flags, logPath, json } = front;
324
+ const anchorRev = stringFlag(flags, "--anchor-rev");
325
+ const wantsAnchor = boolFlag(flags, "--anchor") || anchorRev !== null;
326
+ const wantsCheckpoints = boolFlag(flags, "--checkpoints");
327
+ const check = preflightLog(logPath);
328
+ if (!check.ok)
329
+ return ioError(streams, json, check.message);
330
+ // The policy is consulted for one number, `audit.skew_tolerance` (APRV-58),
331
+ // and it reaches only which anomalies are reported. The verdict below is a
332
+ // function of the log bytes and the schemas, so a missing or unloadable
333
+ // policy leaves this command's answer exactly as it was.
334
+ // `verifyWithRecords` only when the anchor was asked for (APRV-219): the
335
+ // anchor check compares against records the caller has already verified, and
336
+ // a plain run has no use for them.
337
+ const { verify, verifyWithRecords } = await import("../core/verify.js");
338
+ const walked = wantsAnchor || wantsCheckpoints
339
+ ? verifyWithRecords(logPath, { policy: { dir: cwd } })
340
+ : { result: verify(logPath, { policy: { dir: cwd } }), records: [] };
341
+ const result = walked.result;
342
+ if (result.status === "clean") {
343
+ // Loaded only when asked for (APRV-209): the anchor check pulls in git-scope
344
+ // and the chain reconciler, and a plain `log verify` has no use for either.
345
+ const anchor = wantsAnchor
346
+ ? (await import("./log-anchor.js")).checkLogAnchor({
347
+ logPath,
348
+ records: walked.records,
349
+ ...(anchorRev === null ? {} : { rev: anchorRev }),
350
+ })
351
+ : null;
352
+ // A divergence replaces the clean verdict rather than qualifying it. The
353
+ // chain walk's answer is still true and it is no longer the answer to the
354
+ // question `--anchor` asked, so printing `clean` beside it would be this
355
+ // verb reporting a pass it does not mean.
356
+ if (anchor !== null && anchor.status === "diverged") {
357
+ if (json) {
358
+ emitJson(streams, {
359
+ status: "anchor-diverged",
360
+ records: result.records,
361
+ head: result.head,
362
+ ...anchorField(anchor),
363
+ message: anchor.message,
364
+ });
365
+ }
366
+ else {
367
+ streams.err(`${renderRefusal(style({ json }), anchor.code, anchor.message)}\n`);
368
+ }
369
+ return EXIT_INTEGRITY;
370
+ }
371
+ // The second witness (APRV-220), run independently of the first and after
372
+ // it. Independently, because a checkpoint refusal and an anchor divergence
373
+ // are different facts with different repairs, and neither may be softened
374
+ // to let the other report a pass; after it, only so that a log failing both
375
+ // reports the older check's message first.
376
+ const checkpoints = wantsCheckpoints
377
+ ? await runCheckpointCheck(walked.records, cwd)
378
+ : null;
379
+ if (checkpoints !== null && checkpoints.status === "refused") {
380
+ if (json) {
381
+ emitJson(streams, {
382
+ status: "checkpoint-invalid",
383
+ records: result.records,
384
+ head: result.head,
385
+ ...anchorField2(anchor),
386
+ ...checkpointField(checkpoints),
387
+ message: checkpoints.message,
388
+ });
389
+ }
390
+ else {
391
+ streams.err(`${renderRefusal(style({ json }), checkpoints.code, checkpoints.message)}\n`);
392
+ }
393
+ return EXIT_INTEGRITY;
394
+ }
395
+ if (json) {
396
+ emitJson(streams, {
397
+ status: result.status,
398
+ records: result.records,
399
+ head: result.head,
400
+ ...anomalyField(result.anomalies),
401
+ ...(anchor === null ? {} : anchorField(anchor)),
402
+ ...(checkpoints === null ? {} : checkpointField(checkpoints)),
403
+ });
404
+ }
405
+ else {
406
+ const head = result.head === null ? "head none" : `head seq ${result.head.seq} ${result.head.hash}`;
407
+ streams.out(`clean: ${result.records} record(s), ${head}\n`);
408
+ reportAnomalies(streams, result.anomalies);
409
+ if (anchor !== null)
410
+ reportAnchor(anchor, streams);
411
+ if (checkpoints !== null)
412
+ reportCheckpoints(checkpoints, streams);
413
+ }
414
+ return EXIT_OK;
415
+ }
416
+ if (result.status === "torn-tail") {
417
+ if (json) {
418
+ emitJson(streams, {
419
+ status: result.status,
420
+ records: result.records,
421
+ head: null,
422
+ intactThroughSeq: result.intactThroughSeq,
423
+ message: result.message,
424
+ ...anomalyField(result.anomalies),
425
+ });
426
+ }
427
+ else {
428
+ reportAnomalies(streams, result.anomalies);
429
+ streams.out(`torn-tail: ${result.records} record(s), intact through seq ${result.intactThroughSeq}\n`);
430
+ streams.err(`approval: ${result.message}\n`);
431
+ }
432
+ return EXIT_TORN_TAIL;
433
+ }
434
+ if (json) {
435
+ emitJson(streams, {
436
+ status: result.status,
437
+ records: null,
438
+ head: null,
439
+ firstBadSeq: result.firstBadSeq,
440
+ reason: result.reason,
441
+ message: result.message,
442
+ });
443
+ }
444
+ else {
445
+ const where = result.firstBadSeq === null ? "unknown seq" : `seq ${result.firstBadSeq}`;
446
+ // APRV-102: the shared refusal shape. `corrupt` is the machine-readable word
447
+ // here (it is `status` in `--json`, which is unchanged), and the reason and
448
+ // the seq are the message.
449
+ streams.err(`${renderRefusal(style({ json }), "corrupt", `${result.reason} at ${where}`)}\n`);
450
+ streams.err(`approval: ${result.message}\n`);
451
+ }
452
+ return EXIT_INTEGRITY;
453
+ }
454
+ /**
455
+ * `tail` and `export` share everything but the slice and the rendering, so they
456
+ * share the verify → read → refuse-or-print sequence too.
457
+ */
458
+ async function readForOutput(logPath, streams, json) {
459
+ const { verify } = await import("../core/verify.js");
460
+ const result = verify(logPath);
461
+ if (result.status === "corrupt") {
462
+ return {
463
+ code: integrityError(streams, json, `log ${logPath} failed chain verification (${result.reason}); refusing to print records from a tampered log: ${result.message}`),
464
+ };
465
+ }
466
+ const read = readCompleteLines(logPath, result.records);
467
+ if (!read.ok)
468
+ return { code: ioError(streams, json, read.message) };
469
+ return {
470
+ lines: read.lines,
471
+ warning: result.status === "torn-tail"
472
+ ? `log ${logPath} ends with a torn line (an unterminated final record, the signature of a crashed write); the ${result.records} intact record(s) are shown and the log is left exactly as it is — nothing was repaired or truncated`
473
+ : null,
474
+ };
475
+ }
476
+ async function commandTail(argv, streams, cwd) {
477
+ const front = prelude(argv, { "--log": "string", "--json": "boolean", "-n": "string" }, TAIL_HELP, streams, cwd);
478
+ if (front.kind === "handled")
479
+ return front.code;
480
+ const { flags, logPath, json } = front;
481
+ const count = countFlag(flags, "-n");
482
+ if (!count.ok)
483
+ return usageError(streams, json, count.message, TAIL_HELP);
484
+ const limit = count.value ?? DEFAULT_TAIL_COUNT;
485
+ const check = preflightLog(logPath);
486
+ if (!check.ok)
487
+ return ioError(streams, json, check.message);
488
+ const outcome = await readForOutput(logPath, streams, json);
489
+ if ("code" in outcome)
490
+ return outcome.code;
491
+ const selected = limit === 0 ? [] : outcome.lines.slice(-limit);
492
+ const parsed = parseLines(logPath, selected);
493
+ if (!parsed.ok)
494
+ return ioError(streams, json, parsed.message);
495
+ if (json) {
496
+ const status = outcome.warning === null ? "ok" : "torn-tail";
497
+ emitJson(streams, outcome.warning === null
498
+ ? { status, records: parsed.records }
499
+ : { status, records: parsed.records, warning: outcome.warning });
500
+ }
501
+ else {
502
+ streams.out(renderTailHuman(parsed.records, style({ json })));
503
+ }
504
+ if (outcome.warning !== null && !json)
505
+ streams.err(`approval: ${outcome.warning}\n`);
506
+ return EXIT_OK;
507
+ }
508
+ async function commandExport(argv, streams, cwd) {
509
+ const front = prelude(argv, { "--log": "string", "--json": "boolean" }, EXPORT_HELP, streams, cwd);
510
+ if (front.kind === "handled")
511
+ return front.code;
512
+ const { logPath, json } = front;
513
+ const check = preflightLog(logPath);
514
+ if (!check.ok)
515
+ return ioError(streams, json, check.message);
516
+ const outcome = await readForOutput(logPath, streams, json);
517
+ if ("code" in outcome)
518
+ return outcome.code;
519
+ if (json) {
520
+ const parsed = parseLines(logPath, outcome.lines);
521
+ if (!parsed.ok)
522
+ return ioError(streams, json, parsed.message);
523
+ emitJson(streams, outcome.warning === null
524
+ ? { records: parsed.records }
525
+ : { records: parsed.records, warning: outcome.warning });
526
+ }
527
+ else {
528
+ // Verbatim: the stored line plus the newline that terminated it. No parse,
529
+ // no re-serialization — export of a clean log is a byte-for-byte copy.
530
+ for (const line of outcome.lines)
531
+ streams.out(`${line}\n`);
532
+ }
533
+ if (outcome.warning !== null && !json)
534
+ streams.err(`approval: ${outcome.warning}\n`);
535
+ return EXIT_OK;
536
+ }
537
+ async function commandReindex(argv, streams, cwd) {
538
+ const front = prelude(argv, {
539
+ "--log": "string",
540
+ "--index": "string",
541
+ "--force": "boolean",
542
+ "--json": "boolean",
543
+ }, REINDEX_HELP, streams, cwd);
544
+ if (front.kind === "handled")
545
+ return front.code;
546
+ const { flags, logPath, json } = front;
547
+ const indexPath = resolvePath(stringFlag(flags, "--index"), DEFAULT_INDEX_PATH, cwd);
548
+ const check = preflightLog(logPath);
549
+ if (!check.ok)
550
+ return ioError(streams, json, check.message);
551
+ // The projection is the only thing in this CLI that loads `better-sqlite3`,
552
+ // and it is loaded here rather than at the top of the file so that the verbs
553
+ // that never touch the index never pay for the native addon (APRV-209).
554
+ const { reindex } = await import("../core/reindex.js");
555
+ const result = reindex(logPath, indexPath, boolFlag(flags, "--force") ? { force: true } : {});
556
+ if (result.ok) {
557
+ if (json) {
558
+ emitJson(streams, {
559
+ ok: true,
560
+ records: result.records,
561
+ head: result.head,
562
+ truncated: result.truncated,
563
+ });
564
+ }
565
+ else {
566
+ const head = result.head === null ? "head none" : `head seq ${result.head.seq} ${result.head.hash}`;
567
+ streams.out(`indexed ${result.records} record(s) into ${indexPath}: ${head}, truncated ${result.truncated}\n`);
568
+ }
569
+ return EXIT_OK;
570
+ }
571
+ if (json) {
572
+ emitJson(streams, {
573
+ ok: false,
574
+ error: { code: result.error.code, message: result.error.message },
575
+ });
576
+ }
577
+ else {
578
+ streams.err(`approval: ${result.error.message}\n`);
579
+ }
580
+ switch (result.error.code) {
581
+ case "not-clean":
582
+ return EXIT_INTEGRITY;
583
+ case "torn-tail":
584
+ return EXIT_TORN_TAIL;
585
+ default:
586
+ return EXIT_IO;
587
+ }
588
+ }
589
+ async function commandLog(argv, streams, cwd) {
590
+ const sub = argv[0];
591
+ const rest = argv.slice(1);
592
+ if (sub === undefined) {
593
+ return usageError(streams, wantsJson(argv), "missing subcommand for `approval log`", LOG_HELP);
594
+ }
595
+ if (sub === "--help" || sub === "-h" || sub === "help") {
596
+ streams.out(`${LOG_HELP}\n`);
597
+ return EXIT_OK;
598
+ }
599
+ switch (sub) {
600
+ case "verify":
601
+ return commandVerify(rest, streams, cwd);
602
+ case "tail":
603
+ return commandTail(rest, streams, cwd);
604
+ case "export":
605
+ return commandExport(rest, streams, cwd);
606
+ // APRV-125. The two verbs that move the log FILE rather than reading it: a
607
+ // fast-forward pull with a chain reconcile, and the commit-and-push of what
608
+ // the chain has grown since. Neither appends an event.
609
+ case "sync": {
610
+ const { commandLogSync } = await import("./log-verbs.js");
611
+ return commandLogSync(rest, streams, cwd);
612
+ }
613
+ case "advance": {
614
+ const { commandLogAdvance } = await import("./log-verbs.js");
615
+ return commandLogAdvance(rest, streams, cwd);
616
+ }
617
+ // APRV-220. The one verb here that APPENDS: a human signing the current
618
+ // head with a key no agent process holds. Loaded lazily like the two above,
619
+ // because it reaches the vault and the signing primitives and a plain
620
+ // `approval log tail` has no use for either.
621
+ case "checkpoint": {
622
+ const { commandLogCheckpoint } = await import("./log-checkpoint.js");
623
+ return commandLogCheckpoint(rest, streams, cwd);
624
+ }
625
+ default:
626
+ return usageError(streams, wantsJson(argv), `unknown subcommand ${JSON.stringify(sub)} for \`approval log\``, LOG_HELP);
627
+ }
628
+ }
629
+ /**
630
+ * The part of a command line that belongs to `approval` itself.
631
+ *
632
+ * `approval run … -- git push` and `hook classify -- <command…>` hand the tail
633
+ * to a child, and a `--no-color` in THAT half is the child's business. Reading
634
+ * presentation flags only from the near side is what keeps this CLI from
635
+ * quietly editing the command it was asked to run.
636
+ */
637
+ function beforeSeparator(argv) {
638
+ const separator = argv.indexOf("--");
639
+ return separator === -1 ? [...argv] : argv.slice(0, separator);
640
+ }
641
+ /** Remove `--no-color`, near side only, so no verb needs it in its flag spec. */
642
+ function stripNoColor(argv) {
643
+ const separator = argv.indexOf("--");
644
+ const near = (separator === -1 ? argv : argv.slice(0, separator)).filter((word) => word !== "--no-color");
645
+ return separator === -1 ? near : [...near, ...argv.slice(separator)];
646
+ }
647
+ /** The five verbs a new operator needs, under the wordmark, and nothing else. */
648
+ function splash(theme) {
649
+ const rows = [
650
+ { left: "init", right: "scaffold APPROVAL.md and .approval/ here" },
651
+ { left: "setup", right: "declare who you are and store credentials" },
652
+ { left: "doctor", right: "can this machine run the system?" },
653
+ { left: "queue", right: "what is waiting for your decision" },
654
+ { left: "--help", right: "every verb, and the exit codes" },
655
+ ];
656
+ return `${wordmark(theme)}\n\n${theme.table(rows, { indent: 2, gap: 3 })}`;
657
+ }
658
+ /**
659
+ * The help text `--long` was asked for, or null when it was not asked for.
660
+ *
661
+ * `--long` means nothing on its own: it is a modifier on a help request, so it
662
+ * is honoured only alongside `--help`/`-h` or the `help` verb. Anywhere else it
663
+ * falls through to the verb, which will call it an unknown flag, which is the
664
+ * right answer.
665
+ */
666
+ function longHelpRequest(argv) {
667
+ const near = beforeSeparator(argv);
668
+ if (!near.includes("--long"))
669
+ return null;
670
+ const asking = near[0] === "help" || near.includes("--help") || near.includes("-h");
671
+ if (!asking)
672
+ return null;
673
+ const words = (near[0] === "help" ? near.slice(1) : near).filter((word) => !word.startsWith("-"));
674
+ return (words.length === 0 ? null : helpFor(words)) ?? ROOT_HELP;
675
+ }
676
+ /**
677
+ * Await a verb that may answer asynchronously, and return its code.
678
+ *
679
+ * Before APRV-209 the arms that call this could not return the code at all:
680
+ * `main()` was synchronous, so an asynchronous verb's promise was dropped into
681
+ * `process.exitCode` and the arm returned {@link EXIT_OK}. Awaiting is now
682
+ * possible, and it also closes a hole the drop had opened: the entry point's own
683
+ * assignment to `process.exitCode` could land after the dropped promise's and
684
+ * overwrite a usage error with a zero.
685
+ *
686
+ * `label` is the phrase that named the verb in the old rejection message
687
+ * ("doctor failed", "MCP server failed"), so those messages are unchanged.
688
+ */
689
+ async function settle(outcome, streams, label) {
690
+ try {
691
+ return await outcome;
692
+ }
693
+ catch (cause) {
694
+ streams.err(`approval: ${label}: ${cause instanceof Error ? cause.message : String(cause)}\n`);
695
+ return EXIT_IO;
696
+ }
697
+ }
698
+ /**
699
+ * Run the CLI. Resolves to the process exit code rather than calling
700
+ * `process.exit`, so buffered stdout is flushed by the normal exit path — a
701
+ * truncated JSON object would be worse than no output at all.
702
+ *
703
+ * ASYNCHRONOUS since APRV-209, and for one reason: every verb is loaded by
704
+ * `await import()` inside the switch below, and ESM has no synchronous dynamic
705
+ * import. The awaits do not make any verb concurrent — exactly one runs per
706
+ * invocation, the preamble still decides presentation once before any of them
707
+ * can print, and the long-lived verbs (`channel`, `daemon`, `up`, `mcp`) report
708
+ * their eventual code through `process.exitCode` exactly as they did.
709
+ */
710
+ export async function main(argv, options = {}) {
711
+ const streams = options.streams ?? defaultStreams();
712
+ const cwd = options.cwd ?? process.cwd();
713
+ // Presentation is decided ONCE per invocation, before any verb can print
714
+ // (APRV-91). `--no-color` is answered here and stripped, so no verb has to
715
+ // carry it in its flag spec and none can disagree about it; `--json` is a
716
+ // veto on colour, which is why it is read before the verb parses anything.
717
+ const argvForStyle = beforeSeparator(argv);
718
+ const noColor = argvForStyle.includes("--no-color");
719
+ resetStyle();
720
+ const theme = style({ json: wantsJson(argvForStyle), noColor });
721
+ const cleanArgv = noColor ? stripNoColor(argv) : argv;
722
+ const command = cleanArgv[0];
723
+ const rest = cleanArgv.slice(1);
724
+ // `--help --long` and `approval help <verb> --long` (APRV-91 #16): the short
725
+ // help verbatim, then the reference section its `why:` footer points at.
726
+ // Intercepted HERE rather than in each verb, because the alternative is the
727
+ // same three lines in sixty places and one of them getting it wrong.
728
+ const longRequest = longHelpRequest(cleanArgv);
729
+ if (longRequest !== null) {
730
+ streams.out(`${longHelp(longRequest, { style: theme })}\n`);
731
+ return EXIT_OK;
732
+ }
733
+ if (command === undefined) {
734
+ // The orientation screen (APRV-91 #7/#12). It goes to STDOUT while the
735
+ // refusal stays on stderr with today's exit 2: a bare invocation is still a
736
+ // usage error for anything scripting this CLI, and the human staring at a
737
+ // terminal still gets the wordmark and the five verbs they need.
738
+ streams.out(`${splash(theme)}\n`);
739
+ return usageError(streams, false, "no command given", ROOT_HELP);
740
+ }
741
+ if (command === "--help" || command === "-h" || command === "help") {
742
+ // `approval help <verb>` is the third spelling of `approval <verb> --help`,
743
+ // and the one a person guesses first.
744
+ const words = rest.filter((word) => !word.startsWith("-"));
745
+ const target = words.length === 0 ? null : helpFor(words);
746
+ if (target !== null) {
747
+ streams.out(`${target}\n`);
748
+ return EXIT_OK;
749
+ }
750
+ streams.out(`${wordmark(theme)}\n\n${ROOT_HELP}\n`);
751
+ return EXIT_OK;
752
+ }
753
+ switch (command) {
754
+ // The self-describing verb (APRV-85). `instructions` prints the agent-facing
755
+ // guide, and `--schemas` prints the verb registry the guide's table is
756
+ // generated from — the one source SPEC.md §10.5's MCP wrapper derives its
757
+ // tool descriptions and input schemas from, so the two surfaces cannot
758
+ // drift. It reads no log, resolves no policy, and writes nothing.
759
+ case "instructions": {
760
+ const { commandInstructions } = await import("./instructions.js");
761
+ return commandInstructions(rest, streams, cwd);
762
+ }
763
+ // The scaffolding verb (APRV-71). It is the only command that writes files
764
+ // a human has not asked for by name, and it is deliberately the least
765
+ // authoritative one in the CLI: it appends nothing, attests nothing, and
766
+ // overwrites nothing. Everything it creates is inert until a human attests.
767
+ case "init": {
768
+ const { commandInit } = await import("./init.js");
769
+ return commandInit(rest, streams, cwd);
770
+ }
771
+ case "log":
772
+ return commandLog(rest, streams, cwd);
773
+ case "policy": {
774
+ const { commandPolicy } = await import("./policy.js");
775
+ return commandPolicy(rest, streams, cwd);
776
+ }
777
+ // The gate verbs (APRV-16). grant/reject/revoke are human-only and expire
778
+ // is the system verb; the enforcement lives in core, not in this dispatch.
779
+ case "register": {
780
+ const { commandRegister } = await import("./gate.js");
781
+ return commandRegister(rest, streams, cwd);
782
+ }
783
+ case "request": {
784
+ const { commandRequest } = await import("./gate.js");
785
+ return commandRequest(rest, streams, cwd);
786
+ }
787
+ case "grant":
788
+ case "reject":
789
+ case "revoke": {
790
+ const { commandDecide } = await import("./gate.js");
791
+ return commandDecide(command, rest, streams, cwd);
792
+ }
793
+ // APRV-106. The one terminal gate verb that is NOT human-only: withdrawal
794
+ // is the requester retracting its own question, and the requester is
795
+ // usually an agent. The gate checks the actor against the request record,
796
+ // so the verb cannot be used to clear anyone else's queue.
797
+ case "withdraw": {
798
+ const { commandWithdraw } = await import("./gate.js");
799
+ return commandWithdraw(rest, streams, cwd);
800
+ }
801
+ case "expire": {
802
+ const { commandExpire } = await import("./gate.js");
803
+ return commandExpire(rest, streams, cwd);
804
+ }
805
+ // The token verbs (APRV-17). `token` reports status and writes nothing;
806
+ // `consume` is internal plumbing for APRV-18's `approval run` and is the
807
+ // only sanctioned appender of execution.started on the manual path.
808
+ case "token": {
809
+ const { commandToken } = await import("./token.js");
810
+ return commandToken(rest, streams, cwd);
811
+ }
812
+ case "consume": {
813
+ const { commandConsume } = await import("./token.js");
814
+ return commandConsume(rest, streams, cwd);
815
+ }
816
+ // The execution verbs (APRV-18). `run` is the only command that spawns
817
+ // anything and the only one that can exit 5; `wait` the only one that can
818
+ // exit 6. `queue` is the pending-decision inbox and `status` is system
819
+ // health — deliberately two verbs, because they answer to two different
820
+ // people (the human who decides, the operator who repairs).
821
+ case "run": {
822
+ const { commandRun } = await import("./execute.js");
823
+ return commandRun(rest, streams, cwd);
824
+ }
825
+ // The starving verb (APRV-193). It authorizes nothing and appends nothing:
826
+ // it runs a command with outbound network denied, which is what the hook
827
+ // cannot do for the commands it merely allows. `approval run` is the gate;
828
+ // this is the room the code the gate never saw runs in.
829
+ case "sandbox": {
830
+ const { commandSandbox } = await import("./sandbox.js");
831
+ return commandSandbox(rest, streams, cwd);
832
+ }
833
+ // The recovery verb (APRV-20 pass two). `execution resolve` is the only
834
+ // sanctioned way to close a dangling execution, and it is human-only,
835
+ // note-mandatory, and records no invented exit code.
836
+ case "execution": {
837
+ const { commandExecution } = await import("./execute.js");
838
+ return commandExecution(rest, streams, cwd);
839
+ }
840
+ // The audit verbs (APRV-40). `audit list` reads the sampled-audit backlog
841
+ // and `audit review` closes one item of it, human-only. There is no
842
+ // `audit sample`: selection is the runtime's, made by the daemon from an
843
+ // operator-held secret, and a caller who could sample could decline to.
844
+ case "audit": {
845
+ const { commandAudit } = await import("./audit.js");
846
+ return commandAudit(rest, streams, cwd);
847
+ }
848
+ case "wait": {
849
+ const { commandWait } = await import("./execute.js");
850
+ return commandWait(rest, streams, cwd);
851
+ }
852
+ case "queue": {
853
+ const { commandQueue } = await import("./execute.js");
854
+ return commandQueue(rest, streams, cwd);
855
+ }
856
+ // The open window (APRV-214, amended SPEC.md §5.2). `gate open` is the one
857
+ // verb that SUSPENDS the policy for the harness hook, so it is human-only
858
+ // three times over: it classifies `policy.core` (which APPROVAL.md holds
859
+ // human-only, so the hook denies an agent running it), it refuses a stdin
860
+ // that is not a terminal, and it reads the word `understood` with no --yes
861
+ // and no --force. `gate close` only tightens and `gate status` decides
862
+ // nothing. The window's whole state is in the log; no file holds it.
863
+ case "gate": {
864
+ const { commandGate } = await import("./gate-window.js");
865
+ return commandGate(rest, streams, cwd);
866
+ }
867
+ case "status": {
868
+ const { commandStatus } = await import("./execute.js");
869
+ return commandStatus(rest, streams, cwd);
870
+ }
871
+ // The witness verb (APRV-245). `status` reports what this runtime knows
872
+ // about itself; `coverage` asks git, `gh` and a provider what happened
873
+ // whether or not anybody routed it through the gate, and joins the answer
874
+ // to the verified log. Informational: gaps are questions, not verdicts.
875
+ case "coverage": {
876
+ const { commandCoverage } = await import("./coverage.js");
877
+ return commandCoverage(rest, streams, cwd);
878
+ }
879
+ // The diagnostic verb (APRV-31). `doctor` answers for the MACHINE what
880
+ // `status` answers for the system, and it is asynchronous for the same
881
+ // reason `channel` is: two of its checks touch the network stack (a Bot API
882
+ // `getMe`, a loopback bind probe). It writes nothing anywhere.
883
+ case "doctor": {
884
+ const { commandDoctor } = await import("./doctor.js");
885
+ return settle(commandDoctor(rest, streams, cwd), streams, "doctor failed");
886
+ }
887
+ // The channel verbs (APRV-23 cli, APRV-26 telegram). `channel cli` renders
888
+ // the pending queue over the plugin contract and, with a terminal, collects
889
+ // decisions through `recordChannelDecision` — the same human-only gate
890
+ // `grant` and `reject` call. `channel telegram listen` is the first of the
891
+ // LONG-LIVED commands in this CLI: it delivers the pending queue and then
892
+ // long-polls until it is interrupted. `main` awaits it since APRV-209, so
893
+ // the promise stays pending for as long as the listener runs and its code
894
+ // is returned rather than dropped into `process.exitCode`.
895
+ case "channel": {
896
+ const { commandChannel } = await import("./channel.js");
897
+ return settle(commandChannel(rest, streams, cwd), streams, "channel listener failed");
898
+ }
899
+ // The daemon verb (APRV-39). `daemon run` is the second LONG-LIVED command
900
+ // in this CLI and is handled exactly like `channel`. It is the only command
901
+ // that both watches and appends, and the only one whose ordinary ending is a
902
+ // signal (which is exit 0, not a failure).
903
+ case "daemon": {
904
+ const { commandDaemon } = await import("./daemon.js");
905
+ return settle(commandDaemon(rest, streams, cwd), streams, "daemon failed");
906
+ }
907
+ // The ambient runtime (APRV-110). `approval up` is the daemon loop and every
908
+ // channel the policy configures in ONE supervised process, and it is the
909
+ // fourth LONG-LIVED command here, awaited exactly as `channel` and `daemon`
910
+ // are. `daemon run --with-channels` reaches the same function.
911
+ case "up": {
912
+ const { commandUp } = await import("./up.js");
913
+ return settle(commandUp(rest, streams, cwd), streams, "the ambient runtime failed");
914
+ }
915
+ // The binding verb (APRV-29). `payload hash` prints the payload_hash of a
916
+ // JSON document through the same core function the gate uses, so nobody has
917
+ // to import an internal module (or reinvent JCS) to fill in a declaration.
918
+ // It reads no log and writes nothing.
919
+ // `payload agentmail-draft` (APRV-223) reads one draft over HTTPS, so this
920
+ // verb joins the asynchronous family and is awaited the same way; the
921
+ // `hash` path is still synchronous, and awaiting a number is a number.
922
+ case "payload": {
923
+ const { commandPayload } = await import("./payload.js");
924
+ return settle(commandPayload(rest, streams, cwd), streams, "payload failed");
925
+ }
926
+ // The ungated channel (APRV-195). `journal write` is the one verb in this
927
+ // switch that reaches no policy, no log and no token: it appends free text
928
+ // to a local file so that an agent complying perfectly can still say it
929
+ // thinks something is wrong. Nothing in the runtime reads what it writes,
930
+ // which is what makes leaving it ungated safe (SPEC.md §11.1 invariant 4).
931
+ case "journal": {
932
+ const { commandJournal } = await import("./journal.js");
933
+ return commandJournal(rest, streams, cwd);
934
+ }
935
+ // The human's half of the same pair (APRV-238). `values` prints the
936
+ // optional values block of APPROVAL.md — what the operator values, wants
937
+ // and how they answer — and it is guidance rather than policy: it grants
938
+ // nothing, and no path that computes a verdict, a class, a sample, a budget
939
+ // or a token reads it (SPEC.md §11.1 invariant 10). It resolves no policy
940
+ // rule, reads no log and appends nothing.
941
+ case "values": {
942
+ const { commandValues } = await import("./values.js");
943
+ return commandValues(rest, streams, cwd);
944
+ }
945
+ // The other direction of the same channel (APRV-239). `journal read` is the
946
+ // operator reading what the agents said; this is the agents reading what the
947
+ // operator said about their work. It reads a verified log and writes
948
+ // nothing, and every output form labels what it prints as human-authored
949
+ // GUIDANCE: no enforcement path anywhere in this dispatch reads a reaction
950
+ // (SPEC.md §11.1 invariant 10), so a surface that let one read as a rule
951
+ // would be the only place the invariant could break.
952
+ case "feedback": {
953
+ const { commandFeedback } = await import("./feedback.js");
954
+ return commandFeedback(rest, streams, cwd);
955
+ }
956
+ // The environment verb (APRV-73). `env` resolves `.approval/env` — the
957
+ // source map naming where each *_env variable's value lives — and prints an
958
+ // export block for a shell to evaluate. IT IS THE ONLY COMMAND IN THIS
959
+ // SWITCH THAT READS THAT FILE, and no command in this switch loads it into
960
+ // its own environment: human identity is one of the variables it can carry,
961
+ // so a file a process read on its own would let anything able to write it
962
+ // act as the human on every human-only verb (SPEC.md §11.1 invariant 7).
963
+ case "env": {
964
+ const { commandEnv } = await import("./env.js");
965
+ return commandEnv(rest, streams, cwd);
966
+ }
967
+ // The configuration verb (APRV-74) and the only WRITER of .approval/env.
968
+ // It is interactive by construction: every subcommand refuses a
969
+ // non-terminal stdin and --json, because a setup a pipe could drive would
970
+ // be a way for a CI job or an agent to declare a human identity and store
971
+ // a credential. It appends nothing to the log, attests nothing, and edits
972
+ // no policy file. `setup channel telegram` reaches the network, so the dispatch
973
+ // unwraps a promise exactly as `channel`, `daemon` and `adapter` do.
974
+ case "setup": {
975
+ const { commandSetup } = await import("./setup.js");
976
+ return settle(commandSetup(rest, streams, cwd), streams, "setup failed");
977
+ }
978
+ // The credential verbs (APRV-68). `vault set|list|remove` manage the
979
+ // encrypted store adapters read from, and all three are human-only. There
980
+ // is deliberately no `vault get`: a credential's only sanctioned journey is
981
+ // from the vault into an adapter inside the verified-token window, and a
982
+ // verb that printed one would put it in a terminal and a shell history.
983
+ // Nothing under this verb appends to the log.
984
+ case "vault": {
985
+ const { commandVault } = await import("./vault.js");
986
+ return commandVault(rest, streams, cwd);
987
+ }
988
+ // The side-effect verb (APRV-69). `adapter email` is the first thing in
989
+ // this CLI that reaches the world: it executes one granted action through
990
+ // the adapter contract, which spends the token and writes both execution
991
+ // events around the send. It is asynchronous for the obvious reason (a
992
+ // socket), and is unwrapped exactly as `channel` and `daemon` are.
993
+ case "adapter": {
994
+ const { commandAdapter } = await import("./adapter.js");
995
+ return settle(commandAdapter(rest, streams, cwd), streams, "adapter failed");
996
+ }
997
+ // The harness verbs (APRV-82, APRV-133). `hook claude-code` and
998
+ // `hook cursor` each read a pre-tool event on STDIN and answer allow or
999
+ // deny, so a command the harness runs itself cannot skip the gate the way
1000
+ // `approval run` cannot. They are the commands whose stdout is a decision
1001
+ // object for another program rather than a report for a human, and whose
1002
+ // exit code is deliberately 0 on a refusal: the harness reads a hook's
1003
+ // verdict only on exit 0.
1004
+ case "hook": {
1005
+ // The latency-critical case (APRV-209): a session pays this load on every
1006
+ // command it runs, so `hook.ts` and its core dependencies are the only
1007
+ // verb graph a pass-through invocation brings in.
1008
+ const { commandHook } = await import("./hook.js");
1009
+ return commandHook(rest, streams, cwd);
1010
+ }
1011
+ // The interoperability verb (APRV-64). `import agents-md` reads permissions
1012
+ // PROSE and prints a draft policy block. It is the only verb whose output is
1013
+ // a proposal: it writes no policy, appends nothing, and attests nothing —
1014
+ // the human's `policy amend` is what puts any of it in force.
1015
+ case "import": {
1016
+ const { commandImport } = await import("./import.js");
1017
+ return commandImport(rest, streams, cwd);
1018
+ }
1019
+ // The wrapper verb (APRV-87). `mcp serve` publishes the agent-facing verbs
1020
+ // as MCP tools over stdio (SPEC.md §10.5) and is the third LONG-LIVED
1021
+ // command here, unwrapped exactly as `channel` and `daemon` are. It is
1022
+ // AGENT-FACING BY CONSTRUCTION: its tool list is the verb registry filtered
1023
+ // by human_only, so nothing that records a human's authority is reachable
1024
+ // through it, and the identity it runs as is fixed before the transport
1025
+ // exists. The verb itself is human-only, because starting one is an
1026
+ // operator's act.
1027
+ case "mcp": {
1028
+ const { commandMcp } = await import("./mcp.js");
1029
+ return settle(commandMcp(rest, streams, cwd), streams, "MCP server failed");
1030
+ }
1031
+ case "reindex":
1032
+ return commandReindex(rest, streams, cwd);
1033
+ // The projection verb (APRV-24). `render` writes .approval/QUEUE.md and
1034
+ // nothing else; the projection itself is `channels/render-queue.ts`.
1035
+ case "render": {
1036
+ const { commandRender } = await import("./render.js");
1037
+ return commandRender(rest, streams, cwd);
1038
+ }
1039
+ default:
1040
+ return usageError(streams, wantsJson(argv), `unknown command ${JSON.stringify(command)}`, ROOT_HELP);
1041
+ }
1042
+ }
1043
+ // Direct execution: `node dist/src/cli/main.js …` behaves exactly like the
1044
+ // `approval` bin, which is a thin loader around this module.
1045
+ const invoked = process.argv[1];
1046
+ if (invoked !== undefined && import.meta.url === pathToFileURL(invoked).href) {
1047
+ // `main` resolves rather than returns since APRV-209; the code still reaches
1048
+ // the process through `process.exitCode`, so stdout is flushed by the normal
1049
+ // exit path. The rejection is DELIBERATELY not caught: a throw out of the
1050
+ // dispatch used to be an uncaught exception (stack trace, exit 1) and it stays
1051
+ // one, rather than being dressed up as one of the frozen exit codes.
1052
+ void main(process.argv.slice(2)).then((code) => {
1053
+ process.exitCode = code;
1054
+ });
1055
+ }
1056
+ //# sourceMappingURL=main.js.map