approval-md 0.0.1 → 0.2.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 (722) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +940 -4
  4. package/SPEC.md +476 -34
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.d.ts +426 -0
  7. package/dist/src/adapters/agentmail.js +1200 -0
  8. package/dist/src/adapters/agentmail.js.map +1 -0
  9. package/dist/src/adapters/conformance.d.ts +149 -0
  10. package/dist/src/adapters/conformance.js +461 -0
  11. package/dist/src/adapters/conformance.js.map +1 -0
  12. package/dist/src/adapters/contract.d.ts +628 -0
  13. package/dist/src/adapters/contract.js +1035 -0
  14. package/dist/src/adapters/contract.js.map +1 -0
  15. package/dist/src/adapters/email.d.ts +324 -0
  16. package/dist/src/adapters/email.js +749 -0
  17. package/dist/src/adapters/email.js.map +1 -0
  18. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  19. package/dist/src/adapters/env-passphrase.js +132 -0
  20. package/dist/src/adapters/env-passphrase.js.map +1 -0
  21. package/dist/src/adapters/public.d.ts +11 -0
  22. package/dist/src/adapters/public.js +11 -0
  23. package/dist/src/adapters/public.js.map +1 -0
  24. package/dist/src/adapters/registry.d.ts +59 -0
  25. package/dist/src/adapters/registry.js +77 -0
  26. package/dist/src/adapters/registry.js.map +1 -0
  27. package/dist/src/adapters/smtp.d.ts +213 -0
  28. package/dist/src/adapters/smtp.js +499 -0
  29. package/dist/src/adapters/smtp.js.map +1 -0
  30. package/dist/src/adapters/vault-provider.d.ts +114 -0
  31. package/dist/src/adapters/vault-provider.js +161 -0
  32. package/dist/src/adapters/vault-provider.js.map +1 -0
  33. package/dist/src/adapters/zzz.d.ts +66 -0
  34. package/dist/src/adapters/zzz.js +299 -0
  35. package/dist/src/adapters/zzz.js.map +1 -0
  36. package/dist/src/channels/batch.d.ts +109 -0
  37. package/dist/src/channels/batch.js +121 -0
  38. package/dist/src/channels/batch.js.map +1 -0
  39. package/dist/src/channels/cli.d.ts +193 -0
  40. package/dist/src/channels/cli.js +468 -0
  41. package/dist/src/channels/cli.js.map +1 -0
  42. package/dist/src/channels/conformance.d.ts +92 -0
  43. package/dist/src/channels/conformance.js +445 -0
  44. package/dist/src/channels/conformance.js.map +1 -0
  45. package/dist/src/channels/contract.d.ts +623 -0
  46. package/dist/src/channels/contract.js +494 -0
  47. package/dist/src/channels/contract.js.map +1 -0
  48. package/dist/src/channels/payload-view.d.ts +35 -0
  49. package/dist/src/channels/payload-view.js +43 -0
  50. package/dist/src/channels/payload-view.js.map +1 -0
  51. package/dist/src/channels/render-queue.d.ts +149 -0
  52. package/dist/src/channels/render-queue.js +564 -0
  53. package/dist/src/channels/render-queue.js.map +1 -0
  54. package/dist/src/channels/tagging.d.ts +196 -0
  55. package/dist/src/channels/tagging.js +723 -0
  56. package/dist/src/channels/tagging.js.map +1 -0
  57. package/dist/src/channels/telegram.d.ts +1832 -0
  58. package/dist/src/channels/telegram.js +3190 -0
  59. package/dist/src/channels/telegram.js.map +1 -0
  60. package/dist/src/channels/web.d.ts +341 -0
  61. package/dist/src/channels/web.js +903 -0
  62. package/dist/src/channels/web.js.map +1 -0
  63. package/dist/src/cli/adapter.d.ts +90 -0
  64. package/dist/src/cli/adapter.js +288 -0
  65. package/dist/src/cli/adapter.js.map +1 -0
  66. package/dist/src/cli/amend.d.ts +59 -0
  67. package/dist/src/cli/amend.js +2171 -0
  68. package/dist/src/cli/amend.js.map +1 -0
  69. package/dist/src/cli/args.d.ts +43 -0
  70. package/dist/src/cli/args.js +86 -0
  71. package/dist/src/cli/args.js.map +1 -0
  72. package/dist/src/cli/attest.d.ts +41 -0
  73. package/dist/src/cli/attest.js +307 -0
  74. package/dist/src/cli/attest.js.map +1 -0
  75. package/dist/src/cli/audit-card.d.ts +62 -0
  76. package/dist/src/cli/audit-card.js +201 -0
  77. package/dist/src/cli/audit-card.js.map +1 -0
  78. package/dist/src/cli/audit.d.ts +59 -0
  79. package/dist/src/cli/audit.js +460 -0
  80. package/dist/src/cli/audit.js.map +1 -0
  81. package/dist/src/cli/channel-telegram.d.ts +806 -0
  82. package/dist/src/cli/channel-telegram.js +2063 -0
  83. package/dist/src/cli/channel-telegram.js.map +1 -0
  84. package/dist/src/cli/channel-web.d.ts +131 -0
  85. package/dist/src/cli/channel-web.js +357 -0
  86. package/dist/src/cli/channel-web.js.map +1 -0
  87. package/dist/src/cli/channel.d.ts +71 -0
  88. package/dist/src/cli/channel.js +438 -0
  89. package/dist/src/cli/channel.js.map +1 -0
  90. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  91. package/dist/src/cli/checkpoint-tap.js +238 -0
  92. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  93. package/dist/src/cli/codex.d.ts +2 -0
  94. package/dist/src/cli/codex.js +172 -0
  95. package/dist/src/cli/codex.js.map +1 -0
  96. package/dist/src/cli/coverage.d.ts +61 -0
  97. package/dist/src/cli/coverage.js +343 -0
  98. package/dist/src/cli/coverage.js.map +1 -0
  99. package/dist/src/cli/daemon.d.ts +120 -0
  100. package/dist/src/cli/daemon.js +631 -0
  101. package/dist/src/cli/daemon.js.map +1 -0
  102. package/dist/src/cli/doctor.d.ts +129 -0
  103. package/dist/src/cli/doctor.js +2762 -0
  104. package/dist/src/cli/doctor.js.map +1 -0
  105. package/dist/src/cli/env.d.ts +65 -0
  106. package/dist/src/cli/env.js +302 -0
  107. package/dist/src/cli/env.js.map +1 -0
  108. package/dist/src/cli/execute.d.ts +202 -0
  109. package/dist/src/cli/execute.js +1682 -0
  110. package/dist/src/cli/execute.js.map +1 -0
  111. package/dist/src/cli/exit-codes.d.ts +73 -0
  112. package/dist/src/cli/exit-codes.js +82 -0
  113. package/dist/src/cli/exit-codes.js.map +1 -0
  114. package/dist/src/cli/feedback.d.ts +60 -0
  115. package/dist/src/cli/feedback.js +205 -0
  116. package/dist/src/cli/feedback.js.map +1 -0
  117. package/dist/src/cli/gate-window.d.ts +40 -0
  118. package/dist/src/cli/gate-window.js +294 -0
  119. package/dist/src/cli/gate-window.js.map +1 -0
  120. package/dist/src/cli/gate.d.ts +68 -0
  121. package/dist/src/cli/gate.js +557 -0
  122. package/dist/src/cli/gate.js.map +1 -0
  123. package/dist/src/cli/git-scope.d.ts +190 -0
  124. package/dist/src/cli/git-scope.js +295 -0
  125. package/dist/src/cli/git-scope.js.map +1 -0
  126. package/dist/src/cli/gloss-attach.d.ts +85 -0
  127. package/dist/src/cli/gloss-attach.js +107 -0
  128. package/dist/src/cli/gloss-attach.js.map +1 -0
  129. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  130. package/dist/src/cli/gloss-codex-child.js +149 -0
  131. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  132. package/dist/src/cli/gloss-codex.d.ts +24 -0
  133. package/dist/src/cli/gloss-codex.js +255 -0
  134. package/dist/src/cli/gloss-codex.js.map +1 -0
  135. package/dist/src/cli/gloss-options.d.ts +42 -0
  136. package/dist/src/cli/gloss-options.js +79 -0
  137. package/dist/src/cli/gloss-options.js.map +1 -0
  138. package/dist/src/cli/gloss.d.ts +265 -0
  139. package/dist/src/cli/gloss.js +362 -0
  140. package/dist/src/cli/gloss.js.map +1 -0
  141. package/dist/src/cli/help.d.ts +103 -0
  142. package/dist/src/cli/help.js +2339 -0
  143. package/dist/src/cli/help.js.map +1 -0
  144. package/dist/src/cli/hook-codex.d.ts +78 -0
  145. package/dist/src/cli/hook-codex.js +167 -0
  146. package/dist/src/cli/hook-codex.js.map +1 -0
  147. package/dist/src/cli/hook.d.ts +331 -0
  148. package/dist/src/cli/hook.js +2849 -0
  149. package/dist/src/cli/hook.js.map +1 -0
  150. package/dist/src/cli/import.d.ts +35 -0
  151. package/dist/src/cli/import.js +175 -0
  152. package/dist/src/cli/import.js.map +1 -0
  153. package/dist/src/cli/init.d.ts +84 -0
  154. package/dist/src/cli/init.js +336 -0
  155. package/dist/src/cli/init.js.map +1 -0
  156. package/dist/src/cli/instructions.d.ts +23 -0
  157. package/dist/src/cli/instructions.js +262 -0
  158. package/dist/src/cli/instructions.js.map +1 -0
  159. package/dist/src/cli/journal.d.ts +41 -0
  160. package/dist/src/cli/journal.js +238 -0
  161. package/dist/src/cli/journal.js.map +1 -0
  162. package/dist/src/cli/log-advance.d.ts +287 -0
  163. package/dist/src/cli/log-advance.js +840 -0
  164. package/dist/src/cli/log-advance.js.map +1 -0
  165. package/dist/src/cli/log-anchor.d.ts +176 -0
  166. package/dist/src/cli/log-anchor.js +387 -0
  167. package/dist/src/cli/log-anchor.js.map +1 -0
  168. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  169. package/dist/src/cli/log-checkpoint.js +128 -0
  170. package/dist/src/cli/log-checkpoint.js.map +1 -0
  171. package/dist/src/cli/log-sync.d.ts +243 -0
  172. package/dist/src/cli/log-sync.js +849 -0
  173. package/dist/src/cli/log-sync.js.map +1 -0
  174. package/dist/src/cli/log-verbs.d.ts +16 -0
  175. package/dist/src/cli/log-verbs.js +360 -0
  176. package/dist/src/cli/log-verbs.js.map +1 -0
  177. package/dist/src/cli/long-help.d.ts +70 -0
  178. package/dist/src/cli/long-help.js +148 -0
  179. package/dist/src/cli/long-help.js.map +1 -0
  180. package/dist/src/cli/main.d.ts +77 -0
  181. package/dist/src/cli/main.js +1206 -0
  182. package/dist/src/cli/main.js.map +1 -0
  183. package/dist/src/cli/mcp.d.ts +52 -0
  184. package/dist/src/cli/mcp.js +306 -0
  185. package/dist/src/cli/mcp.js.map +1 -0
  186. package/dist/src/cli/paths.d.ts +56 -0
  187. package/dist/src/cli/paths.js +79 -0
  188. package/dist/src/cli/paths.js.map +1 -0
  189. package/dist/src/cli/payload.d.ts +58 -0
  190. package/dist/src/cli/payload.js +253 -0
  191. package/dist/src/cli/payload.js.map +1 -0
  192. package/dist/src/cli/policy.d.ts +43 -0
  193. package/dist/src/cli/policy.js +229 -0
  194. package/dist/src/cli/policy.js.map +1 -0
  195. package/dist/src/cli/preflight.d.ts +363 -0
  196. package/dist/src/cli/preflight.js +1175 -0
  197. package/dist/src/cli/preflight.js.map +1 -0
  198. package/dist/src/cli/progress.d.ts +78 -0
  199. package/dist/src/cli/progress.js +112 -0
  200. package/dist/src/cli/progress.js.map +1 -0
  201. package/dist/src/cli/prompt.d.ts +209 -0
  202. package/dist/src/cli/prompt.js +312 -0
  203. package/dist/src/cli/prompt.js.map +1 -0
  204. package/dist/src/cli/quickstart.d.ts +46 -0
  205. package/dist/src/cli/quickstart.js +297 -0
  206. package/dist/src/cli/quickstart.js.map +1 -0
  207. package/dist/src/cli/records.d.ts +34 -0
  208. package/dist/src/cli/records.js +66 -0
  209. package/dist/src/cli/records.js.map +1 -0
  210. package/dist/src/cli/render.d.ts +22 -0
  211. package/dist/src/cli/render.js +132 -0
  212. package/dist/src/cli/render.js.map +1 -0
  213. package/dist/src/cli/sandbox.d.ts +51 -0
  214. package/dist/src/cli/sandbox.js +150 -0
  215. package/dist/src/cli/sandbox.js.map +1 -0
  216. package/dist/src/cli/scaffold.d.ts +79 -0
  217. package/dist/src/cli/scaffold.js +137 -0
  218. package/dist/src/cli/scaffold.js.map +1 -0
  219. package/dist/src/cli/setup-adapter.d.ts +137 -0
  220. package/dist/src/cli/setup-adapter.js +509 -0
  221. package/dist/src/cli/setup-adapter.js.map +1 -0
  222. package/dist/src/cli/setup-channel.d.ts +117 -0
  223. package/dist/src/cli/setup-channel.js +635 -0
  224. package/dist/src/cli/setup-channel.js.map +1 -0
  225. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  226. package/dist/src/cli/setup-checkpoint.js +196 -0
  227. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  228. package/dist/src/cli/setup-common.d.ts +275 -0
  229. package/dist/src/cli/setup-common.js +376 -0
  230. package/dist/src/cli/setup-common.js.map +1 -0
  231. package/dist/src/cli/setup-flow.d.ts +287 -0
  232. package/dist/src/cli/setup-flow.js +476 -0
  233. package/dist/src/cli/setup-flow.js.map +1 -0
  234. package/dist/src/cli/setup-service.d.ts +96 -0
  235. package/dist/src/cli/setup-service.js +308 -0
  236. package/dist/src/cli/setup-service.js.map +1 -0
  237. package/dist/src/cli/setup.d.ts +202 -0
  238. package/dist/src/cli/setup.js +473 -0
  239. package/dist/src/cli/setup.js.map +1 -0
  240. package/dist/src/cli/style.d.ts +320 -0
  241. package/dist/src/cli/style.js +469 -0
  242. package/dist/src/cli/style.js.map +1 -0
  243. package/dist/src/cli/token.d.ts +39 -0
  244. package/dist/src/cli/token.js +274 -0
  245. package/dist/src/cli/token.js.map +1 -0
  246. package/dist/src/cli/up.d.ts +155 -0
  247. package/dist/src/cli/up.js +849 -0
  248. package/dist/src/cli/up.js.map +1 -0
  249. package/dist/src/cli/usage.d.ts +37 -0
  250. package/dist/src/cli/usage.js +91 -0
  251. package/dist/src/cli/usage.js.map +1 -0
  252. package/dist/src/cli/values.d.ts +40 -0
  253. package/dist/src/cli/values.js +189 -0
  254. package/dist/src/cli/values.js.map +1 -0
  255. package/dist/src/cli/vault.d.ts +59 -0
  256. package/dist/src/cli/vault.js +362 -0
  257. package/dist/src/cli/vault.js.map +1 -0
  258. package/dist/src/cli/verb-registry.d.ts +76 -0
  259. package/dist/src/cli/verb-registry.js +2341 -0
  260. package/dist/src/cli/verb-registry.js.map +1 -0
  261. package/dist/src/cli/wordmark.d.ts +31 -0
  262. package/dist/src/cli/wordmark.js +52 -0
  263. package/dist/src/cli/wordmark.js.map +1 -0
  264. package/dist/src/codex/doctor.d.ts +13 -0
  265. package/dist/src/codex/doctor.js +41 -0
  266. package/dist/src/codex/doctor.js.map +1 -0
  267. package/dist/src/codex/manifest.d.ts +49 -0
  268. package/dist/src/codex/manifest.js +103 -0
  269. package/dist/src/codex/manifest.js.map +1 -0
  270. package/dist/src/codex/templates.d.ts +41 -0
  271. package/dist/src/codex/templates.js +319 -0
  272. package/dist/src/codex/templates.js.map +1 -0
  273. package/dist/src/codex/trust.d.ts +19 -0
  274. package/dist/src/codex/trust.js +183 -0
  275. package/dist/src/codex/trust.js.map +1 -0
  276. package/dist/src/codex/workspace-plan.d.ts +131 -0
  277. package/dist/src/codex/workspace-plan.js +561 -0
  278. package/dist/src/codex/workspace-plan.js.map +1 -0
  279. package/dist/src/core/actor.d.ts +2 -0
  280. package/dist/src/core/actor.js +5 -0
  281. package/dist/src/core/actor.js.map +1 -0
  282. package/dist/src/core/advance-cycle.d.ts +170 -0
  283. package/dist/src/core/advance-cycle.js +200 -0
  284. package/dist/src/core/advance-cycle.js.map +1 -0
  285. package/dist/src/core/agents-md.d.ts +276 -0
  286. package/dist/src/core/agents-md.js +747 -0
  287. package/dist/src/core/agents-md.js.map +1 -0
  288. package/dist/src/core/apply-patch.d.ts +49 -0
  289. package/dist/src/core/apply-patch.js +266 -0
  290. package/dist/src/core/apply-patch.js.map +1 -0
  291. package/dist/src/core/attest.d.ts +420 -0
  292. package/dist/src/core/attest.js +589 -0
  293. package/dist/src/core/attest.js.map +1 -0
  294. package/dist/src/core/audit.d.ts +492 -0
  295. package/dist/src/core/audit.js +882 -0
  296. package/dist/src/core/audit.js.map +1 -0
  297. package/dist/src/core/budgets.d.ts +238 -0
  298. package/dist/src/core/budgets.js +449 -0
  299. package/dist/src/core/budgets.js.map +1 -0
  300. package/dist/src/core/checkpoint.d.ts +500 -0
  301. package/dist/src/core/checkpoint.js +738 -0
  302. package/dist/src/core/checkpoint.js.map +1 -0
  303. package/dist/src/core/child-env.d.ts +88 -0
  304. package/dist/src/core/child-env.js +86 -0
  305. package/dist/src/core/child-env.js.map +1 -0
  306. package/dist/src/core/clock.d.ts +52 -0
  307. package/dist/src/core/clock.js +43 -0
  308. package/dist/src/core/clock.js.map +1 -0
  309. package/dist/src/core/command-class.d.ts +543 -0
  310. package/dist/src/core/command-class.js +2356 -0
  311. package/dist/src/core/command-class.js.map +1 -0
  312. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  313. package/dist/src/core/coverage-sources/adapter.js +71 -0
  314. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  315. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  316. package/dist/src/core/coverage-sources/gh.js +136 -0
  317. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  318. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  319. package/dist/src/core/coverage-sources/git.js +269 -0
  320. package/dist/src/core/coverage-sources/git.js.map +1 -0
  321. package/dist/src/core/coverage.d.ts +217 -0
  322. package/dist/src/core/coverage.js +337 -0
  323. package/dist/src/core/coverage.js.map +1 -0
  324. package/dist/src/core/credential-spec.d.ts +72 -0
  325. package/dist/src/core/credential-spec.js +23 -0
  326. package/dist/src/core/credential-spec.js.map +1 -0
  327. package/dist/src/core/dark-session.d.ts +331 -0
  328. package/dist/src/core/dark-session.js +714 -0
  329. package/dist/src/core/dark-session.js.map +1 -0
  330. package/dist/src/core/decision-refusal.d.ts +185 -0
  331. package/dist/src/core/decision-refusal.js +265 -0
  332. package/dist/src/core/decision-refusal.js.map +1 -0
  333. package/dist/src/core/env-file.d.ts +450 -0
  334. package/dist/src/core/env-file.js +837 -0
  335. package/dist/src/core/env-file.js.map +1 -0
  336. package/dist/src/core/execute.d.ts +858 -0
  337. package/dist/src/core/execute.js +1271 -0
  338. package/dist/src/core/execute.js.map +1 -0
  339. package/dist/src/core/frontmatter.d.ts +78 -0
  340. package/dist/src/core/frontmatter.js +100 -0
  341. package/dist/src/core/frontmatter.js.map +1 -0
  342. package/dist/src/core/gate-window.d.ts +312 -0
  343. package/dist/src/core/gate-window.js +506 -0
  344. package/dist/src/core/gate-window.js.map +1 -0
  345. package/dist/src/core/gate.d.ts +1364 -0
  346. package/dist/src/core/gate.js +3002 -0
  347. package/dist/src/core/gate.js.map +1 -0
  348. package/dist/src/core/git-run.d.ts +73 -0
  349. package/dist/src/core/git-run.js +93 -0
  350. package/dist/src/core/git-run.js.map +1 -0
  351. package/dist/src/core/harness-version.d.ts +157 -0
  352. package/dist/src/core/harness-version.js +211 -0
  353. package/dist/src/core/harness-version.js.map +1 -0
  354. package/dist/src/core/harness-wait.d.ts +55 -0
  355. package/dist/src/core/harness-wait.js +58 -0
  356. package/dist/src/core/harness-wait.js.map +1 -0
  357. package/dist/src/core/head-retry.d.ts +107 -0
  358. package/dist/src/core/head-retry.js +121 -0
  359. package/dist/src/core/head-retry.js.map +1 -0
  360. package/dist/src/core/instance.d.ts +253 -0
  361. package/dist/src/core/instance.js +319 -0
  362. package/dist/src/core/instance.js.map +1 -0
  363. package/dist/src/core/intake-limits.d.ts +247 -0
  364. package/dist/src/core/intake-limits.js +350 -0
  365. package/dist/src/core/intake-limits.js.map +1 -0
  366. package/dist/src/core/jcs.d.ts +52 -0
  367. package/dist/src/core/jcs.js +132 -0
  368. package/dist/src/core/jcs.js.map +1 -0
  369. package/dist/src/core/journal.d.ts +144 -0
  370. package/dist/src/core/journal.js +200 -0
  371. package/dist/src/core/journal.js.map +1 -0
  372. package/dist/src/core/live-draw.d.ts +436 -0
  373. package/dist/src/core/live-draw.js +703 -0
  374. package/dist/src/core/live-draw.js.map +1 -0
  375. package/dist/src/core/log-reconcile.d.ts +89 -0
  376. package/dist/src/core/log-reconcile.js +136 -0
  377. package/dist/src/core/log-reconcile.js.map +1 -0
  378. package/dist/src/core/log-subscribe.d.ts +36 -0
  379. package/dist/src/core/log-subscribe.js +162 -0
  380. package/dist/src/core/log-subscribe.js.map +1 -0
  381. package/dist/src/core/log.d.ts +278 -0
  382. package/dist/src/core/log.js +546 -0
  383. package/dist/src/core/log.js.map +1 -0
  384. package/dist/src/core/loop.d.ts +274 -0
  385. package/dist/src/core/loop.js +487 -0
  386. package/dist/src/core/loop.js.map +1 -0
  387. package/dist/src/core/md-fence.d.ts +41 -0
  388. package/dist/src/core/md-fence.js +74 -0
  389. package/dist/src/core/md-fence.js.map +1 -0
  390. package/dist/src/core/money.d.ts +147 -0
  391. package/dist/src/core/money.js +195 -0
  392. package/dist/src/core/money.js.map +1 -0
  393. package/dist/src/core/payload-census.d.ts +74 -0
  394. package/dist/src/core/payload-census.js +146 -0
  395. package/dist/src/core/payload-census.js.map +1 -0
  396. package/dist/src/core/payload-store.d.ts +175 -0
  397. package/dist/src/core/payload-store.js +340 -0
  398. package/dist/src/core/payload-store.js.map +1 -0
  399. package/dist/src/core/payload.d.ts +71 -0
  400. package/dist/src/core/payload.js +80 -0
  401. package/dist/src/core/payload.js.map +1 -0
  402. package/dist/src/core/policy-diff.d.ts +292 -0
  403. package/dist/src/core/policy-diff.js +588 -0
  404. package/dist/src/core/policy-diff.js.map +1 -0
  405. package/dist/src/core/policy-expectations.d.ts +199 -0
  406. package/dist/src/core/policy-expectations.js +394 -0
  407. package/dist/src/core/policy-expectations.js.map +1 -0
  408. package/dist/src/core/policy-explain.d.ts +150 -0
  409. package/dist/src/core/policy-explain.js +258 -0
  410. package/dist/src/core/policy-explain.js.map +1 -0
  411. package/dist/src/core/policy-load.d.ts +527 -0
  412. package/dist/src/core/policy-load.js +536 -0
  413. package/dist/src/core/policy-load.js.map +1 -0
  414. package/dist/src/core/policy-match.d.ts +281 -0
  415. package/dist/src/core/policy-match.js +478 -0
  416. package/dist/src/core/policy-match.js.map +1 -0
  417. package/dist/src/core/policy-proposal.d.ts +265 -0
  418. package/dist/src/core/policy-proposal.js +458 -0
  419. package/dist/src/core/policy-proposal.js.map +1 -0
  420. package/dist/src/core/prompt-layout.d.ts +221 -0
  421. package/dist/src/core/prompt-layout.js +422 -0
  422. package/dist/src/core/prompt-layout.js.map +1 -0
  423. package/dist/src/core/protected-path-guard.d.ts +453 -0
  424. package/dist/src/core/protected-path-guard.js +1566 -0
  425. package/dist/src/core/protected-path-guard.js.map +1 -0
  426. package/dist/src/core/registration.d.ts +25 -0
  427. package/dist/src/core/registration.js +39 -0
  428. package/dist/src/core/registration.js.map +1 -0
  429. package/dist/src/core/reindex.d.ts +99 -0
  430. package/dist/src/core/reindex.js +336 -0
  431. package/dist/src/core/reindex.js.map +1 -0
  432. package/dist/src/core/sampler.d.ts +313 -0
  433. package/dist/src/core/sampler.js +388 -0
  434. package/dist/src/core/sampler.js.map +1 -0
  435. package/dist/src/core/sandbox.d.ts +290 -0
  436. package/dist/src/core/sandbox.js +424 -0
  437. package/dist/src/core/sandbox.js.map +1 -0
  438. package/dist/src/core/seal.d.ts +165 -0
  439. package/dist/src/core/seal.js +290 -0
  440. package/dist/src/core/seal.js.map +1 -0
  441. package/dist/src/core/state.d.ts +505 -0
  442. package/dist/src/core/state.js +1009 -0
  443. package/dist/src/core/state.js.map +1 -0
  444. package/dist/src/core/task-file.d.ts +185 -0
  445. package/dist/src/core/task-file.js +464 -0
  446. package/dist/src/core/task-file.js.map +1 -0
  447. package/dist/src/core/telegram-config.d.ts +93 -0
  448. package/dist/src/core/telegram-config.js +114 -0
  449. package/dist/src/core/telegram-config.js.map +1 -0
  450. package/dist/src/core/token.d.ts +409 -0
  451. package/dist/src/core/token.js +561 -0
  452. package/dist/src/core/token.js.map +1 -0
  453. package/dist/src/core/validate.d.ts +138 -0
  454. package/dist/src/core/validate.js +0 -0
  455. package/dist/src/core/validate.js.map +1 -0
  456. package/dist/src/core/values.d.ts +137 -0
  457. package/dist/src/core/values.js +153 -0
  458. package/dist/src/core/values.js.map +1 -0
  459. package/dist/src/core/vault.d.ts +291 -0
  460. package/dist/src/core/vault.js +612 -0
  461. package/dist/src/core/vault.js.map +1 -0
  462. package/dist/src/core/verified-snapshot.d.ts +204 -0
  463. package/dist/src/core/verified-snapshot.js +506 -0
  464. package/dist/src/core/verified-snapshot.js.map +1 -0
  465. package/dist/src/core/verify.d.ts +336 -0
  466. package/dist/src/core/verify.js +549 -0
  467. package/dist/src/core/verify.js.map +1 -0
  468. package/dist/src/core/version.d.ts +8 -0
  469. package/dist/src/core/version.js +9 -0
  470. package/dist/src/core/version.js.map +1 -0
  471. package/dist/src/core/wysiwys.d.ts +370 -0
  472. package/dist/src/core/wysiwys.js +728 -0
  473. package/dist/src/core/wysiwys.js.map +1 -0
  474. package/dist/src/daemon/advance-child.d.ts +39 -0
  475. package/dist/src/daemon/advance-child.js +78 -0
  476. package/dist/src/daemon/advance-child.js.map +1 -0
  477. package/dist/src/daemon/advance.d.ts +466 -0
  478. package/dist/src/daemon/advance.js +849 -0
  479. package/dist/src/daemon/advance.js.map +1 -0
  480. package/dist/src/daemon/audit.d.ts +87 -0
  481. package/dist/src/daemon/audit.js +90 -0
  482. package/dist/src/daemon/audit.js.map +1 -0
  483. package/dist/src/daemon/daemon.d.ts +1180 -0
  484. package/dist/src/daemon/daemon.js +1988 -0
  485. package/dist/src/daemon/daemon.js.map +1 -0
  486. package/dist/src/daemon/dark-session.d.ts +64 -0
  487. package/dist/src/daemon/dark-session.js +119 -0
  488. package/dist/src/daemon/dark-session.js.map +1 -0
  489. package/dist/src/daemon/draw-child.d.ts +36 -0
  490. package/dist/src/daemon/draw-child.js +132 -0
  491. package/dist/src/daemon/draw-child.js.map +1 -0
  492. package/dist/src/daemon/draw.d.ts +154 -0
  493. package/dist/src/daemon/draw.js +458 -0
  494. package/dist/src/daemon/draw.js.map +1 -0
  495. package/dist/src/daemon/git-evidence.d.ts +173 -0
  496. package/dist/src/daemon/git-evidence.js +345 -0
  497. package/dist/src/daemon/git-evidence.js.map +1 -0
  498. package/dist/src/daemon/projection.d.ts +180 -0
  499. package/dist/src/daemon/projection.js +233 -0
  500. package/dist/src/daemon/projection.js.map +1 -0
  501. package/dist/src/daemon/prune.d.ts +207 -0
  502. package/dist/src/daemon/prune.js +376 -0
  503. package/dist/src/daemon/prune.js.map +1 -0
  504. package/dist/src/mcp/http.d.ts +113 -0
  505. package/dist/src/mcp/http.js +343 -0
  506. package/dist/src/mcp/http.js.map +1 -0
  507. package/dist/src/mcp/server.d.ts +265 -0
  508. package/dist/src/mcp/server.js +602 -0
  509. package/dist/src/mcp/server.js.map +1 -0
  510. package/docs/adapter-api.md +106 -0
  511. package/docs/cli-reference.md +5716 -0
  512. package/docs/codex-enforced-session.md +30 -0
  513. package/package.json +53 -4
  514. package/schema/.gitkeep +0 -0
  515. package/schema/LICENSE +117 -0
  516. package/schema/codex-instance.schema.json +82 -0
  517. package/schema/envelope.schema.json +137 -0
  518. package/schema/event.schema.json +1811 -0
  519. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  520. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  521. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  522. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  523. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  524. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  525. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  526. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  527. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  528. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  529. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  530. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  531. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  532. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  533. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  534. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  535. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  536. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  537. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  538. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  539. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  540. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  541. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  542. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  543. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  544. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  545. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  546. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  547. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  548. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  549. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  550. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  551. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  552. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  553. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  554. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  555. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  556. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  557. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  558. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  559. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  560. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  561. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  562. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  563. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  564. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  565. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  566. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  567. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  568. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  569. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  570. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  571. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  572. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  573. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  574. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  575. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  576. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  577. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  578. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  579. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  580. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  581. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  582. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  583. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  584. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  585. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  586. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  587. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  588. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  589. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  590. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  591. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  592. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  593. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  594. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  595. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  596. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  597. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  598. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  599. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  600. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  601. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  602. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  603. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  604. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  605. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  606. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  607. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  608. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  609. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  610. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  611. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  612. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  613. package/schema/fixtures/event/valid/execution-started.json +14 -0
  614. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  615. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  616. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  617. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  618. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  619. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  620. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  621. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  622. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  623. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  624. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  625. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  626. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  627. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  628. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  629. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  630. package/schema/fixtures/event/valid/spec-example.json +15 -0
  631. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  632. package/schema/fixtures/event/valid/task-registered.json +14 -0
  633. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  634. package/schema/fixtures/hash/known-answer.json +74 -0
  635. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  636. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  637. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  638. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  639. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  640. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  641. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  642. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  643. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  644. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  645. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  646. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  647. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  648. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  649. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  650. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  651. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  652. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  653. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  654. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  655. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  656. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  657. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  658. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  659. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  660. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  661. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  662. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  663. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  664. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  665. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  666. package/schema/fixtures/policy/valid/canonical.json +47 -0
  667. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  668. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  669. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  670. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  671. package/schema/fixtures/policy/valid/human-only.json +9 -0
  672. package/schema/fixtures/policy/valid/minimal.json +6 -0
  673. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  674. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  675. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  676. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  677. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  678. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  679. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  680. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  681. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  682. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  683. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  684. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  685. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  686. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  687. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  688. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  689. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  690. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  691. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  692. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  693. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  694. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  695. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  696. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  697. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  698. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  699. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  700. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  701. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  702. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  703. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  704. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  705. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  706. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  707. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  708. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  709. package/schema/fixtures/values/invalid/version-string.json +1 -0
  710. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  711. package/schema/fixtures/values/valid/full.json +20 -0
  712. package/schema/fixtures/values/valid/minimal.json +1 -0
  713. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  714. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  715. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  716. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  717. package/schema/fixtures/values-md/valid/absent.md +50 -0
  718. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  719. package/schema/policy.schema.json +501 -0
  720. package/schema/sample-record.schema.json +26 -0
  721. package/schema/values.schema.json +55 -0
  722. package/templates/codex/README.md +9 -0
@@ -0,0 +1,1682 @@
1
+ /**
2
+ * The execution verbs of SPEC.md §10.1: `approval run`, `approval wait`,
3
+ * `approval status`, and `approval queue`.
4
+ *
5
+ * As everywhere else in this CLI, **no logic lives here.** Authorization,
6
+ * budgets, loop safety, and the appends are `core/execute.ts`; state derivation
7
+ * is `core/gate.ts`; chain verification is `core/verify.ts`. This file splits
8
+ * argv, resolves paths and identity, spawns the child, chooses an exit code, and
9
+ * formats output.
10
+ *
11
+ * Four choices are load-bearing enough to state plainly.
12
+ *
13
+ * **`approval run` is transparent.** Its own exit code is the CHILD's exit code
14
+ * — a wrapper that swallowed it would break every `make`, every CI step, and
15
+ * every `&&` that ever wrapped a command. run's own refusals are the only thing
16
+ * that can produce a different code, and they all happen *before* the child is
17
+ * spawned: 5 when no valid token was presented, 2 for a usage error, 4 for a
18
+ * filesystem fact, 3 for a torn tail, 1 for any other gate refusal.
19
+ *
20
+ * **Exit 5 is new, and it is only here.** The human-settled design (2026-08-06)
21
+ * asked for a distinct code when `run` refuses for want of a token, and
22
+ * `exit-codes.ts` defines it as an addition to the frozen table rather than a
23
+ * redefinition of anything in it. `approval wait` likewise adds 6 for timeout.
24
+ * No other command emits either.
25
+ *
26
+ * **The child owns stdout.** `run` inherits stdio, so the child's output is not
27
+ * captured, buffered, or interleaved with ours. That leaves `--json` nowhere
28
+ * safe to print on stdout, so run's own JSON summary goes to **stderr** — the
29
+ * one place we can write without corrupting a stream the child is entitled to.
30
+ * This is stated in `--help` because it is the single place this CLI departs
31
+ * from "one JSON object on stdout".
32
+ *
33
+ * **`run` and the adapter contract are two callers of one core path.** A command
34
+ * is an adapter whose `act` is a spawn, whose payload is SPEC.md §6.2's `{argv,
35
+ * cwd}`, and whose credentials are the ambient environment.
36
+ * `src/adapters/contract.ts` wraps the same `startExecution` / `finishExecution`
37
+ * pair for adapters that are objects rather than processes; `run` calls the core
38
+ * verbs directly because its stdio inheritance, exit-code transparency, and `--`
39
+ * argv split are CLI concerns with nothing to do with adapters. What both must
40
+ * obey belongs in `core/execute.ts`, where both already read it; a rule added to
41
+ * the adapter contract alone protects adapters and not this verb.
42
+ *
43
+ * **`status` and `queue` answer different questions.** `queue` is the pending
44
+ * decision inbox and nothing else: requests awaiting a human, inside their TTL.
45
+ * `status` is system health: attestation, dangling executions, budget headroom,
46
+ * the latest verification, loop escalations. A dangling execution appears in
47
+ * `status` and never in `queue` — nobody is being asked to decide it, and
48
+ * putting operational debris in a human's approval inbox is how inboxes get
49
+ * ignored.
50
+ */
51
+ import { spawnSync } from "node:child_process";
52
+ import { readdirSync, rmSync } from "node:fs";
53
+ import { constants as osConstants } from "node:os";
54
+ import { dirname, isAbsolute, resolve as resolvePathSegments } from "node:path";
55
+ import { HUMAN_ACTOR_ENV, checkAttestation, resolveHumanActor } from "../core/attest.js";
56
+ import { RESOLVE_DANGLING_COMMAND, proveDanglingAdvances, } from "../core/advance-cycle.js";
57
+ import { openObligations } from "../core/audit.js";
58
+ import { evaluateBudgets } from "../core/budgets.js";
59
+ import { declaredCredentialsForClass } from "../adapters/registry.js";
60
+ import { childEnvironment } from "../core/child-env.js";
61
+ import { coverageReport } from "../core/coverage.js";
62
+ import { credentialPathsFor, detectSandbox, resolveExecutable, sandboxPosture, sandboxRequired, wrapForSandbox, } from "../core/sandbox.js";
63
+ import { DEFAULT_TRUNK_REF, defaultRange, observeGit, } from "../core/coverage-sources/git.js";
64
+ import { danglingExecutions, findDeclaration, finishExecution, indexDeclarations, indeterminateExecutions, isReconcileResolution, loopEscalation, reconcileExecution, resolveExecution, startExecution, } from "../core/execute.js";
65
+ import { harnessLoopEscalation, harnessOutcomeCoverage, loopClearance } from "../core/loop.js";
66
+ import { isPayloadHash, runPayloadHash } from "../core/payload.js";
67
+ import { payloadStoreCensus } from "../core/payload-census.js";
68
+ import { payloadStoreDirFor } from "../core/payload-store.js";
69
+ import { withdraw } from "../core/gate.js";
70
+ import { openGateWindow } from "../core/gate-window.js";
71
+ import { keyStoreDirFor } from "../core/seal.js";
72
+ import { readVerifiedRecords, requestState } from "../core/state.js";
73
+ import { deliveredToken } from "../core/token.js";
74
+ import { passphraseEnvFor } from "../core/vault.js";
75
+ import { loadPolicy, parseDuration, POLICY_FILENAMES } from "../core/policy-load.js";
76
+ import { verify } from "../core/verify.js";
77
+ import { boolFlag, parseFlags, stringFlag } from "./args.js";
78
+ import { EXIT_INTEGRITY, EXIT_IO, EXIT_NO_TOKEN, EXIT_OK, EXIT_TIMEOUT, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
79
+ import { repoRoot } from "./git-scope.js";
80
+ import { publishedState } from "./log-advance.js";
81
+ import { confirmUntil, createPrompter } from "./prompt.js";
82
+ import { EXECUTION_HELP, QUEUE_HELP, RECONCILE_HELP, RESOLVE_HELP, RUN_HELP, STATUS_HELP, WAIT_HELP, } from "./help.js";
83
+ import { DEFAULT_LOG_PATH, preflightLog, resolvePath } from "./paths.js";
84
+ import { refusal as renderRefusal, relPath, style, table, } from "./style.js";
85
+ import { usageErrorText } from "./usage.js";
86
+ /** Identity accepted by `run`: a person or an agent, never the runtime. */
87
+ const PRINCIPAL_ACTOR = /^(human|agent):.+/u;
88
+ /** Poll interval for `approval wait`, in milliseconds. */
89
+ const DEFAULT_WAIT_INTERVAL_MS = 500;
90
+ /** Exit code recorded when the command itself could not be spawned. */
91
+ const EXIT_COMMAND_NOT_RUN = 127;
92
+ const COMMON_FLAGS = {
93
+ "--log": "string",
94
+ "--json": "boolean",
95
+ "--help": "boolean",
96
+ "-h": "boolean",
97
+ };
98
+ const POLICY_FLAGS = {
99
+ "--policy": "string",
100
+ "--dir": "string",
101
+ };
102
+ function absolute(value, cwd) {
103
+ return isAbsolute(value) ? value : resolvePathSegments(cwd, value);
104
+ }
105
+ function usageError(streams, json, message, helpText) {
106
+ if (json)
107
+ streams.err(`${JSON.stringify({ error: { code: "usage", message } })}\n`);
108
+ else
109
+ streams.err(usageErrorText(message, helpText));
110
+ return EXIT_USAGE;
111
+ }
112
+ function ioError(streams, json, message) {
113
+ if (json)
114
+ streams.err(`${JSON.stringify({ error: { code: "io", message } })}\n`);
115
+ else
116
+ streams.err(`approval: ${message}\n`);
117
+ return EXIT_IO;
118
+ }
119
+ function emitJson(streams, value) {
120
+ streams.out(`${JSON.stringify(value)}\n`);
121
+ }
122
+ /** The clock is read here, at the edge, and handed to core. */
123
+ function now() {
124
+ return new Date().toISOString();
125
+ }
126
+ /**
127
+ * Map an execution refusal onto the exit table.
128
+ *
129
+ * `token-required` is the one addition: exit 5, and only `run` can produce it.
130
+ * Everything else follows the split the gate and token verbs already draw —
131
+ * filesystem facts are 4, a crashed write is 3, and every decision the runtime
132
+ * itself made is 1.
133
+ *
134
+ * Exported because `approval adapter` (APRV-69) is the second caller of the core
135
+ * execution path and must map its refusals identically. Two copies of this
136
+ * switch would drift the first time a code was added, and an agent's retry logic
137
+ * keys on the difference between 5 and 1.
138
+ */
139
+ export function executeRefusalExitCode(refusal) {
140
+ switch (refusal.code) {
141
+ case "token-required":
142
+ return EXIT_NO_TOKEN;
143
+ case "log-unreadable":
144
+ return EXIT_IO;
145
+ case "log-torn-tail":
146
+ return EXIT_TORN_TAIL;
147
+ case "append-failed":
148
+ switch (refusal.append?.code) {
149
+ case "corrupt-tail":
150
+ return EXIT_TORN_TAIL;
151
+ case "io":
152
+ case "lock-timeout":
153
+ return EXIT_IO;
154
+ default:
155
+ return EXIT_INTEGRITY;
156
+ }
157
+ default:
158
+ return EXIT_INTEGRITY;
159
+ }
160
+ }
161
+ function emitRefusal(streams, json, refusal) {
162
+ if (json) {
163
+ const error = { code: refusal.code, message: refusal.message };
164
+ if (refusal.detail !== undefined)
165
+ error["detail"] = refusal.detail;
166
+ if (refusal.verdicts !== undefined)
167
+ error["verdicts"] = refusal.verdicts;
168
+ if (refusal.seq !== undefined)
169
+ error["seq"] = refusal.seq;
170
+ if (refusal.record !== undefined)
171
+ error["event_seq"] = refusal.record.seq;
172
+ streams.err(`${JSON.stringify({ ok: false, error })}\n`);
173
+ }
174
+ else {
175
+ // APRV-102: the one refusal shape — glyph, machine-readable code, message,
176
+ // and never a help page after it. No `fix:` line is invented: an execution
177
+ // refusal names a STATE (no token, already started, budget exhausted) and
178
+ // the repair depends on which, so a guessed command would be wrong more
179
+ // often than right. Argument and payload refusals, which DO have one
180
+ // command each, keep theirs.
181
+ streams.err(`${renderRefusal(style({ json }), refusal.code, refusal.message)}\n`);
182
+ }
183
+ return executeRefusalExitCode(refusal);
184
+ }
185
+ /** Where policy lives, from `--policy` / `--dir`, with the CLI's cwd default. */
186
+ function policyLocation(flags, cwd) {
187
+ const policyFlag = stringFlag(flags, "--policy");
188
+ const dirFlag = stringFlag(flags, "--dir");
189
+ if (policyFlag !== null)
190
+ return { file: absolute(policyFlag, cwd) };
191
+ return { dir: dirFlag === null ? cwd : absolute(dirFlag, cwd) };
192
+ }
193
+ function executeOptions(flags, cwd, token) {
194
+ return {
195
+ policy: policyLocation(flags, cwd),
196
+ ...(token === null ? {} : { token }),
197
+ };
198
+ }
199
+ /**
200
+ * The environment the granted child gets, and the count of what was withheld
201
+ * (APRV-205).
202
+ *
203
+ * Three inputs, none of them a flag. The policy names the passphrase variable
204
+ * (`vault.passphrase_env`); the credential-bearing prefixes and their allowlist
205
+ * come from the classifier's own list (APRV-194, exported for this); and the
206
+ * pass-through set is whatever adapter serves the DECLARED class of this action
207
+ * named in its `requiredCredentials` (APRV-169). The declaration is read from
208
+ * verified records — SPEC.md §11.1's first invariant, and the reason this is a
209
+ * second read of the log rather than a peek at the task file.
210
+ *
211
+ * A log this cannot read yields the empty pass-through set and a scrub that
212
+ * removes more, which is the fail-closed direction: `startExecution` is about to
213
+ * refuse the same read anyway, and if it somehow does not, the child is starved
214
+ * rather than fed.
215
+ */
216
+ function childEnvFor(logPath, actionKey, flags, cwd) {
217
+ const location = policyLocation(flags, cwd);
218
+ const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
219
+ const read = readVerifiedRecords(logPath);
220
+ const declared = read.ok ? findDeclaration(read.records, actionKey) : null;
221
+ return childEnvironment({
222
+ passphraseEnv: passphraseEnvFor(load),
223
+ declaredCredentials: declared === null ? [] : declaredCredentialsForClass(declared.class),
224
+ });
225
+ }
226
+ /**
227
+ * The wrapped spawn for a child that must not reach the network (APRV-193), or
228
+ * `null` when there is nothing to wrap.
229
+ *
230
+ * The allowance is the runtime's, not the caller's: outbound network denied,
231
+ * loopback with it (the gate's IPC is a file, so there is no socket to except),
232
+ * and the credential material beside the log unreadable. No flag widens it.
233
+ * `--no-sandbox` is all or nothing, and it is recorded.
234
+ */
235
+ function wrapExecutable(mechanism, command, args, env, logPath) {
236
+ const resolved = resolveExecutable(command, env);
237
+ if (resolved === null)
238
+ return null;
239
+ return wrapForSandbox(mechanism, resolved, args, {
240
+ loopback: false,
241
+ denyRead: credentialPathsFor(logPath),
242
+ });
243
+ }
244
+ /** `defaults.approval_ttl` in force, or `null` when the policy declares none. */
245
+ function ttlOf(flags, cwd) {
246
+ const location = policyLocation(flags, cwd);
247
+ const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
248
+ return load.ok ? load.durations.approvalTtlMs : null;
249
+ }
250
+ function front(argv, spec, helpText, streams, cwd) {
251
+ const json = argv.includes("--json");
252
+ const parsed = parseFlags(argv, spec);
253
+ if (!parsed.ok) {
254
+ return { kind: "handled", code: usageError(streams, json, parsed.message, helpText) };
255
+ }
256
+ if (boolFlag(parsed.flags, "--help") || boolFlag(parsed.flags, "-h")) {
257
+ streams.out(`${helpText}\n`);
258
+ return { kind: "handled", code: EXIT_OK };
259
+ }
260
+ return {
261
+ kind: "run",
262
+ flags: parsed.flags,
263
+ positionals: parsed.positionals,
264
+ json,
265
+ logPath: resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd),
266
+ };
267
+ }
268
+ // ===========================================================================
269
+ // approval run
270
+ // ===========================================================================
271
+ /**
272
+ * The exit code a child's outcome reduces to.
273
+ *
274
+ * A child killed by a signal has no exit code, so the shell convention is used:
275
+ * `128 + signal number` (SIGKILL → 137, SIGTERM → 143). It is recorded in the
276
+ * `execution.failed` payload as that number, and `run` exits with it, so a
277
+ * killed execution reads identically from the log and from the shell.
278
+ */
279
+ function childExitCode(status, signal) {
280
+ if (signal !== null) {
281
+ const numbers = osConstants.signals;
282
+ const number = numbers[signal];
283
+ return 128 + (number ?? 0);
284
+ }
285
+ return status ?? EXIT_COMMAND_NOT_RUN;
286
+ }
287
+ const INHERIT_CHILD_IO = { stdio: "inherit" };
288
+ export function commandRun(argv, streams, cwd, childIo = INHERIT_CHILD_IO) {
289
+ // `--` separates our flags from the child's argv, and the child's argv may
290
+ // legitimately contain anything at all — including flags this CLI knows. So
291
+ // the split happens on the RAW argv, before any parsing, and everything to the
292
+ // right of the first `--` is handed to the child untouched.
293
+ const separator = argv.indexOf("--");
294
+ const ours = separator === -1 ? argv : argv.slice(0, separator);
295
+ const childArgv = separator === -1 ? [] : argv.slice(separator + 1);
296
+ const outcome = front(ours, {
297
+ ...COMMON_FLAGS,
298
+ ...POLICY_FLAGS,
299
+ "--token": "string",
300
+ "--as": "string",
301
+ "--payload-hash": "string",
302
+ "--no-sandbox": "boolean",
303
+ }, RUN_HELP, streams, cwd);
304
+ if (outcome.kind === "handled")
305
+ return outcome.code;
306
+ const { flags, positionals, json, logPath } = outcome;
307
+ const actionKey = positionals[0];
308
+ if (actionKey === undefined) {
309
+ return usageError(streams, json, "missing <action-key> argument", RUN_HELP);
310
+ }
311
+ const extra = positionals[1];
312
+ if (extra !== undefined) {
313
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}; the command to run goes after \`--\``, RUN_HELP);
314
+ }
315
+ const command = childArgv[0];
316
+ if (command === undefined) {
317
+ return usageError(streams, json, "missing command: `approval run <action-key> [--token <t>] -- <cmd…>`", RUN_HELP);
318
+ }
319
+ const asFlag = stringFlag(flags, "--as");
320
+ const actor = asFlag === null ? resolveHumanActor() : asFlag;
321
+ if (actor === null || !PRINCIPAL_ACTOR.test(actor)) {
322
+ if (asFlag !== null) {
323
+ return usageError(streams, json, `--as expects human:<id> or agent:<id>, got ${JSON.stringify(asFlag)}`, RUN_HELP);
324
+ }
325
+ return usageError(streams, json, `no identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id> | agent:<id>`, RUN_HELP);
326
+ }
327
+ // Content binding (amended SPEC.md §6.2, §10.4). §6.2 defines `approval run`'s
328
+ // payload as "the argv array and cwd", so run computes the hash itself from
329
+ // the command it is about to spawn — an executor that had to be *told* what it
330
+ // was running could be told wrong.
331
+ //
332
+ // APRV-140 (red-team F3) closes the door that used to be here. `--payload-hash`
333
+ // was an OVERRIDE: when it was present the computation was skipped entirely,
334
+ // so presenting the grant's own hash while spawning arbitrary argv spent the
335
+ // token and ran something nobody approved. Adapters whose real payload is
336
+ // something else (a message body, a proposed record) were the reason, but they
337
+ // do not need this door: `src/adapters/contract.ts` hashes the bytes and calls
338
+ // core directly, and `approval consume` spends a token for a payload this
339
+ // process is not spawning. Neither is a plain `approval run` invocation, which
340
+ // is what an agent has.
341
+ //
342
+ // So the flag survives as a CHECK, never a substitute: run always recomputes,
343
+ // and a supplied value must equal what will actually spawn. The refusal is
344
+ // `payload-mismatch` — the same code the manual path already emits for the
345
+ // same fact, because an agent's response to it is the same either way — and it
346
+ // happens before `startExecution`, so nothing is appended and no token moves.
347
+ const hashFlag = stringFlag(flags, "--payload-hash");
348
+ if (hashFlag !== null && !isPayloadHash(hashFlag)) {
349
+ return usageError(streams, json, `--payload-hash expects 64 lowercase hex characters (SHA-256 over the RFC 8785 canonical serialization of the payload), got ${JSON.stringify(hashFlag)}`, RUN_HELP);
350
+ }
351
+ const payloadHash = runPayloadHash(childArgv, cwd);
352
+ if (hashFlag !== null && hashFlag !== payloadHash) {
353
+ return emitRefusal(streams, json, {
354
+ ok: false,
355
+ code: "payload-mismatch",
356
+ message: `--payload-hash ${hashFlag} is not the hash of the command this would spawn: ${JSON.stringify(childArgv[0])} and ${childArgv.length - 1} argument(s) in ${cwd} hash to ${payloadHash}. \`approval run\` recomputes the binding from the argv and cwd it is about to spawn and never accepts a caller's substitute for it (amended SPEC.md §10.4, APRV-140); the flag states what you believe you are running, and this is a refusal to run something else. Nothing was appended.`,
357
+ });
358
+ }
359
+ // APRV-205. The child's environment is built BEFORE `execution.started`,
360
+ // because the count of what was withheld is recorded on that event and a
361
+ // number written after the fact would be a number nobody measured. The child
362
+ // gets everything the session holds except the credential-bearing names: see
363
+ // `core/child-env.ts` for the three rules and for what this deliberately does
364
+ // NOT do (it is a scrub, not the sandbox APRV-193 designs).
365
+ const childEnv = childEnvFor(logPath, actionKey, flags, cwd);
366
+ // APRV-193. The room the child runs in, decided BEFORE `execution.started`
367
+ // for the same reason the count above is: the record says what happened, and
368
+ // a value written after the fact would be a value nobody measured.
369
+ //
370
+ // `granted` is the presence of a token, and it is the whole class test this
371
+ // verb needs. The manual path is a human's grant over these exact bytes, and
372
+ // `approval run` on a grant is the one door to the world the design leaves
373
+ // open (the registry for `deps.add`, the API host for `network.call`). Every
374
+ // other path — autonomous, and supervised-sampled — is one nobody was asked
375
+ // about, and that is where the child is starved. Reading the token rather
376
+ // than re-resolving the class keeps this decision on THIS side of the append,
377
+ // and it widens nothing an agent can reach on its own: a token that does not
378
+ // verify runs no command at all, so the loosening needs something a human
379
+ // minted (SPEC.md §11.1 invariant 4).
380
+ const posture = sandboxPosture({
381
+ optedOut: boolFlag(flags, "--no-sandbox"),
382
+ granted: stringFlag(flags, "--token") !== null,
383
+ detection: detectSandbox(),
384
+ ...(sandboxRequired() ? { requireSupported: true } : {}),
385
+ });
386
+ if (posture.kind === "refuse") {
387
+ // Fail closed, and BEFORE anything is appended: a machine that cannot
388
+ // protect an execution costs no authority, so the same token still spends
389
+ // once the mechanism works. Not one of `EXECUTE_REFUSAL_CODES` — adding
390
+ // `sandbox-unavailable` to that union widens frozen public API (§11.1
391
+ // invariant 6), which is a human's decision and is drafted for sign-off in
392
+ // `docs/proposals/aprv-193-amendments.md`. Until then this is what it is:
393
+ // the executor declining to run something it cannot put in the room it
394
+ // promised, with the exit code that already means "the command did not run".
395
+ streams.err(`approval: the egress sandbox is unavailable (${posture.reason}); the command was NOT run and nothing was appended. Fix the sandbox, or take the recorded opt-out with \`--no-sandbox\` (docs/sandboxed-exec.md).\n`);
396
+ return EXIT_COMMAND_NOT_RUN;
397
+ }
398
+ // execution.started is appended HERE, before the child exists. A crash from
399
+ // this line until the finish below leaves a dangling execution, which
400
+ // `approval status` reports and nothing repairs on its own.
401
+ const started = startExecution(logPath, actionKey, {
402
+ ...executeOptions(flags, cwd, stringFlag(flags, "--token")),
403
+ presentedPayloadHash: payloadHash,
404
+ envStripped: childEnv.stripped,
405
+ sandbox: posture.state,
406
+ }, actor);
407
+ if (!started.ok)
408
+ return emitRefusal(streams, json, started);
409
+ // APRV-193. The wrapper is built here and not inside `core/sandbox.ts`'s own
410
+ // spawn, because this verb's spawn is the one place the payload binding, the
411
+ // starved environment and the stdio contract already meet: a second spawn
412
+ // site would be a second place for them to drift.
413
+ //
414
+ // The command is resolved to an absolute path first. `sandbox-exec` execs
415
+ // through `execvp`, so the lookup would still happen — but a lookup that
416
+ // FAILS exits 71, and 71 recorded as the child's exit code is a lie about a
417
+ // command that never ran. A command that does not resolve is left unwrapped
418
+ // and fails as the ENOENT it is.
419
+ const wrapped = posture.kind === "apply"
420
+ ? wrapExecutable(posture.mechanism, command, childArgv.slice(1), childEnv.env, logPath)
421
+ : null;
422
+ const child = spawnSync(wrapped?.command ?? command, wrapped?.args ?? childArgv.slice(1), {
423
+ cwd,
424
+ stdio: childIo.stdio,
425
+ encoding: "utf8",
426
+ env: childEnv.env,
427
+ });
428
+ if (wrapped !== null)
429
+ rmSync(wrapped.cleanup, { recursive: true, force: true });
430
+ if (childIo.onOutput !== undefined) {
431
+ childIo.onOutput({ stdout: child.stdout ?? "", stderr: child.stderr ?? "" });
432
+ }
433
+ const exitCode = child.error === undefined
434
+ ? childExitCode(child.status, child.signal)
435
+ : EXIT_COMMAND_NOT_RUN;
436
+ if (child.error !== undefined) {
437
+ streams.err(`approval: the command could not be run (${child.error.message}); recording execution.failed with exit_code ${EXIT_COMMAND_NOT_RUN}\n`);
438
+ }
439
+ const finished = finishExecution(logPath, actionKey, exitCode, actor);
440
+ if (!finished.ok) {
441
+ const code = emitRefusal(streams, json, finished);
442
+ // The child's code is the more important fact when the child itself failed;
443
+ // when it succeeded, a failure to RECORD that success must not read as one.
444
+ return exitCode === 0 ? code : exitCode;
445
+ }
446
+ if (json) {
447
+ // stderr, not stdout: stdout belongs to the child. See the module header.
448
+ streams.err(`${JSON.stringify({
449
+ ok: true,
450
+ action_key: actionKey,
451
+ task: started.task,
452
+ class: started.class,
453
+ autonomy: started.autonomy,
454
+ started_seq: started.record.seq,
455
+ outcome: finished.event,
456
+ outcome_seq: finished.record.seq,
457
+ exit_code: exitCode,
458
+ payload_hash: payloadHash,
459
+ })}\n`);
460
+ }
461
+ return exitCode;
462
+ }
463
+ // ===========================================================================
464
+ // approval wait
465
+ // ===========================================================================
466
+ /** Synchronous sleep with no dependency and no busy-spin. */
467
+ function sleepSync(ms) {
468
+ if (ms <= 0)
469
+ return;
470
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
471
+ }
472
+ /**
473
+ * The role a decision state wears. Colour is redundant here: the word is
474
+ * printed beside the glyph, so a pipe (or a colour-blind reader) loses nothing.
475
+ */
476
+ function stateRole(state) {
477
+ if (state === "granted")
478
+ return "ok";
479
+ if (state === "expired" || state === "requested" || state === "withdrawn")
480
+ return "warn";
481
+ return "fail";
482
+ }
483
+ /**
484
+ * `approval wait`'s human answer: the verdict, then one aligned row per action.
485
+ *
486
+ * Action keys are copyable, so the left column opts out of `key` styling
487
+ * (`plainLeft`) and the state beside it carries the colour instead.
488
+ */
489
+ export function renderWaitHuman(task, status, actions, st = style()) {
490
+ const glyph = status === "granted" ? "ok" : status === "expired" || status === "withdrawn" ? "skip" : "fail";
491
+ const head = `${st.glyph(glyph)} ${st.key(task)} ${st.paint(stateRole(status), status)}`;
492
+ if (actions.length === 0)
493
+ return `${head}\n`;
494
+ const rows = actions.map((action) => ({
495
+ left: action.action_key,
496
+ right: st.paint(stateRole(action.state), action.state),
497
+ plainLeft: true,
498
+ }));
499
+ return `${head}\n${st.table(rows, { indent: 2 })}\n`;
500
+ }
501
+ /** Every action key of `task` that ever carried an `approval.requested`. */
502
+ function requestedKeysOf(records, task) {
503
+ const keys = [];
504
+ for (const record of records) {
505
+ if (record.event !== "approval.requested")
506
+ continue;
507
+ if (record.task !== task)
508
+ continue;
509
+ const key = record.action_key;
510
+ if (typeof key !== "string" || key.length === 0)
511
+ continue;
512
+ if (!keys.includes(key))
513
+ keys.push(key);
514
+ }
515
+ return keys;
516
+ }
517
+ export function commandWait(argv, streams, cwd) {
518
+ const outcome = front(argv, {
519
+ ...COMMON_FLAGS,
520
+ ...POLICY_FLAGS,
521
+ "--timeout": "string",
522
+ "--interval": "string",
523
+ "--withdraw-on-timeout": "boolean",
524
+ "--as": "string",
525
+ }, WAIT_HELP, streams, cwd);
526
+ if (outcome.kind === "handled")
527
+ return outcome.code;
528
+ const { flags, positionals, json, logPath } = outcome;
529
+ const task = positionals[0];
530
+ if (task === undefined)
531
+ return usageError(streams, json, "missing <task> argument", WAIT_HELP);
532
+ const extra = positionals[1];
533
+ if (extra !== undefined) {
534
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, WAIT_HELP);
535
+ }
536
+ const timeoutText = stringFlag(flags, "--timeout");
537
+ if (timeoutText === null) {
538
+ return usageError(streams, json, "missing --timeout <duration>", WAIT_HELP);
539
+ }
540
+ const timeoutMs = parseDuration(timeoutText);
541
+ if (timeoutMs === null) {
542
+ return usageError(streams, json, `--timeout expects a duration like 30s, 10m, 6h, got ${JSON.stringify(timeoutText)}`, WAIT_HELP);
543
+ }
544
+ const intervalText = stringFlag(flags, "--interval");
545
+ const intervalMs = intervalText === null ? DEFAULT_WAIT_INTERVAL_MS : parseDuration(intervalText);
546
+ if (intervalMs === null) {
547
+ return usageError(streams, json, `--interval expects a duration like 500ms, 2s, got ${JSON.stringify(intervalText)}`, WAIT_HELP);
548
+ }
549
+ // APRV-106. OFF by default, and it is the only thing that makes `wait` a
550
+ // writer: a caller that merely stopped waiting has not necessarily stopped
551
+ // wanting an answer (a supervisor may wait again), so the retraction is
552
+ // opt-in. When it is on, the actor is required up front — resolving identity
553
+ // after a nine-minute wait, and failing then, would leave exactly the stale
554
+ // request the flag exists to prevent.
555
+ const withdrawOnTimeout = boolFlag(flags, "--withdraw-on-timeout");
556
+ const asFlag = stringFlag(flags, "--as");
557
+ const withdrawActor = asFlag ?? resolveHumanActor();
558
+ if (withdrawOnTimeout && (withdrawActor === null || !PRINCIPAL_ACTOR.test(withdrawActor))) {
559
+ return usageError(streams, json, `--withdraw-on-timeout needs an identity: pass --as human:<id> | agent:<id>, or set ${HUMAN_ACTOR_ENV}. Only the actor that opened a request may withdraw it, so there is no default.`, WAIT_HELP);
560
+ }
561
+ const ttlMs = ttlOf(flags, cwd);
562
+ const deadline = Date.now() + timeoutMs;
563
+ for (;;) {
564
+ const read = readVerifiedRecords(logPath);
565
+ if (!read.ok) {
566
+ return emitRefusal(streams, json, {
567
+ ok: false,
568
+ code: read.code === "log-torn-tail" ? "log-torn-tail" : "log-unreadable",
569
+ message: read.message,
570
+ });
571
+ }
572
+ const ts = now();
573
+ const actions = [];
574
+ let pending = false;
575
+ for (const key of requestedKeysOf(read.records, task)) {
576
+ const derivation = requestState(read.records, key, ts, ttlMs);
577
+ // APRV-105. The token, when this machine can open it: the grant sealed it
578
+ // to the ephemeral public key this action's request published, and the
579
+ // private half is in the key store beside the log. Attached only to a
580
+ // GRANTED action, and only in `--json` below — a `null` on every other
581
+ // state would be a field consumers have to ignore, and a token on a
582
+ // rejected action would be a value with nothing behind it.
583
+ const token = json && derivation.state === "granted"
584
+ ? deliveredToken(read.records, key, keyStoreDirFor(logPath))
585
+ : null;
586
+ actions.push({
587
+ action_key: key,
588
+ state: derivation.state,
589
+ seq: derivation.decisionSeq ?? derivation.requestSeq,
590
+ ...(token === null ? {} : { token }),
591
+ });
592
+ if (derivation.state === "requested")
593
+ pending = true;
594
+ }
595
+ if (!pending) {
596
+ // Precedence, documented in --help: a human's "no" outranks a lapse, and
597
+ // both outrank "everything was granted". A task with no requests at all
598
+ // has nothing to wait for and is granted vacuously.
599
+ //
600
+ // APRV-106 puts `withdrawn` between the two, and REUSES exit 1 rather
601
+ // than adding a code. The table in `cli/exit-codes.ts` is frozen public
602
+ // API — adding a number is a spec change, and agents already branch on
603
+ // these seven — while the fact an agent needs is the one exit 1 already
604
+ // carries: this action is NOT authorized and no retry of the same request
605
+ // will change that. The distinction lives where a distinction can be
606
+ // added without breaking anyone, in `status`, which is `"withdrawn"` in
607
+ // the JSON and printed beside the action in the human render.
608
+ const rejected = actions.some((action) => action.state === "rejected" || action.state === "revoked");
609
+ const withdrawn = actions.some((action) => action.state === "withdrawn");
610
+ const expired = actions.some((action) => action.state === "expired");
611
+ const status = rejected
612
+ ? "rejected"
613
+ : withdrawn
614
+ ? "withdrawn"
615
+ : expired
616
+ ? "expired"
617
+ : "granted";
618
+ const code = rejected || withdrawn ? EXIT_INTEGRITY : expired ? EXIT_TORN_TAIL : EXIT_OK;
619
+ if (json)
620
+ emitJson(streams, { ok: true, task, status, actions });
621
+ else
622
+ streams.out(renderWaitHuman(task, status, actions, style({ json })));
623
+ return code;
624
+ }
625
+ if (Date.now() >= deadline) {
626
+ // APRV-106. Best effort, and never fatal: the exit code is still 6, which
627
+ // is what the caller branches on. A withdrawal that itself fails leaves
628
+ // the request live — the pre-APRV-106 behaviour — and says so on stderr
629
+ // rather than converting a timeout into a different outcome.
630
+ const withdrawn = [];
631
+ if (withdrawOnTimeout && withdrawActor !== null) {
632
+ for (const action of actions) {
633
+ if (action.state !== "requested")
634
+ continue;
635
+ const result = withdraw(logPath, action.action_key, withdrawActor, {
636
+ policy: policyLocation(flags, cwd),
637
+ reason: "timeout",
638
+ note: `the waiting process stopped waiting after ${timeoutText}; a decision on this request can no longer be consumed`,
639
+ });
640
+ if (result.ok)
641
+ withdrawn.push(action.action_key);
642
+ else {
643
+ streams.err(`approval: could not withdraw ${action.action_key} on timeout (${result.code}): ${result.message}\n`);
644
+ }
645
+ }
646
+ }
647
+ if (json) {
648
+ // `withdrawn` appears only when the flag asked for it. The default
649
+ // timeout object is the shape callers already parse, and adding an
650
+ // always-empty array to it would be a breaking change bought for
651
+ // nothing.
652
+ streams.err(`${JSON.stringify({
653
+ ok: false,
654
+ task,
655
+ status: "timeout",
656
+ actions,
657
+ ...(withdrawOnTimeout ? { withdrawn } : {}),
658
+ })}\n`);
659
+ }
660
+ else {
661
+ streams.err(withdrawn.length === 0
662
+ ? `approval: timeout: ${task} still has undecided request(s) after ${timeoutText}; nothing was appended and the request(s) remain live\n`
663
+ : `approval: timeout: ${task} was undecided after ${timeoutText}; withdrew ${withdrawn.length} request(s) so no one is asked a question this process can no longer answer to: ${withdrawn.join(", ")}\n`);
664
+ }
665
+ return EXIT_TIMEOUT;
666
+ }
667
+ sleepSync(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
668
+ }
669
+ }
670
+ /** The live inbox: requests inside their TTL, awaiting a human decision. */
671
+ function pendingRequests(records, ts, ttlMs) {
672
+ const keys = [];
673
+ for (const record of records) {
674
+ if (record.event !== "approval.requested")
675
+ continue;
676
+ const key = record.action_key;
677
+ if (typeof key !== "string" || key.length === 0)
678
+ continue;
679
+ if (!keys.includes(key))
680
+ keys.push(key);
681
+ }
682
+ const entries = [];
683
+ for (const key of keys) {
684
+ const derivation = requestState(records, key, ts, ttlMs);
685
+ if (derivation.state !== "requested")
686
+ continue;
687
+ const requestedAt = Date.parse(derivation.requestTs ?? "");
688
+ const asked = Date.parse(ts);
689
+ const remaining = ttlMs === null || Number.isNaN(requestedAt) || Number.isNaN(asked)
690
+ ? null
691
+ : requestedAt + ttlMs - asked;
692
+ entries.push({
693
+ action_key: key,
694
+ task: derivation.task,
695
+ class: derivation.declared.class,
696
+ est_cost_usd: derivation.declared.est_cost_usd,
697
+ requested_ts: derivation.requestTs,
698
+ seq: derivation.requestSeq,
699
+ ttl_remaining_ms: remaining,
700
+ });
701
+ }
702
+ return entries.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
703
+ }
704
+ /** `23h 58m left`, `45s left`, `lapsed` — the TTL column's text. */
705
+ function ttlText(remainingMs) {
706
+ if (remainingMs === null)
707
+ return "no TTL";
708
+ const seconds = Math.max(0, Math.round(remainingMs / 1000));
709
+ if (seconds === 0)
710
+ return "lapsed";
711
+ if (seconds < 60)
712
+ return `${seconds}s left`;
713
+ const minutes = Math.floor(seconds / 60);
714
+ if (minutes < 60)
715
+ return `${minutes}m left`;
716
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m left`;
717
+ }
718
+ /**
719
+ * The TTL column's role, by the fraction of the window still standing:
720
+ * comfortable above half, worth noticing above a tenth, urgent below it.
721
+ */
722
+ function ttlRole(remainingMs, ttlMs) {
723
+ if (remainingMs === null || ttlMs === null || ttlMs <= 0)
724
+ return "muted";
725
+ const fraction = remainingMs / ttlMs;
726
+ if (fraction > 0.5)
727
+ return "ok";
728
+ if (fraction > 0.1)
729
+ return "warn";
730
+ return "fail";
731
+ }
732
+ /**
733
+ * The live inbox as an aligned table (APRV-91 #9).
734
+ *
735
+ * The old shape was tab-separated, which lines up only when every field happens
736
+ * to be the same width, and a queue's fields never are. The alignment is
737
+ * `style.table`'s (APRV-102 replaced a hand-rolled copy of it here), so widths
738
+ * are measured on the UNDRESSED cells and the coloured render is the plain one
739
+ * with escapes inserted.
740
+ *
741
+ * The TTL is the only dressed cell. The request timestamp was `muted` until
742
+ * APRV-102 and is not any more: a timestamp is a value an operator copies into
743
+ * a `grep` or a bug report, and rule 3 of `style.ts` does not have a "but this
744
+ * one is only dim" exception.
745
+ */
746
+ export function renderQueueHuman(pending, ttlMs, st = style()) {
747
+ const rows = pending.map((entry) => [
748
+ entry.action_key,
749
+ entry.task ?? "-",
750
+ entry.class ?? "-",
751
+ `$${String(entry.est_cost_usd ?? 0)}`,
752
+ entry.requested_ts ?? "-",
753
+ { text: ttlText(entry.ttl_remaining_ms), role: ttlRole(entry.ttl_remaining_ms, ttlMs) },
754
+ ]);
755
+ return `${table(st, rows, {
756
+ header: ["action", "task", "class", "cost", "requested", "ttl"],
757
+ })}\n`;
758
+ }
759
+ export function commandQueue(argv, streams, cwd) {
760
+ const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS }, QUEUE_HELP, streams, cwd);
761
+ if (outcome.kind === "handled")
762
+ return outcome.code;
763
+ const { flags, positionals, json, logPath } = outcome;
764
+ const extra = positionals[0];
765
+ if (extra !== undefined) {
766
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, QUEUE_HELP);
767
+ }
768
+ const check = preflightLog(logPath);
769
+ if (!check.ok)
770
+ return ioError(streams, json, check.message);
771
+ const read = readVerifiedRecords(logPath);
772
+ if (!read.ok) {
773
+ return emitRefusal(streams, json, {
774
+ ok: false,
775
+ // The read refusal's code is already one of this command's codes
776
+ // (`log-unreadable`, `log-torn-tail`, `log-corrupt`); it is surfaced
777
+ // unchanged so a corrupt log is reported as corruption, not as I/O.
778
+ code: read.code,
779
+ message: read.message,
780
+ });
781
+ }
782
+ // ONE read of the policy: `ttlOf` loads and parses it, and calling it twice
783
+ // (as this did before APRV-102) risks the two halves of one render disagreeing
784
+ // about the TTL if the file changes underneath, on top of the wasted work.
785
+ const ttlMs = ttlOf(flags, cwd);
786
+ const pending = pendingRequests(read.records, now(), ttlMs);
787
+ if (json) {
788
+ emitJson(streams, { ok: true, pending });
789
+ }
790
+ else if (pending.length === 0) {
791
+ streams.out("queue: empty — no requests awaiting a decision\n");
792
+ }
793
+ else {
794
+ streams.out(renderQueueHuman(pending, ttlMs, style({ json })));
795
+ }
796
+ // An empty inbox is a healthy inbox: queue never exits non-zero for having
797
+ // nothing (or something) in it. Only the filesystem and a torn log can.
798
+ return EXIT_OK;
799
+ }
800
+ // ===========================================================================
801
+ // approval status
802
+ // ===========================================================================
803
+ /** The policy file whose bytes attestation is judged against. */
804
+ function policyPathFor(flags, cwd) {
805
+ const location = policyLocation(flags, cwd);
806
+ if (location.file !== undefined)
807
+ return location.file;
808
+ const dir = location.dir ?? cwd;
809
+ return resolvePathSegments(dir, POLICY_FILENAMES[0] ?? "APPROVAL.md");
810
+ }
811
+ function budgetHeadroom(records, flags, cwd, ts) {
812
+ const location = policyLocation(flags, cwd);
813
+ const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
814
+ // A ZERO-COST PROBE: the hypothetical next action, declaring $0 and no class.
815
+ // Only the global budgets are evaluated (class limits need a matched rule and
816
+ // therefore a specific action, which status does not have). `remaining` is
817
+ // consequently headroom AFTER that probe — which for daily_actions means one
818
+ // action is already subtracted, because every authorization counts as one.
819
+ // Stated here and in --help rather than quietly adjusted: the number a reader
820
+ // sees is the number the evaluator would produce for the next action.
821
+ return evaluateBudgets(records, {
822
+ classLimits: null,
823
+ classPattern: null,
824
+ globalBudgets: load.ok ? load.policy.budgets ?? null : null,
825
+ }, { class: "", est_cost_usd: "0" }, ts).verdicts;
826
+ }
827
+ /**
828
+ * The one warning `status` exists to keep in front of an operator: the payload
829
+ * store is the only thing under `.approval/` that a rebuild cannot recreate.
830
+ *
831
+ * QUEUE.md regenerates and `index.sqlite` reindexes, both from the log. The
832
+ * store does not: the log records the *hash* a request bound to, never the
833
+ * bytes, so bytes deleted from `.approval/payloads/` are gone. What survives is
834
+ * the binding, which is why the loss is visible rather than silent: every
835
+ * manual request whose material went with it renders `payload-unavailable`
836
+ * (`channels/tagging.ts`) instead of showing an approver something no hash ever
837
+ * bound.
838
+ */
839
+ const PAYLOAD_STORE_NOTE = "the payload store holds the bytes approvals bind to, keyed by their hash; " +
840
+ "it is the one cache that cannot be rebuilt from the log, and losing it leaves " +
841
+ "manual requests rendering as payload-unavailable rather than showing bytes no hash bound";
842
+ /**
843
+ * How many payloads are stored, whether the store exists, and what the log says
844
+ * about the ones that are gone (APRV-41).
845
+ *
846
+ * `pruned` counts distinct hashes named by a `payload.pruned` event: retention
847
+ * removes bytes and leaves that record behind on purpose, so a reader can tell
848
+ * "this store never held it" from "this store held it and the daemon let it go".
849
+ * `orphans` counts files no record binds — head-moved residue, which the daemon
850
+ * removes once `payload_retention` is set and which nothing removes while it is
851
+ * absent. Both are facts, never health inputs.
852
+ */
853
+ function payloadStoreSummary(logPath, records) {
854
+ let files = 0;
855
+ let present = true;
856
+ try {
857
+ for (const entry of readdirSync(payloadStoreDirFor(logPath), { withFileTypes: true })) {
858
+ // `<hash>.json` and nothing else. Temp files from an interrupted atomic
859
+ // write start with a dot and are not payloads anybody can read.
860
+ if (entry.isFile() && entry.name.endsWith(".json") && !entry.name.startsWith(".")) {
861
+ files += 1;
862
+ }
863
+ }
864
+ }
865
+ catch {
866
+ // Unreadable and absent are reported the same way on purpose: `status` is
867
+ // not the environment diagnostic. `approval doctor` distinguishes them, and
868
+ // an unwritable store is a failure there.
869
+ present = false;
870
+ files = 0;
871
+ }
872
+ const census = payloadStoreCensus(records, payloadStoreDirFor(logPath));
873
+ return {
874
+ present,
875
+ files,
876
+ pruned: census.pruned,
877
+ orphans: census.orphans,
878
+ note: PAYLOAD_STORE_NOTE,
879
+ };
880
+ }
881
+ /**
882
+ * The informational git-coverage line of `approval status` (APRV-245).
883
+ *
884
+ * The range is the CURRENT BRANCH's own commits: `defaultRange` takes the merge
885
+ * base with `origin/main`, so what this counts is what this branch added and not
886
+ * the whole history. Two states are reported instead of a count, because in
887
+ * neither of them would a count mean anything: a directory that is not a git
888
+ * checkout, and a checkout where `origin/main` does not resolve. The second is
889
+ * NOT quietly swapped for the last twenty commits here — `approval coverage`
890
+ * announces that fallback in its own output where there is room to say so, and
891
+ * a one-line summary that silently changed what it measured would be worse than
892
+ * one that says it cannot measure.
893
+ *
894
+ * Informational, exactly as `harness outcomes` beside it is (APRV-145): it is a
895
+ * coverage measurement rather than an integrity verdict, so it is deliberately
896
+ * outside `healthy` and outside the exit code. A gap here is a question for a
897
+ * person ("was that commit ever declared?"), and questions with legitimate
898
+ * answers must not turn a `status` run red.
899
+ */
900
+ function gitCoverageSummary(records, flags, cwd) {
901
+ const empty = (reason) => ({
902
+ available: false,
903
+ reason,
904
+ observed: 0,
905
+ covered: 0,
906
+ });
907
+ const root = repoRoot(cwd);
908
+ if (root === null)
909
+ return empty("not a git checkout");
910
+ const range = defaultRange(root, DEFAULT_TRUNK_REF);
911
+ if (range.note !== undefined)
912
+ return empty(`${DEFAULT_TRUNK_REF} absent`);
913
+ const location = policyLocation(flags, cwd);
914
+ const load = loadPolicy(location.file === undefined ? { dir: location.dir ?? cwd } : { file: location.file });
915
+ const seen = observeGit(root, {
916
+ base: range.base,
917
+ head: range.head,
918
+ policyProtectedPaths: load.ok ? (load.policy.protected_paths ?? []) : [],
919
+ });
920
+ if (!seen.available)
921
+ return empty(seen.reason ?? "git could not be asked");
922
+ const report = coverageReport(seen.effects, records);
923
+ return {
924
+ available: true,
925
+ reason: seen.reason ?? null,
926
+ observed: report.observed,
927
+ covered: report.covered,
928
+ };
929
+ }
930
+ export function commandStatus(argv, streams, cwd) {
931
+ const outcome = front(argv, { ...COMMON_FLAGS, ...POLICY_FLAGS, "--verbose": "boolean" }, STATUS_HELP, streams, cwd);
932
+ if (outcome.kind === "handled")
933
+ return outcome.code;
934
+ const { flags, positionals, json, logPath } = outcome;
935
+ const extra = positionals[0];
936
+ if (extra !== undefined) {
937
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, STATUS_HELP);
938
+ }
939
+ const check = preflightLog(logPath);
940
+ if (!check.ok)
941
+ return ioError(streams, json, check.message);
942
+ // The policy is read here for one number, the skew tolerance of amended
943
+ // SPEC.md §8 (APRV-58), and it reaches only which anomalies are reported. The
944
+ // verdict, the health line and the exit code below are unmoved by it.
945
+ const verification = verify(logPath, { policy: policyLocation(flags, cwd) });
946
+ const read = readVerifiedRecords(logPath);
947
+ // A log that cannot be read at all is an I/O fact, not a health report.
948
+ if (!read.ok && read.code === "log-unreadable") {
949
+ return ioError(streams, json, read.message);
950
+ }
951
+ // A torn or corrupt log still produces a health report — that *is* the report,
952
+ // and `verification` below names the damage. Projections over an unverifiable
953
+ // log are simply empty: `status` describes the log, it never authorizes
954
+ // anything from one.
955
+ const records = read.ok ? read.records : [];
956
+ const attestation = checkAttestation(records, policyPathFor(flags, cwd));
957
+ // APRV-120. `dangling` is now the `open` custody state only: a harness
958
+ // execution is terminal by design and never gains an outcome, so listing it
959
+ // as debris trained operators to ignore this list (the reference repository's
960
+ // own log carried dozens). Indeterminate outcomes are reported beside it
961
+ // rather than inside it, because the two ask a person for different things:
962
+ // look at what our runtime did, against establish whether the far side
963
+ // committed.
964
+ const dangling = danglingExecutions(records).map((entry) => ({
965
+ action_key: entry.actionKey,
966
+ task: entry.task,
967
+ ts: entry.ts,
968
+ seq: entry.seq,
969
+ }));
970
+ const indeterminate = indeterminateExecutions(records).map((entry) => ({
971
+ action_key: entry.actionKey,
972
+ task: entry.task,
973
+ ts: entry.ts,
974
+ seq: entry.indeterminateSeq,
975
+ reason: entry.reason,
976
+ }));
977
+ // APRV-145. Three streaks now, one shape. The per-task streak of SPEC.md
978
+ // §10.2 keeps its `task` field and its meaning; the two harness scopes of the
979
+ // amended §10.2 report their scope KEY in that same field, so it stays a
980
+ // non-empty string an operator can grep the log for and every pre-existing
981
+ // consumer reads the three fields it always read. `scope` is the additive
982
+ // field that says which derivation produced the key.
983
+ //
984
+ // APRV-280 adds `clears`, additively: an escalated scope is a repository state
985
+ // an operator has to get OUT of, and a row that names the scope without naming
986
+ // the exit is a row that sends them to the source. The sentence is
987
+ // `core/loop.ts`'s own, so `status`, the gate's refusals and the hook's denies
988
+ // cannot come to disagree about what recovery is.
989
+ const escalations = [
990
+ ...loopEscalation(records)
991
+ .filter((state) => state.escalated)
992
+ .map((state) => ({
993
+ task: state.task,
994
+ scope: "task",
995
+ consecutive_failures: state.consecutiveFailures,
996
+ escalated: true,
997
+ clears: loopClearance("task", state.task),
998
+ })),
999
+ ...harnessLoopEscalation(records)
1000
+ .filter((state) => state.escalated)
1001
+ .map((state) => ({
1002
+ task: state.key,
1003
+ scope: state.scope,
1004
+ consecutive_failures: state.consecutiveFailures,
1005
+ escalated: true,
1006
+ clears: loopClearance(state.scope, state.key),
1007
+ })),
1008
+ ];
1009
+ // Informational, and deliberately outside `healthy` and the exit code, for the
1010
+ // reason the timestamp anomalies below are: this is a coverage measurement,
1011
+ // not an integrity verdict. A persistently high `unreported` is how an
1012
+ // operator learns the post-execution hook is not installed or not firing.
1013
+ const harnessOutcomes = harnessOutcomeCoverage(records);
1014
+ // APRV-245, and informational for the reason directly above: the same rule,
1015
+ // applied to a witness this project does not write. `harness outcomes` counts
1016
+ // the tool calls the runtime was told about; this counts the commits git saw
1017
+ // whether or not anybody told the runtime anything.
1018
+ const coverage = gitCoverageSummary(records, flags, cwd);
1019
+ // APRV-127. The reconciliation backlog: obligations opened by a retrospective
1020
+ // DENIAL and not yet discharged by a person. It counts toward `healthy` for
1021
+ // the same reason a dangling execution does — an unreconciled denial is a "no"
1022
+ // that has so far changed nothing, and a "no" nobody can see is the failure
1023
+ // the whole retrospective path exists to prevent. Quiet here would mean a
1024
+ // human said an action should not have happened and the system moved on.
1025
+ const obligations = openObligations(records).map((item) => ({
1026
+ seq: item.seq,
1027
+ ts: item.ts,
1028
+ action_key: item.actionKey,
1029
+ task: item.task,
1030
+ class: item.class,
1031
+ obligation: item.obligation,
1032
+ review_seq: item.reviewSeq,
1033
+ }));
1034
+ // APRV-214, amended SPEC.md §5.2. An open window is a suspension of the
1035
+ // policy for every gated tool call under this root, so it belongs in the one
1036
+ // report an operator reads to answer "is this repository in a normal state".
1037
+ // It counts toward `healthy` DELIBERATELY: a CI check or a `doctor` run keyed
1038
+ // on `healthy` should go red while a bypass stands, and a window nobody
1039
+ // noticed was left open is the failure mode of the whole feature.
1040
+ const gateWindow = openGateWindow(records);
1041
+ const budgets = budgetHeadroom(records, flags, cwd, now());
1042
+ // Informational: the store's state never moves `healthy` or the exit code.
1043
+ // A repo that has never made a `--payload` request has no store, and an
1044
+ // operator is being told what it is, not that anything is wrong.
1045
+ const payloadStore = payloadStoreSummary(logPath, records);
1046
+ const verificationSummary = {
1047
+ status: verification.status,
1048
+ records: verification.status === "corrupt" ? null : verification.records,
1049
+ };
1050
+ // APRV-40. Timestamp anomalies (SPEC.md §8) are informational and deliberately
1051
+ // outside `healthy`: they are a judgment, not an integrity verdict. `verify`
1052
+ // already declined to refuse on them, and `status` does not get to overrule it
1053
+ // by flipping a health bit an operator reads as "something is broken". The
1054
+ // field is present only when there is something to report, so every existing
1055
+ // `--json` consumer sees a byte-identical object on a log with no anomaly.
1056
+ //
1057
+ // The sampler's own state is NOT reported here: `status --json` is a frozen
1058
+ // shape, and the configuration fact belongs to `approval audit list`, which
1059
+ // reports it beside the backlog it explains.
1060
+ const anomalies = verification.anomalies.map((anomaly) => ({
1061
+ kind: anomaly.kind,
1062
+ seq: anomaly.seq,
1063
+ previous_seq: anomaly.previousSeq,
1064
+ skew_ms: anomaly.skewMs,
1065
+ message: anomaly.message,
1066
+ }));
1067
+ const healthy = attestation.status === "attested" &&
1068
+ verification.status === "clean" &&
1069
+ dangling.length === 0 &&
1070
+ // An unreconciled indeterminate outcome is a side effect nobody has
1071
+ // established the fate of, and a repo carrying one is not healthy; an open
1072
+ // reconciliation obligation is a denial nobody has answered for yet.
1073
+ indeterminate.length === 0 &&
1074
+ escalations.length === 0 &&
1075
+ obligations.length === 0 &&
1076
+ gateWindow === null;
1077
+ if (json) {
1078
+ emitJson(streams, {
1079
+ ok: true,
1080
+ healthy,
1081
+ attestation: {
1082
+ state: attestation.status,
1083
+ seq: attestation.status === "attested" || attestation.status === "hash-mismatch"
1084
+ ? attestation.seq
1085
+ : null,
1086
+ },
1087
+ verification: verificationSummary,
1088
+ dangling,
1089
+ // Present only when there is something to report, exactly as `anomalies`
1090
+ // is: every existing consumer sees a byte-identical object on a log that
1091
+ // carries no indeterminate outcome.
1092
+ ...(indeterminate.length === 0 ? {} : { indeterminate }),
1093
+ budgets,
1094
+ loop_escalations: escalations,
1095
+ harness_outcomes: harnessOutcomes,
1096
+ // APRV-245. Always present, so every consumer sees the same four keys
1097
+ // whether or not this directory is a checkout, and outside `healthy` and
1098
+ // the exit code for the APRV-145 reason stated where it is computed.
1099
+ coverage: {
1100
+ available: coverage.available,
1101
+ reason: coverage.reason,
1102
+ observed: coverage.observed,
1103
+ covered: coverage.covered,
1104
+ },
1105
+ reconciliation: obligations,
1106
+ payload_store: payloadStore,
1107
+ ...(anomalies.length === 0 ? {} : { anomalies }),
1108
+ // Present only while a window stands, exactly as `anomalies` and
1109
+ // `indeterminate` are: a repository with no window emits the object it
1110
+ // has always emitted, byte for byte.
1111
+ ...(gateWindow === null
1112
+ ? {}
1113
+ : {
1114
+ gate_window: {
1115
+ seq: gateWindow.seq,
1116
+ opened_at: gateWindow.openedAt,
1117
+ opened_by: gateWindow.openedBy,
1118
+ reason: gateWindow.reason,
1119
+ expires_at: gateWindow.expiresAt,
1120
+ bypassed: gateWindow.bypassCount,
1121
+ },
1122
+ }),
1123
+ });
1124
+ }
1125
+ else {
1126
+ const st = style({ json });
1127
+ const verbose = boolFlag(flags, "--verbose");
1128
+ const rows = [
1129
+ {
1130
+ left: "health",
1131
+ right: healthy ? st.ok("ok") : st.warn("attention"),
1132
+ },
1133
+ {
1134
+ left: "attestation",
1135
+ // The seq is a value: an operator pastes it into `approval log tail` or
1136
+ // a bug report, so APRV-102 took the `muted` dressing off it. The state
1137
+ // beside it still carries the colour.
1138
+ right: `${st.paint(attestation.status === "attested" ? "ok" : "warn", attestation.status)}${attestation.status === "attested" || attestation.status === "hash-mismatch"
1139
+ ? ` (seq ${attestation.seq})`
1140
+ : ""}`,
1141
+ },
1142
+ {
1143
+ left: "verification",
1144
+ right: `${st.paint(verificationSummary.status === "clean" ? "ok" : "fail", verificationSummary.status)}${verificationSummary.records === null
1145
+ ? ""
1146
+ : ` ${st.muted(`(${verificationSummary.records} record(s))`)}`}`,
1147
+ },
1148
+ {
1149
+ left: "timestamp anomalies",
1150
+ right: anomalies.length === 0
1151
+ ? st.muted("none")
1152
+ : st.warn(`${anomalies.length} (reported, NOT refused — the chain verifies and health is unaffected)`),
1153
+ ...(anomalies.length === 0
1154
+ ? {}
1155
+ : {
1156
+ under: anomalies.map((anomaly) => `${anomaly.kind} seq ${anomaly.seq} ${anomaly.skew_ms}ms before seq ${anomaly.previous_seq}`),
1157
+ }),
1158
+ },
1159
+ {
1160
+ left: "dangling executions",
1161
+ right: dangling.length === 0 ? st.muted("none") : st.fail(String(dangling.length)),
1162
+ ...(dangling.length === 0
1163
+ ? {}
1164
+ : {
1165
+ under: dangling.map((entry) => `${entry.action_key} started ${entry.ts} seq ${entry.seq}`),
1166
+ }),
1167
+ },
1168
+ {
1169
+ // Its own row, never folded into the one above: the repair is a
1170
+ // different verb answering a different question (APRV-120).
1171
+ left: "indeterminate executions",
1172
+ right: indeterminate.length === 0 ? st.muted("none") : st.fail(String(indeterminate.length)),
1173
+ ...(indeterminate.length === 0
1174
+ ? {}
1175
+ : {
1176
+ under: indeterminate.map((entry) => `${entry.action_key} ${entry.reason ?? "unknown reason"} seq ${String(entry.seq)} — outcome unknown; \`approval execution reconcile\``),
1177
+ }),
1178
+ },
1179
+ ...(budgets.length === 0
1180
+ ? [{ left: "budgets", right: st.muted("none configured") }]
1181
+ : budgets.map((verdict) => ({
1182
+ left: `budget ${verdict.limit}`,
1183
+ right: `consumed ${verdict.consumed}, remaining ${verdict.remaining}`,
1184
+ }))),
1185
+ {
1186
+ // APRV-91: the two-line sentence that used to live here is the store's
1187
+ // rationale, not its state. The state is three numbers, and the
1188
+ // rationale is one `--json` field (`payload_store.note`) and a
1189
+ // paragraph in `docs/` away.
1190
+ left: "payload store",
1191
+ right: `${payloadStore.present ? `${payloadStore.files} file(s)` : "not created yet"}, ${payloadStore.pruned} pruned, ${payloadStore.orphans} unbound`,
1192
+ // …and `--verbose` puts it back (APRV-102). The sentence is the one
1193
+ // thing here a first-time reader cannot reconstruct from the numbers,
1194
+ // so it is one flag away rather than gone.
1195
+ ...(verbose ? { under: [st.muted(payloadStore.note)] } : {}),
1196
+ },
1197
+ {
1198
+ left: "loop escalations",
1199
+ right: escalations.length === 0 ? st.muted("none") : st.fail(String(escalations.length)),
1200
+ ...(escalations.length === 0
1201
+ ? {}
1202
+ : {
1203
+ // APRV-280: the scope, then the way out. The clearing sentence is
1204
+ // long and it earns its line — an operator reading this row is
1205
+ // looking at a repository where a floor is routing everything to a
1206
+ // phone, and the next thing they need is what ends that.
1207
+ under: escalations.flatMap((entry) => [
1208
+ entry.scope === "task"
1209
+ ? `${entry.task} (${entry.consecutive_failures} consecutive failed side-effecting executions, task) — escalated to manual`
1210
+ : `${entry.task} (${entry.consecutive_failures} consecutive failed side-effecting tool calls, ${entry.scope}) — escalated to manual`,
1211
+ st.muted(` clears: ${entry.clears}`),
1212
+ ]),
1213
+ }),
1214
+ },
1215
+ {
1216
+ // APRV-145. Its own INFORMATIONAL row, because the append-nothing rule
1217
+ // of the counterpart is otherwise invisible: a harness start with no
1218
+ // outcome is not debris, it is a tool call nobody reported on, and the
1219
+ // number is how an operator sees that the post-execution hook is dark.
1220
+ left: "harness outcomes",
1221
+ right: st.muted(`${harnessOutcomes.started} started, ${harnessOutcomes.reported} reported, ${harnessOutcomes.unreported} unreported`),
1222
+ },
1223
+ {
1224
+ // APRV-245, and INFORMATIONAL for the APRV-145 reason the row above is:
1225
+ // a coverage measurement is not an integrity verdict, so it moves
1226
+ // neither `healthy` nor the exit code. What it counts is this branch's
1227
+ // own commits, as git recorded them, against the verified log. The full
1228
+ // report, `gh` and the adapters included, is `approval coverage`.
1229
+ left: "git coverage",
1230
+ right: st.muted(coverage.available
1231
+ ? `${String(coverage.covered)} of ${String(coverage.observed)} effects carry evidence`
1232
+ : (coverage.reason ?? "unavailable")),
1233
+ },
1234
+ {
1235
+ // APRV-214. Its own row and not a footnote: while this says OPEN, the
1236
+ // policy is deciding nothing for the harness, and the person reading
1237
+ // this report is the person who can end that.
1238
+ left: "gate window",
1239
+ right: gateWindow === null
1240
+ ? st.muted("closed")
1241
+ : st.warn(`OPEN until ${gateWindow.expiresAt}, opened by ${gateWindow.openedBy}, ${String(gateWindow.bypassCount)} call(s) bypassed`),
1242
+ ...(gateWindow === null
1243
+ ? {}
1244
+ : {
1245
+ under: [
1246
+ `seq ${String(gateWindow.seq)} reason: ${gateWindow.reason}`,
1247
+ "every gated tool call under this root is allowed without approval — `approval gate close`",
1248
+ ],
1249
+ }),
1250
+ },
1251
+ {
1252
+ left: "reconciliation",
1253
+ right: obligations.length === 0
1254
+ ? st.muted("none open")
1255
+ : st.fail(`${obligations.length} UNRECONCILED DENIAL(S)`),
1256
+ ...(obligations.length === 0
1257
+ ? {}
1258
+ : {
1259
+ under: obligations.map((item) => `seq ${item.seq} ${item.action_key} ${item.class} ${item.obligation} — close with \`approval audit reconcile ${item.seq}\``),
1260
+ }),
1261
+ },
1262
+ { left: "log", right: relPath(logPath, cwd) },
1263
+ ];
1264
+ streams.out(`${st.table(rows)}\n`);
1265
+ }
1266
+ return healthy ? EXIT_OK : EXIT_INTEGRITY;
1267
+ }
1268
+ /** The manual command for a key nothing can prove, spelled once. */
1269
+ function manualResolveCommand(actionKey) {
1270
+ return `approval execution resolve ${actionKey} --outcome completed|failed --note "<what you observed>"`;
1271
+ }
1272
+ /**
1273
+ * The note a bulk resolution writes, which is the evidence and not a summary.
1274
+ *
1275
+ * It names the ref, the seq that ref carries, and the fact that the operator
1276
+ * confirmed it after being shown exactly that. A human-attested record whose
1277
+ * note said only "closed in bulk" would be the unexplained attestation the
1278
+ * single form refuses, arriving five at a time.
1279
+ */
1280
+ function sweptNote(entry) {
1281
+ return `${entry.actionKey} named seq ${String(entry.toSeq)}, which ${String(entry.provenBy)} carries in this checkout, so the action it was authorized for completed. Confirmed against that evidence and closed with \`${RESOLVE_DANGLING_COMMAND}\`.`;
1282
+ }
1283
+ /**
1284
+ * `approval execution resolve --dangling [--class <class>] [--yes] [--json]`
1285
+ *
1286
+ * The bulk form, and the manual step it removes (APRV-264). On 2026-09-05
1287
+ * `approval status` listed five dangling daemon advance executions left by the
1288
+ * 2026-09-02 loop; the daemon refused one advance per tick naming one key each,
1289
+ * and Carter closed all five by hand with five near-identical commands in a
1290
+ * second terminal window. The cadence exists to remove taps, and this was five
1291
+ * of them for one fact.
1292
+ *
1293
+ * What it does NOT do is decide anything the single form would not. Every rule
1294
+ * of `resolve` is intact: human-only, one `execution.completed` per key through
1295
+ * {@link resolveExecution}'s own compare-and-append, `exit_code: null`,
1296
+ * `attested_by_human: true`, and a mandatory non-empty note — generated here
1297
+ * rather than typed, because what it has to say is the evidence the runtime
1298
+ * showed the operator and the operator agreed with, which is a sentence a
1299
+ * person retyping it five times would only ever get less exact.
1300
+ *
1301
+ * The evidence is the trunk. A key is provable when it is one of the daemon's
1302
+ * own advance keys and a ref in this checkout carries the seq that key names
1303
+ * (`core/advance-cycle.ts`'s rule, read through the same `publishedState` the
1304
+ * daemon and the doctor row read). Everything else is UNPROVABLE and is left
1305
+ * exactly alone, listed with the one-line manual command: an outcome nobody can
1306
+ * demonstrate is a person's to go and look at, and a bulk verb that guessed at
1307
+ * one would be writing five guesses instead of one.
1308
+ *
1309
+ * One confirmation, on a terminal. Without a terminal it refuses
1310
+ * (`dangling-stdin-not-tty`) unless `--yes` is passed, which is the flag a
1311
+ * runbook uses after it has read the same list with `--json`. `--json` on its
1312
+ * own still asks, because the list and the question are the whole of what makes
1313
+ * this safe.
1314
+ */
1315
+ function resolveDangling(streams, cwd, front_, actor, deps) {
1316
+ const { flags, json, logPath } = front_;
1317
+ const check = preflightLog(logPath);
1318
+ if (!check.ok)
1319
+ return ioError(streams, json, check.message);
1320
+ const read = readVerifiedRecords(logPath);
1321
+ if (!read.ok)
1322
+ return emitRefusal(streams, json, read);
1323
+ const classFilter = stringFlag(flags, "--class");
1324
+ const index = indexDeclarations(read.records);
1325
+ // The LOG's repository, exactly as the `log-advance-cadence` doctor row asks
1326
+ // it: the refs that can prove anything about an advance are the ones in the
1327
+ // checkout the log lives in, which is not necessarily where the operator is
1328
+ // standing when they run this.
1329
+ const root = repoRoot(dirname(logPath)) ?? repoRoot(cwd);
1330
+ // The git read, once, for the whole list. `null` when this is not a git
1331
+ // checkout at all, in which case nothing is provable and every key is listed
1332
+ // as a person's — the fail-closed direction, and the honest one.
1333
+ const published = root === null
1334
+ ? { publishedSeq: 0, publishedRev: null }
1335
+ : publishedState(root, logPath, read.records, { remote: "origin", base: null }, now());
1336
+ const proved = new Map(proveDanglingAdvances(read.records, published).map((entry) => [entry.actionKey, entry]));
1337
+ const candidates = danglingExecutions([...read.records])
1338
+ .map((entry) => {
1339
+ const advance = proved.get(entry.actionKey);
1340
+ return {
1341
+ actionKey: entry.actionKey,
1342
+ task: entry.task,
1343
+ ts: entry.ts,
1344
+ seq: entry.seq,
1345
+ cls: index.declarations.get(entry.actionKey)?.class ?? null,
1346
+ provenBy: advance?.provenBy ?? null,
1347
+ toSeq: advance?.toSeq ?? null,
1348
+ };
1349
+ })
1350
+ .filter((entry) => classFilter === null || entry.cls === classFilter);
1351
+ const provable = candidates.filter((entry) => entry.provenBy !== null);
1352
+ const unprovable = candidates.filter((entry) => entry.provenBy === null);
1353
+ const listed = candidates.map((entry) => ({
1354
+ action_key: entry.actionKey,
1355
+ task: entry.task,
1356
+ class: entry.cls,
1357
+ seq: entry.seq,
1358
+ ts: entry.ts,
1359
+ provable: entry.provenBy !== null,
1360
+ proven_by: entry.provenBy,
1361
+ proven_seq: entry.provenBy === null ? null : entry.toSeq,
1362
+ ...(entry.provenBy === null ? { fix: manualResolveCommand(entry.actionKey) } : {}),
1363
+ }));
1364
+ // Nothing to do is exit 0 and says so: an empty list is a healthy log, and a
1365
+ // repair verb that failed when there was nothing to repair would be a repair
1366
+ // verb nobody could put in a runbook.
1367
+ if (candidates.length === 0) {
1368
+ if (json)
1369
+ emitJson(streams, { ok: true, dangling: [], resolved: [], unresolved: [], actor });
1370
+ else {
1371
+ streams.out(classFilter === null
1372
+ ? "no dangling executions: every execution in this log has an outcome\n"
1373
+ : `no dangling executions in class ${classFilter}\n`);
1374
+ }
1375
+ return EXIT_OK;
1376
+ }
1377
+ const st = style({ json });
1378
+ if (!json)
1379
+ streams.out(renderDanglingList(st, candidates));
1380
+ // The confirmation. `--yes` is the runbook's answer to it and the ONLY way
1381
+ // past it without a terminal: a prompter that fell back to a pipe would let
1382
+ // anything that can write bytes attest, on a record whose whole content is
1383
+ // that a person looked.
1384
+ if (!boolFlag(flags, "--yes")) {
1385
+ const prompter = deps.prompter === undefined ? createPrompter(streams) : deps.prompter;
1386
+ if (prompter === null) {
1387
+ return emitRefusal(streams, json, {
1388
+ ok: false,
1389
+ code: "dangling-stdin-not-tty",
1390
+ message: `stdin is not a terminal, so nobody can be asked to attest. ${String(provable.length)} execution(s) would be closed as completed on this checkout's own evidence, and a human-attested outcome nobody was asked about is not an attestation. Re-run it from a terminal, or pass --yes after reading the list (\`${RESOLVE_DANGLING_COMMAND} --json\`)`,
1391
+ });
1392
+ }
1393
+ if (provable.length === 0) {
1394
+ // There is nothing to confirm: the whole list is a person's to go and
1395
+ // look at, and asking a yes/no question about zero records would train an
1396
+ // operator to say yes to this prompt.
1397
+ if (json)
1398
+ emitJson(streams, { ok: true, dangling: listed, resolved: [], unresolved: listed.map((entry) => entry.action_key), actor });
1399
+ return EXIT_OK;
1400
+ }
1401
+ const agreed = confirmUntil(streams, prompter, `Close ${String(provable.length)} execution(s) as completed, attested by ${actor}?`, false);
1402
+ if (!agreed) {
1403
+ return emitRefusal(streams, json, {
1404
+ ok: false,
1405
+ code: "dangling-declined",
1406
+ message: "nothing was appended: the confirmation was declined",
1407
+ });
1408
+ }
1409
+ }
1410
+ const resolved = [];
1411
+ const failed = [];
1412
+ for (const entry of provable) {
1413
+ const result = resolveExecution(logPath, entry.actionKey, "completed", sweptNote(entry), actor, {
1414
+ policy: policyLocation(flags, cwd),
1415
+ });
1416
+ if (result.ok) {
1417
+ resolved.push({
1418
+ action_key: entry.actionKey,
1419
+ seq: result.record.seq,
1420
+ proven_by: entry.provenBy,
1421
+ });
1422
+ continue;
1423
+ }
1424
+ // One refusal does not stop the sweep: the keys are independent, and a
1425
+ // fourth that cannot be closed is no reason to leave the fifth open. Every
1426
+ // refusal is reported by code, and the exit code below says some of them
1427
+ // were refused.
1428
+ failed.push({ action_key: entry.actionKey, code: result.code, message: result.message });
1429
+ }
1430
+ if (json) {
1431
+ emitJson(streams, {
1432
+ ok: failed.length === 0,
1433
+ dangling: listed,
1434
+ resolved,
1435
+ unresolved: [
1436
+ ...unprovable.map((entry) => entry.actionKey),
1437
+ ...failed.map((entry) => entry.action_key),
1438
+ ],
1439
+ ...(failed.length === 0 ? {} : { failed }),
1440
+ attested_by_human: true,
1441
+ actor,
1442
+ });
1443
+ }
1444
+ else {
1445
+ for (const entry of resolved) {
1446
+ streams.out(`resolved ${entry.action_key} as completed at seq ${String(entry.seq)} by ${actor} (human-attested, no exit code; ${String(entry.proven_by)})\n`);
1447
+ }
1448
+ for (const entry of failed) {
1449
+ streams.err(`${renderRefusal(st, entry.code, `${entry.action_key}: ${entry.message}`)}\n`);
1450
+ }
1451
+ if (unprovable.length > 0) {
1452
+ streams.out(`${String(unprovable.length)} execution(s) were left alone: nothing in this checkout can prove how they ended, and only a person who goes and looks may say. Close each with its own command, listed above.\n`);
1453
+ }
1454
+ }
1455
+ return failed.length === 0 ? EXIT_OK : EXIT_INTEGRITY;
1456
+ }
1457
+ /** The list, as a person reads it: what is provable, by what, and what is not. */
1458
+ function renderDanglingList(st, candidates) {
1459
+ const rows = candidates.map((entry) => ({
1460
+ left: entry.actionKey,
1461
+ right: entry.provenBy === null
1462
+ ? st.fail("nothing proves how it ended")
1463
+ : st.ok(`${entry.provenBy} carries seq ${String(entry.toSeq)}`),
1464
+ under: [
1465
+ `seq ${String(entry.seq)} ${entry.ts} ${entry.cls ?? "class undeclared"}${entry.task === null ? "" : ` ${entry.task}`}`,
1466
+ ...(entry.provenBy === null ? [manualResolveCommand(entry.actionKey)] : []),
1467
+ ],
1468
+ }));
1469
+ return `${st.table(rows)}\n`;
1470
+ }
1471
+ /**
1472
+ * `approval execution resolve <action-key> --outcome completed|failed --note …`
1473
+ *
1474
+ * The human recovery verb for a dangling execution: the runtime died between
1475
+ * `execution.started` and its outcome, `approval status` has been reporting the
1476
+ * gap ever since, and a person went and looked. This records what they saw.
1477
+ *
1478
+ * Three rules, enforced here as usage errors before core is called at all, so
1479
+ * the log is untouched by a malformed invocation:
1480
+ *
1481
+ * - `--outcome` is `completed` or `failed`. Nothing is inferred.
1482
+ * - `--note` is MANDATORY and non-empty. The event's whole value is the
1483
+ * observation behind it.
1484
+ * - The actor must be a human (`--as human:<id>` or `APPROVAL_HUMAN`). An agent
1485
+ * closing its own dangling execution is the executing party reporting on
1486
+ * itself.
1487
+ *
1488
+ * No attestation is required, and the help text says why: resolve records a
1489
+ * fact a human observed; it exercises no policy authority, so it does not
1490
+ * require an attested policy.
1491
+ *
1492
+ * `--dangling` is the bulk form of the same verb ({@link resolveDangling}): the
1493
+ * whole list, one confirmation, one record per key the checkout can prove.
1494
+ */
1495
+ export function commandResolve(argv, streams, cwd, deps = {}) {
1496
+ const outcomeFront = front(argv, {
1497
+ ...COMMON_FLAGS,
1498
+ "--outcome": "string",
1499
+ "--note": "string",
1500
+ "--as": "string",
1501
+ "--dangling": "boolean",
1502
+ "--class": "string",
1503
+ "--yes": "boolean",
1504
+ }, RESOLVE_HELP, streams, cwd);
1505
+ if (outcomeFront.kind === "handled")
1506
+ return outcomeFront.code;
1507
+ const { flags, positionals, json, logPath } = outcomeFront;
1508
+ // The actor is settled before the two forms diverge: both write a
1509
+ // human-attested record and neither may be performed by an agent.
1510
+ const asFlag0 = stringFlag(flags, "--as");
1511
+ const actor0 = resolveHumanActor(asFlag0 === null ? {} : { actor: asFlag0 });
1512
+ const bulk = boolFlag(flags, "--dangling");
1513
+ const actionKey = positionals[0];
1514
+ if (bulk) {
1515
+ if (actionKey !== undefined) {
1516
+ return usageError(streams, json, `--dangling takes no <action-key>: it acts on every dangling execution the log holds, and naming one is the single form (drop --dangling). Got ${JSON.stringify(actionKey)}`, RESOLVE_HELP);
1517
+ }
1518
+ if (stringFlag(flags, "--outcome") !== null || stringFlag(flags, "--note") !== null) {
1519
+ return usageError(streams, json, "--dangling takes neither --outcome nor --note: it records `completed` for exactly the keys this checkout can prove completed, and writes each note from that evidence. Nothing is inferred for a key nothing proves — those are listed and left alone", RESOLVE_HELP);
1520
+ }
1521
+ if (actor0 === null) {
1522
+ return usageError(streams, json, asFlag0 === null
1523
+ ? `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`
1524
+ : `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag0)}; resolve records what a person observed and an agent: or system: actor cannot perform it`, RESOLVE_HELP);
1525
+ }
1526
+ return resolveDangling(streams, cwd, outcomeFront, actor0, deps);
1527
+ }
1528
+ if (stringFlag(flags, "--class") !== null || boolFlag(flags, "--yes")) {
1529
+ return usageError(streams, json, "--class and --yes belong to the bulk form: they select and confirm a LIST, and the single form already names its one key and takes its one note. Add --dangling, or drop them", RESOLVE_HELP);
1530
+ }
1531
+ if (actionKey === undefined) {
1532
+ return usageError(streams, json, "missing <action-key> argument (or --dangling for every dangling execution at once)", RESOLVE_HELP);
1533
+ }
1534
+ const extra = positionals[1];
1535
+ if (extra !== undefined) {
1536
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, RESOLVE_HELP);
1537
+ }
1538
+ const outcomeFlag = stringFlag(flags, "--outcome");
1539
+ if (outcomeFlag === null) {
1540
+ return usageError(streams, json, "missing --outcome completed|failed", RESOLVE_HELP);
1541
+ }
1542
+ if (outcomeFlag !== "completed" && outcomeFlag !== "failed") {
1543
+ return usageError(streams, json, `--outcome expects completed or failed, got ${JSON.stringify(outcomeFlag)}; nothing is inferred from a dangling execution`, RESOLVE_HELP);
1544
+ }
1545
+ const outcome = outcomeFlag;
1546
+ const note = stringFlag(flags, "--note");
1547
+ if (note === null || note.trim().length === 0) {
1548
+ return usageError(streams, json, note === null
1549
+ ? "missing --note \"<what you observed>\": resolve records a human observation, and an unexplained attested outcome cannot be told apart from a guess"
1550
+ : "--note must not be empty: resolve records a human observation, and an unexplained attested outcome cannot be told apart from a guess", RESOLVE_HELP);
1551
+ }
1552
+ const asFlag = stringFlag(flags, "--as");
1553
+ const actor = resolveHumanActor(asFlag === null ? {} : { actor: asFlag });
1554
+ if (actor === null) {
1555
+ if (asFlag !== null) {
1556
+ return usageError(streams, json, `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag)}; resolve records what a person observed and an agent: or system: actor cannot perform it`, RESOLVE_HELP);
1557
+ }
1558
+ return usageError(streams, json, `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`, RESOLVE_HELP);
1559
+ }
1560
+ const result = resolveExecution(logPath, actionKey, outcome, note, actor, {
1561
+ policy: policyLocation(flags, cwd),
1562
+ });
1563
+ if (!result.ok)
1564
+ return emitRefusal(streams, json, result);
1565
+ if (json) {
1566
+ emitJson(streams, {
1567
+ ok: true,
1568
+ action_key: actionKey,
1569
+ task: result.task,
1570
+ event: result.event,
1571
+ outcome: result.outcome,
1572
+ seq: result.record.seq,
1573
+ attested_by_human: true,
1574
+ actor,
1575
+ });
1576
+ }
1577
+ else {
1578
+ streams.out(`resolved ${actionKey} as ${result.outcome} at seq ${result.record.seq} by ${actor} (human-attested, no exit code)\n`);
1579
+ }
1580
+ return EXIT_OK;
1581
+ }
1582
+ // ===========================================================================
1583
+ // approval execution reconcile
1584
+ // ===========================================================================
1585
+ /**
1586
+ * `approval execution reconcile <action-key> --resolution executed|not-executed
1587
+ * --note …`
1588
+ *
1589
+ * The human resolution of an INDETERMINATE execution (APRV-120): the side
1590
+ * effect was attempted, the runtime could not tell whether it committed, and a
1591
+ * person went and looked at the far side.
1592
+ *
1593
+ * It is a separate verb from `resolve` rather than a third `--outcome` value,
1594
+ * because the two answer different questions from different evidence. `resolve`
1595
+ * asks "what did our runtime do?" and is answered from this machine. This asks
1596
+ * "did the provider commit?" and is answered from the provider's own console,
1597
+ * inbox or ledger. An operator who reached for the wrong one is told so
1598
+ * (`not-indeterminate`, `already-finished`) rather than quietly writing the
1599
+ * wrong record into an append-only log.
1600
+ *
1601
+ * The same three rules `resolve` enforces, enforced here before core is called:
1602
+ * the resolution is one of two closed values and nothing is inferred, the note
1603
+ * is mandatory and non-empty because it is the evidence, and the actor must be
1604
+ * a human — the daemon never auto-resolves, and an agent reconciling its own
1605
+ * unknown outcome is the executing party reporting on itself.
1606
+ */
1607
+ export function commandReconcile(argv, streams, cwd) {
1608
+ const outcomeFront = front(argv, { ...COMMON_FLAGS, "--resolution": "string", "--note": "string", "--as": "string" }, RECONCILE_HELP, streams, cwd);
1609
+ if (outcomeFront.kind === "handled")
1610
+ return outcomeFront.code;
1611
+ const { flags, positionals, json, logPath } = outcomeFront;
1612
+ const actionKey = positionals[0];
1613
+ if (actionKey === undefined) {
1614
+ return usageError(streams, json, "missing <action-key> argument", RECONCILE_HELP);
1615
+ }
1616
+ const extra = positionals[1];
1617
+ if (extra !== undefined) {
1618
+ return usageError(streams, json, `unexpected argument ${JSON.stringify(extra)}`, RECONCILE_HELP);
1619
+ }
1620
+ const resolutionFlag = stringFlag(flags, "--resolution");
1621
+ if (resolutionFlag === null) {
1622
+ return usageError(streams, json, "missing --resolution executed|not-executed", RECONCILE_HELP);
1623
+ }
1624
+ if (!isReconcileResolution(resolutionFlag)) {
1625
+ return usageError(streams, json, `--resolution expects executed or not-executed, got ${JSON.stringify(resolutionFlag)}; nothing is inferred about an outcome the runtime could not observe`, RECONCILE_HELP);
1626
+ }
1627
+ const note = stringFlag(flags, "--note");
1628
+ if (note === null || note.trim().length === 0) {
1629
+ return usageError(streams, json, note === null
1630
+ ? 'missing --note "<the evidence>": reconcile records what a person established about an unknown outcome, and an unexplained resolution cannot be told apart from a guess'
1631
+ : "--note must not be empty: reconcile records what a person established about an unknown outcome, and an unexplained resolution cannot be told apart from a guess", RECONCILE_HELP);
1632
+ }
1633
+ const asFlag = stringFlag(flags, "--as");
1634
+ const actor = resolveHumanActor(asFlag === null ? {} : { actor: asFlag });
1635
+ if (actor === null) {
1636
+ if (asFlag !== null) {
1637
+ return usageError(streams, json, `--as expects a human identity matching human:<id>, got ${JSON.stringify(asFlag)}; reconcile records what a person established and an agent: or system: actor cannot perform it`, RECONCILE_HELP);
1638
+ }
1639
+ return usageError(streams, json, `no human identity: set ${HUMAN_ACTOR_ENV}=human:<id> or pass --as human:<id>`, RECONCILE_HELP);
1640
+ }
1641
+ const result = reconcileExecution(logPath, actionKey, resolutionFlag, note, actor, {
1642
+ policy: policyLocation(flags, cwd),
1643
+ });
1644
+ if (!result.ok)
1645
+ return emitRefusal(streams, json, result);
1646
+ if (json) {
1647
+ emitJson(streams, {
1648
+ ok: true,
1649
+ action_key: actionKey,
1650
+ task: result.task,
1651
+ event: "execution.reconciled",
1652
+ resolution: result.resolution,
1653
+ indeterminate_seq: result.indeterminateSeq,
1654
+ seq: result.record.seq,
1655
+ attested_by_human: true,
1656
+ actor,
1657
+ });
1658
+ }
1659
+ else {
1660
+ streams.out(`reconciled ${actionKey} as ${result.resolution} at seq ${result.record.seq} by ${actor}, resolving the indeterminate outcome at seq ${result.indeterminateSeq} (human-attested; the idempotency key stays spent)\n`);
1661
+ }
1662
+ return EXIT_OK;
1663
+ }
1664
+ /** `approval execution <subcommand>`: `resolve` and `reconcile`. */
1665
+ export function commandExecution(argv, streams, cwd, deps = {}) {
1666
+ const sub = argv[0];
1667
+ const rest = argv.slice(1);
1668
+ const json = argv.includes("--json");
1669
+ if (sub === undefined) {
1670
+ return usageError(streams, json, "missing subcommand for `approval execution`", EXECUTION_HELP);
1671
+ }
1672
+ if (sub === "--help" || sub === "-h" || sub === "help") {
1673
+ streams.out(`${EXECUTION_HELP}\n`);
1674
+ return EXIT_OK;
1675
+ }
1676
+ if (sub === "resolve")
1677
+ return commandResolve(rest, streams, cwd, deps);
1678
+ if (sub === "reconcile")
1679
+ return commandReconcile(rest, streams, cwd);
1680
+ return usageError(streams, json, `unknown subcommand ${JSON.stringify(sub)} for \`approval execution\``, EXECUTION_HELP);
1681
+ }
1682
+ //# sourceMappingURL=execute.js.map