@codespar/cli 0.16.0 → 0.18.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 (486) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/mandate-verify.js +98 -19
  3. package/dist/commands/mandate-verify.js.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mandate-codec.js +31 -20
  7. package/dist/mandate-codec.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/bills-agent/README.md +1 -1
  10. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  11. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  12. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  13. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  14. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  15. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  16. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  17. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  18. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  19. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  20. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  21. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  22. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  23. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  24. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  25. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  26. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  27. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  28. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  29. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  30. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  31. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  32. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  33. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  34. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  35. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  36. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  37. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  38. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  39. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  40. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  41. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  42. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  43. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  44. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  45. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  46. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  47. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  48. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  49. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  50. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  54. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  55. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  56. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  57. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  63. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  64. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  66. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  67. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  68. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  69. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  70. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  71. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  72. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  73. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  74. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  75. package/templates/checkout-agent/LICENSE +21 -0
  76. package/templates/checkout-agent/README.md +28 -0
  77. package/templates/checkout-agent/_gitignore +10 -0
  78. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  79. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  80. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  81. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  82. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  83. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  85. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  86. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  87. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  88. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  110. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  111. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  112. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  113. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  114. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  115. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  116. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  117. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  142. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  143. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  144. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  145. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  148. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  149. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  150. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  151. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  158. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  159. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  160. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  161. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  162. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  163. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  164. package/templates/checkout-agent/package.json +38 -0
  165. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  166. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  167. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  169. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  170. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  171. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  172. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  173. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  174. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  175. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  176. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  177. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  178. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  179. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  180. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  181. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  182. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  183. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  184. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  185. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  186. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  187. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  188. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  189. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  190. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  191. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  192. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  193. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  194. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  195. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  196. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  197. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  198. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  199. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  200. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  201. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  202. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  203. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  204. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  205. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  206. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  207. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  208. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  209. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  210. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  211. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  212. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  213. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  214. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  215. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  216. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  217. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  218. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  219. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  220. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  221. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  222. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  223. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  224. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  225. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  226. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  227. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  228. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  262. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  263. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  264. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  265. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  269. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  270. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  271. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  272. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  273. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  274. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  275. package/templates/checkout-agent/tsconfig.base.json +16 -0
  276. package/templates/checkout-agent/vitest.config.ts +12 -0
  277. package/templates/collections-agent/README.md +6 -1
  278. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  279. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  280. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  281. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  282. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  283. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  284. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  285. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  286. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  287. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  288. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  289. package/templates/collections-agent/package.json +1 -0
  290. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  291. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  292. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  293. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  294. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  295. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  296. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  297. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  298. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  299. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  300. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  301. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  302. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  303. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  304. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  305. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  306. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  307. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  308. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  309. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  310. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  311. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  312. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  313. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  314. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  315. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  316. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  317. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  318. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  319. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  320. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  321. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  322. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  323. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  324. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  325. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  326. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  330. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  331. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  332. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  333. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  339. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  340. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  342. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  343. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  344. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  345. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  346. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  347. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  348. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  349. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  350. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  351. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  352. package/templates/hello-agent/README.md +1 -1
  353. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  354. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  355. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  356. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  357. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  358. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  359. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  360. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  361. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  362. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  363. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  364. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  365. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  366. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  367. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  368. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  369. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  370. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  371. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  372. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  373. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  374. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  375. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  376. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  377. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  378. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  379. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  380. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  381. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  382. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  383. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  384. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  385. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  386. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  387. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  388. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  389. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  393. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  394. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  395. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  396. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  402. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  403. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  405. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  406. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  407. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  408. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  409. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  410. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  411. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  412. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  413. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  414. package/templates/kits.lock.json +29 -6
  415. package/templates/supplier-payments-agent/README.md +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  418. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  419. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  420. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  421. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  422. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  423. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  424. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  426. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  427. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  428. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  429. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  430. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  431. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  432. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  433. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  434. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  435. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  437. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  438. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  439. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  440. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  441. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  442. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  444. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  445. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  446. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  447. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  452. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  453. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  454. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  455. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  456. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  457. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  475. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  476. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  477. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  478. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  479. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  481. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  483. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  484. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  485. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  486. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,596 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { ESCOLA, FUNCIONARIA, harness, MERCADO } from "./helpers.js";
5
+
6
+ const approver = { id: "usr_demo", channel: "terminal" };
7
+
8
+ describe("section 4.6: the model proposes, the code executes", () => {
9
+ it("human mode: drafted -> awaiting_approval -> approved -> executing -> settled, with an artifact and a receipt", async () => {
10
+ const h = harness({ mode: "human" });
11
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 185000, description: "outubro" }] });
12
+ expect(draft.ok).toBe(true);
13
+ if (!draft.ok) return;
14
+ expect(draft.execution.state).toBe("awaiting_approval");
15
+ expect(draft.execution.items[0]).toMatchObject({ alias: "escola", beneficiary: "Escola Aurora", payee: ESCOLA, amount: 185000 });
16
+ expect(draft.execution.total).toBe(185000);
17
+
18
+ const approved = h.engine.approve(draft.execution.id, approver);
19
+ expect(approved.state).toBe("approved");
20
+ expect(approved.approval_id).toMatch(/^apr_/);
21
+ const artifact = h.store.getApproval(approved.approval_id!)!;
22
+ expect(artifact.approver).toEqual({ type: "person", id: "usr_demo", channel: "terminal" });
23
+ expect(artifact.items_hash).toBe(approved.items_hash);
24
+
25
+ const settled = await h.engine.execute(approved.id);
26
+ expect(settled.state).toBe("settled");
27
+ expect(settled.history.map((t) => t.to)).toEqual(["awaiting_approval", "approved", "executing", "settled"]);
28
+ expect(settled.outcomes[0]?.receipt_id).toMatch(/^rcpt_stub_/);
29
+ const receipts = readdirSync(join(h.bundle.dir, "receipts"));
30
+ expect(receipts).toHaveLength(1);
31
+ expect(h.bundle.readApprovals()).toHaveLength(1);
32
+ expect(existsSync(join(h.bundle.dir, "events.jsonl"))).toBe(true);
33
+ for (const event of h.bundle.readEvents()) expect(event["actor"]).toBeDefined();
34
+ });
35
+
36
+ it("mandate mode: drafted -> approved by the mandate -> settled, same trail shape", async () => {
37
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
38
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
39
+ if (!draft.ok) throw new Error("refused");
40
+ expect(draft.execution.state).toBe("approved");
41
+ const artifact = h.store.getApproval(draft.execution.approval_id!)!;
42
+ expect(artifact.approver).toEqual({ type: "mandate", id: "mdt_test_0001" });
43
+ const settled = await h.engine.execute(draft.execution.id);
44
+ expect(settled.state).toBe("settled");
45
+ });
46
+
47
+ it("the core computes the total; the model's number is recorded and ignored", async () => {
48
+ const h = harness({ mode: "human" });
49
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 10000 }, { payee: "mercado", amount: 5000 }], claimed_total: 12000 });
50
+ if (!draft.ok) throw new Error("refused");
51
+ expect(draft.execution.total).toBe(15000);
52
+ expect(draft.execution.model_claimed_total).toBe(12000);
53
+ const approved = h.engine.approve(draft.execution.id, approver);
54
+ expect(h.store.getApproval(approved.approval_id!)!.items.reduce((s, i) => s + i.amount, 0)).toBe(15000);
55
+ });
56
+
57
+ it("with model_total_mismatch: refuse, the execution is denied instead", async () => {
58
+ const h = harness({ mode: "human", guardrails: { model_total_mismatch: "refuse" } });
59
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 15000 }], claimed_total: 12000 });
60
+ if (!draft.ok) throw new Error("refused");
61
+ expect(draft.execution.state).toBe("denied");
62
+ expect(draft.execution.reason).toBe("model_total_mismatch");
63
+ });
64
+ });
65
+
66
+ describe("mandate limits are the core's, in both modes", () => {
67
+ it("per-transaction cap: denied, readable", async () => {
68
+ for (const mode of ["human", "mandate"] as const) {
69
+ const h = harness({ mode });
70
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 250001 }] });
71
+ if (!draft.ok) throw new Error("refused");
72
+ expect(draft.execution.state).toBe("denied");
73
+ expect(draft.execution.reason).toBe("per_tx_cap_exceeded");
74
+ expect(draft.execution.detail).toContain("per-payment cap");
75
+ }
76
+ });
77
+
78
+ it("window cap counts what is settled and in flight", async () => {
79
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
80
+ for (let i = 0; i < 2; i += 1) {
81
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 250000 }] });
82
+ if (!d.ok) throw new Error("refused");
83
+ await h.engine.execute(d.execution.id);
84
+ }
85
+ const third = await h.engine.draft({ items: [{ payee: "mercado", amount: 100001 }] });
86
+ if (!third.ok) throw new Error("refused");
87
+ expect(third.execution.state).toBe("denied");
88
+ expect(third.execution.reason).toBe("window_cap_exceeded");
89
+ const fits = await h.engine.draft({ items: [{ payee: "mercado", amount: 100000 }] });
90
+ if (!fits.ok) throw new Error("refused");
91
+ expect(fits.execution.state).toBe("approved");
92
+ });
93
+
94
+ it("payee outside the allowlist: denied in mandate, escalated in human, and never approvable", async () => {
95
+ const m = harness({ mode: "mandate" });
96
+ const denied = await m.engine.draft({ items: [{ payee: "chave-nova@banco.com", amount: 1000 }] });
97
+ if (!denied.ok) throw new Error("refused");
98
+ expect(denied.execution.state).toBe("denied");
99
+ expect(denied.execution.reason).toBe("beneficiary_not_allowed");
100
+
101
+ const hh = harness({ mode: "human" });
102
+ const escalated = await hh.engine.draft({ items: [{ payee: "chave-nova@banco.com", amount: 1000 }] });
103
+ if (!escalated.ok) throw new Error("refused");
104
+ expect(escalated.execution.state).toBe("awaiting_approval");
105
+ expect(escalated.execution.blocking_reasons).toEqual(["beneficiary_not_allowed"]);
106
+ const afterYes = hh.engine.approve(escalated.execution.id, approver);
107
+ expect(afterYes.state).toBe("denied");
108
+ expect(afterYes.reason).toBe("beneficiary_not_allowed");
109
+ });
110
+ });
111
+
112
+ describe("section 4.4 in the engine", () => {
113
+ it("amount above threshold in mandate mode goes to a human; the artifact records the trigger", async () => {
114
+ const h = harness({ mode: "mandate" });
115
+ // Make escola a known payee first, so only `amount` can fire.
116
+ const warm = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
117
+ if (!warm.ok) throw new Error("refused");
118
+ expect(warm.execution.escalation?.trigger).toBe("new_beneficiary");
119
+ await h.engine.execute(h.engine.approve(warm.execution.id, approver).id);
120
+
121
+ const big = await h.engine.draft({ items: [{ payee: "escola", amount: 150001 }] });
122
+ if (!big.ok) throw new Error("refused");
123
+ expect(big.execution.state).toBe("awaiting_approval");
124
+ expect(big.execution.escalation?.trigger).toBe("amount");
125
+ const approved = h.engine.approve(big.execution.id, approver);
126
+ expect(h.store.getApproval(approved.approval_id!)!.escalation?.trigger).toBe("amount");
127
+
128
+ await h.engine.execute(approved.id);
129
+
130
+ // The following one, below the threshold, runs alone: a human-approved payment is not fractioning.
131
+ const small = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
132
+ if (!small.ok) throw new Error("refused");
133
+ expect(small.execution.state).toBe("approved");
134
+ });
135
+
136
+ it("fractioning: five parts below the threshold escalate by the window", async () => {
137
+ const h = harness({ mode: "mandate", manifest: { escalate_above: { amount: 150000 } }, guardrails: { escalate_above: { amount: 150000 } } });
138
+ const states: string[] = [];
139
+ for (let i = 0; i < 5; i += 1) {
140
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 40000 }] });
141
+ if (!d.ok) throw new Error("refused");
142
+ states.push(d.execution.state);
143
+ if (d.execution.state === "approved") await h.engine.execute(d.execution.id);
144
+ }
145
+ expect(states).toEqual(["approved", "approved", "approved", "awaiting_approval", "awaiting_approval"]);
146
+ });
147
+
148
+ it("outside hours escalates, or refuses when the guardrail says so", async () => {
149
+ const night = new Date("2026-09-24T02:00:00Z");
150
+ const a = harness({ mode: "mandate", now: night, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" } } });
151
+ const d1 = await a.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
152
+ if (!d1.ok) throw new Error("refused");
153
+ expect(d1.execution.state).toBe("awaiting_approval");
154
+ expect(d1.execution.escalation?.trigger).toBe("outside_hours");
155
+
156
+ const b = harness({ mode: "mandate", now: night, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" }, outside_hours_action: "refuse" } });
157
+ const d2 = await b.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
158
+ if (!d2.ok) throw new Error("refused");
159
+ expect(d2.execution.state).toBe("denied");
160
+ expect(d2.execution.reason).toBe("outside_hours");
161
+ });
162
+ });
163
+
164
+ describe("section 4.7: revocation and kill switch (against the stub)", () => {
165
+ it("a new request after revocation is refused before drafted", async () => {
166
+ const h = harness({ mode: "human" });
167
+ h.gate.revoke("mdt_test_0001");
168
+ const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
169
+ expect(draft).toMatchObject({ ok: false, refused_before_draft: true, reason: "mandate_revoked" });
170
+ expect(h.store.listExecutions()).toHaveLength(0);
171
+ });
172
+
173
+ it("revoked while awaiting approval: approved artifact does not help, execute answers denied", async () => {
174
+ const h = harness({ mode: "human" });
175
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
176
+ if (!d.ok) throw new Error("refused");
177
+ const approved = h.engine.approve(d.execution.id, approver);
178
+ h.gate.revoke("mdt_test_0001");
179
+ const out = await h.engine.execute(approved.id);
180
+ expect(out.state).toBe("denied");
181
+ expect(out.reason).toBe("mandate_revoked");
182
+ expect(h.store.listOutbox()).toHaveLength(0);
183
+ });
184
+
185
+ it("org pauseAll denies with org_paused; paused mandate with mandate_paused", async () => {
186
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
187
+ h.gate.pauseAll();
188
+ expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, reason: "org_paused" });
189
+ h.gate.resumeAll();
190
+ h.gate.pause("mdt_test_0001");
191
+ expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, reason: "mandate_paused" });
192
+ h.gate.resume("mdt_test_0001");
193
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
194
+ expect(d.ok && d.execution.state).toBe("approved");
195
+ });
196
+
197
+ it("revoked while executing: reconciled from the rail, not cancelled", async () => {
198
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
199
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
200
+ if (!d.ok) throw new Error("refused");
201
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
202
+ // The provider took the payment and answered late: uncertain now, in flight on the first lookup, settled on the second.
203
+ const h2 = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt], inFlightThenSettled: [attempt] } });
204
+ const stuck = await h2.engine.execute(d.execution.id);
205
+ expect(stuck.state).toBe("executing");
206
+ h2.gate.revoke("mdt_test_0001");
207
+ expect((await h2.engine.reconcile(stuck.id)).state).toBe("executing");
208
+ const closed = await h2.engine.reconcile(stuck.id);
209
+ expect(closed.state).toBe("settled");
210
+ expect(h2.rail.payCount).toBe(0);
211
+ });
212
+ });
213
+
214
+ describe("section 10: idempotency, restart, reconcile", () => {
215
+ it("killing after the rail accepted and resuming gives one payment and one receipt", async () => {
216
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("SIGKILL simulated after dispatch"); } } });
217
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
218
+ if (!d.ok) throw new Error("refused");
219
+ await expect(h.engine.execute(d.execution.id)).rejects.toThrow("SIGKILL");
220
+ const stuck = h.store.getExecution(d.execution.id)!;
221
+ expect(stuck.state).toBe("executing");
222
+ expect(h.store.listOutbox({ status: ["sent"] })).toHaveLength(1);
223
+
224
+ // A fresh process over the same state.db.
225
+ const resumed = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
226
+ const done = await resumed.engine.reconcile(stuck.id);
227
+ expect(done.state).toBe("settled");
228
+ expect(readdirSync(join(resumed.bundle.dir, "receipts"))).toHaveLength(1);
229
+ // Exactly one attempt reached the rail, ever.
230
+ expect(resumed.store.stubRailGet(`att_${stuck.idempotency_key.slice(4)}_0`)).toBeDefined();
231
+ const events = resumed.store.listEvents({ execution_id: stuck.id }).filter((e) => e.type === "commerce.payment.succeeded");
232
+ expect(events).toHaveLength(1);
233
+ });
234
+
235
+ it("an uncertain outcome is never retried blind: it stays executing, marked unresolved, until the rail answers", async () => {
236
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
237
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
238
+ if (!d.ok) throw new Error("refused");
239
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
240
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
241
+ const stuck = await flaky.engine.execute(d.execution.id);
242
+ expect(stuck.state).toBe("executing");
243
+ expect(stuck.reason).toBe("rail_uncertain");
244
+ expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
245
+ const still = await flaky.engine.reconcile(stuck.id);
246
+ expect(still.state).toBe("executing");
247
+ expect(still.outcomes).toHaveLength(0);
248
+ expect(flaky.rail.payCount).toBe(0);
249
+ });
250
+
251
+ it("duplicate and out-of-order rail events settle once", async () => {
252
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
253
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
254
+ if (!d.ok) throw new Error("refused");
255
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
256
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
257
+ const stuck = await flaky.engine.execute(d.execution.id);
258
+ expect(stuck.state).toBe("executing");
259
+ const paid = { event_id: "evt_paid_1", type: "commerce.payment.succeeded", attempt_id: attempt };
260
+ expect(flaky.engine.ingestExternalEvent(paid)).toEqual({ applied: true, reason: "settled" });
261
+ expect(flaky.engine.ingestExternalEvent(paid)).toEqual({ applied: false, reason: "duplicate event id" });
262
+ expect(flaky.engine.ingestExternalEvent({ event_id: "evt_created_late", type: "commerce.payment.created", attempt_id: attempt })).toMatchObject({ applied: false });
263
+ expect(flaky.engine.ingestExternalEvent({ event_id: "evt_paid_2", type: "commerce.payment.succeeded", attempt_id: attempt })).toMatchObject({ applied: false, reason: "execution already settled" });
264
+ expect(flaky.store.getExecution(stuck.id)!.state).toBe("settled");
265
+ expect(flaky.store.listEvents({ execution_id: stuck.id }).filter((e) => e.type === "execution.transition" && (e.payload as { to: string }).to === "settled")).toHaveLength(1);
266
+ });
267
+
268
+ it("a failed rail answer closes as failed with the code, and a second draft to the same payee is a new execution", async () => {
269
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { refusePayees: [MERCADO] } });
270
+ const d = await h.engine.draft({ items: [{ payee: "mercado", amount: 1000 }] });
271
+ if (!d.ok) throw new Error("refused");
272
+ const failed = await h.engine.execute(d.execution.id);
273
+ expect(failed.state).toBe("failed");
274
+ expect(failed.detail).toContain("psp_dispatch_failed");
275
+ });
276
+
277
+ it("a stale open execution expires", async () => {
278
+ const h = harness({ mode: "human" });
279
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
280
+ if (!d.ok) throw new Error("refused");
281
+ h.setNow(new Date(h.now.getTime() + 16 * 60 * 1000));
282
+ const expired = h.engine.expireStale();
283
+ expect(expired.map((e) => e.state)).toEqual(["expired"]);
284
+ });
285
+
286
+ it("the items_hash is recomputed before executing; a changed list goes back to awaiting_approval", async () => {
287
+ const h = harness({ mode: "human" });
288
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
289
+ if (!d.ok) throw new Error("refused");
290
+ const approved = h.engine.approve(d.execution.id, approver);
291
+ // Tamper with the persisted list after approval, the way a bug or an attacker would.
292
+ h.store.saveExecution({ ...approved, items: [{ ...approved.items[0]!, amount: 999999 }] });
293
+ const back = await h.engine.execute(approved.id);
294
+ expect(back.state).toBe("awaiting_approval");
295
+ expect(back.reason).toBe("items_hash_mismatch");
296
+ expect(back.approval_id).toBeUndefined();
297
+ expect(h.store.listOutbox()).toHaveLength(0);
298
+ });
299
+ });
300
+
301
+ describe("the policy runs at every gate, not only at draft (review of PR #1)", () => {
302
+ it("two drafts each inside the window cap cannot both settle: the second is refused by the reservation, and the last gate refuses what slipped past", async () => {
303
+ const h = harness({ mode: "human" });
304
+ const a = await h.engine.draft({ items: [{ payee: "escola", amount: 240000 }, { payee: "mercado", amount: 160000 }] }); // 400000
305
+ if (!a.ok) throw new Error("refused");
306
+ expect(a.execution.state).toBe("awaiting_approval");
307
+ const b = await h.engine.draft({ items: [{ payee: "funcionaria", amount: 200000 }, { payee: "mercado", amount: 100000 }] }); // 300000
308
+ if (!b.ok) throw new Error("refused");
309
+ expect(b.execution.state).toBe("denied");
310
+ expect(b.execution.reason).toBe("window_cap_exceeded");
311
+
312
+ // What slipped past: an approved execution whose window filled up before it ran (another process settled 300000 meanwhile).
313
+ const approvedA = h.engine.approve(a.execution.id, approver);
314
+ expect(approvedA.state).toBe("approved");
315
+ const foreign = { ...approvedA, id: "exe_other_process", state: "settled" as const, total: 300000, approval_id: undefined, history: [] };
316
+ h.store.saveExecution(foreign as never);
317
+ const last = await h.engine.execute(approvedA.id);
318
+ expect(last.state).toBe("denied");
319
+ expect(last.reason).toBe("window_cap_exceeded");
320
+ expect(h.store.listOutbox()).toHaveLength(0);
321
+ expect(h.engine.list({ state: "settled" }).reduce((s, e) => s + e.total, 0)).toBe(300000);
322
+ });
323
+
324
+ it("a list tampered after approval goes back to awaiting_approval, and a yes on the tampered list is refused by the policy, not re-approved", async () => {
325
+ const h = harness({ mode: "human" });
326
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
327
+ if (!d.ok) throw new Error("refused");
328
+ const approved = h.engine.approve(d.execution.id, approver);
329
+ h.store.saveExecution({ ...approved, items: [{ ...approved.items[0]!, beneficiary: "atacante", payee: "pix@atacante.example.com" }] });
330
+ const back = await h.engine.execute(approved.id);
331
+ expect(back.state).toBe("awaiting_approval");
332
+ expect(back.reason).toBe("items_hash_mismatch");
333
+ const afterYes = h.engine.approve(back.id, approver);
334
+ expect(afterYes.state).toBe("denied");
335
+ expect(afterYes.reason).toBe("beneficiary_not_allowed");
336
+ expect(h.store.listOutbox()).toHaveLength(0);
337
+ });
338
+
339
+ it("a mandate-approved execution re-checks escalate_above at the last gate; a person's approval satisfies it", async () => {
340
+ // Approved by the mandate during the day, executed at night: back to a human.
341
+ const day = new Date("2026-09-23T18:00:00Z");
342
+ const h = harness({ mode: "mandate", now: day, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" }, approval_ttl_minutes: 24 * 60 } });
343
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
344
+ if (!d.ok) throw new Error("refused");
345
+ expect(d.execution.state).toBe("approved");
346
+ h.setNow(new Date("2026-09-24T02:00:00Z"));
347
+ const night = await h.engine.execute(d.execution.id);
348
+ expect(night.state).toBe("awaiting_approval");
349
+ expect(night.escalation?.trigger).toBe("outside_hours");
350
+ const settled = await h.engine.execute(h.engine.approve(night.id, approver).id);
351
+ expect(settled.state).toBe("settled");
352
+ });
353
+ });
354
+
355
+ describe("reconcile never re-dispatches (review of PR #1)", () => {
356
+ async function stuckExecution(rail: import("../src/stubs/rail.js").StubRailOptions | undefined) {
357
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
358
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
359
+ if (!d.ok) throw new Error("refused");
360
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
361
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt], ...(rail ?? {}), inFlightThenSettled: [attempt] } });
362
+ const stuck = await flaky.engine.execute(d.execution.id);
363
+ expect(stuck.state).toBe("executing");
364
+ expect(stuck.reason).toBe("rail_uncertain");
365
+ return { h: flaky, stuck, attempt };
366
+ }
367
+
368
+ it("in flight, then settled: two reconciles, zero new payments", async () => {
369
+ const { h, stuck, attempt } = await stuckExecution(undefined);
370
+ const paysBefore = h.rail.payCount;
371
+ const first = await h.engine.reconcile(stuck.id);
372
+ expect(first.state).toBe("executing");
373
+ expect(first.detail).toContain("in_flight");
374
+ expect(h.rail.payCount).toBe(paysBefore);
375
+ expect(h.store.stubRailGet(attempt)).toBeUndefined();
376
+ const second = await h.engine.reconcile(stuck.id);
377
+ expect(second.state).toBe("settled");
378
+ expect(h.rail.payCount).toBe(paysBefore);
379
+ expect(h.store.listOutbox()[0]?.status).toBe("done");
380
+ });
381
+
382
+ it("unknown to the rail after the outbox was sent: stays executing for a human, nothing re-sent", async () => {
383
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
384
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
385
+ if (!d.ok) throw new Error("refused");
386
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
387
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
388
+ const stuck = await flaky.engine.execute(d.execution.id);
389
+ expect(stuck.state).toBe("executing");
390
+ const again = await flaky.engine.reconcile(stuck.id);
391
+ expect(again.state).toBe("executing");
392
+ expect(again.reason).toBe("rail_uncertain");
393
+ expect(flaky.rail.payCount).toBe(0);
394
+ expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
395
+ });
396
+
397
+ it("outbox still pending (crash before any call): the attempts go out once, under the same ids", async () => {
398
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("crash"); } } });
399
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
400
+ if (!d.ok) throw new Error("refused");
401
+ // Simulate a crash between the transaction and the first call: rewind the outbox to pending.
402
+ await expect(h.engine.execute(d.execution.id)).rejects.toThrow("crash");
403
+ const fresh = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
404
+ expect(fresh.store.getOutbox(d.execution.idempotency_key)?.status).toBe("sent");
405
+ const closed = await fresh.engine.reconcile(d.execution.id);
406
+ expect(closed.state).toBe("settled");
407
+ expect(fresh.rail.payCount).toBe(0);
408
+ });
409
+ });
410
+
411
+ describe("a multi-item execution closes only when every attempt has its outcome (review of PR #1)", () => {
412
+ it("one rail event settles one attempt; the second closes the execution and the outbox", async () => {
413
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
414
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }] });
415
+ if (!d.ok) throw new Error("refused");
416
+ const a0 = `att_${d.execution.idempotency_key.slice(4)}_0`;
417
+ const a1 = `att_${d.execution.idempotency_key.slice(4)}_1`;
418
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [a0, a1] } });
419
+ const stuck = await flaky.engine.execute(d.execution.id);
420
+ expect(stuck.state).toBe("executing");
421
+ expect(flaky.engine.ingestExternalEvent({ event_id: "e0", type: "commerce.payment.succeeded", attempt_id: a0 })).toMatchObject({ applied: true });
422
+ expect(flaky.store.getExecution(stuck.id)!.state).toBe("executing");
423
+ expect(flaky.store.getOutbox(stuck.idempotency_key)!.status).toBe("sent");
424
+ expect(flaky.engine.ingestExternalEvent({ event_id: "e0", type: "commerce.payment.succeeded", attempt_id: a0 })).toMatchObject({ applied: false, reason: "duplicate event id" });
425
+ expect(flaky.engine.ingestExternalEvent({ event_id: "e1", type: "commerce.payment.succeeded", attempt_id: a1 })).toEqual({ applied: true, reason: "settled" });
426
+ expect(flaky.store.getExecution(stuck.id)!.state).toBe("settled");
427
+ expect(flaky.store.getOutbox(stuck.idempotency_key)!.status).toBe("done");
428
+ });
429
+ });
430
+
431
+ describe("probes named in the second review: the mandate can change between approve and execute", () => {
432
+ it("(a) the mandate was tightened to a smaller per-payment cap: the last gate refuses", async () => {
433
+ const h = harness({ mode: "human" });
434
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 200000 }] });
435
+ if (!d.ok) throw new Error("refused");
436
+ const approved = h.engine.approve(d.execution.id, approver);
437
+ // A new process reads the tightened mandate (same version: an operator lowered the cap in place).
438
+ const tightened = harness({ mode: "human", dir: h.dir, mandate: { per_tx_cap_minor: 100000 } });
439
+ const out = await tightened.engine.execute(approved.id);
440
+ expect(out.state).toBe("denied");
441
+ expect(out.reason).toBe("per_tx_cap_exceeded");
442
+ expect(tightened.store.listOutbox()).toHaveLength(0);
443
+ });
444
+
445
+ it("(a') the mandate was re-signed (new version): the old artifact no longer matches, and the yes that follows is judged again", async () => {
446
+ const h = harness({ mode: "human" });
447
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 200000 }] });
448
+ if (!d.ok) throw new Error("refused");
449
+ const approved = h.engine.approve(d.execution.id, approver);
450
+ const resigned = harness({ mode: "human", dir: h.dir, mandate: { version: 2, per_tx_cap_minor: 100000 } });
451
+ const back = await resigned.engine.execute(approved.id);
452
+ expect(back.state).toBe("awaiting_approval");
453
+ expect(back.reason).toBe("mandate_changed");
454
+ expect(back.mandate).toEqual({ id: "mdt_test_0001", version: 2 });
455
+ const afterYes = resigned.engine.approve(back.id, approver);
456
+ expect(afterYes.state).toBe("denied");
457
+ expect(afterYes.reason).toBe("per_tx_cap_exceeded");
458
+ });
459
+
460
+ it("(c) the payee left the allowlist between approve and execute: denied, nothing sent", async () => {
461
+ const h = harness({ mode: "human" });
462
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
463
+ if (!d.ok) throw new Error("refused");
464
+ const approved = h.engine.approve(d.execution.id, approver);
465
+ const narrowed = harness({ mode: "human", dir: h.dir, mandate: { merchant_allowlist: [MERCADO], beneficiaries: [{ alias: "mercado", name: "Mercado do Bairro", payee: MERCADO }] } });
466
+ const out = await narrowed.engine.execute(approved.id);
467
+ expect(out.state).toBe("denied");
468
+ expect(out.reason).toBe("beneficiary_not_allowed");
469
+ expect(narrowed.store.listOutbox()).toHaveLength(0);
470
+ expect(narrowed.rail.payCount).toBe(0);
471
+ });
472
+ });
473
+
474
+ describe("reconcile is read-only; only resume dispatches, and only a pending outbox", () => {
475
+ it("outbox sent + lookup absent: reconcile never calls rail.pay and records execution.uncertain", async () => {
476
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
477
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
478
+ if (!d.ok) throw new Error("refused");
479
+ const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
480
+ const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
481
+ const stuck = await flaky.engine.execute(d.execution.id);
482
+ expect(flaky.store.getOutbox(stuck.idempotency_key)?.status).toBe("sent");
483
+ const pays = flaky.rail.payCount;
484
+ const still = await flaky.engine.reconcile(stuck.id);
485
+ expect(still.state).toBe("executing");
486
+ expect(flaky.rail.payCount).toBe(pays);
487
+ expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
488
+ expect(flaky.store.listEvents({ execution_id: stuck.id }).some((e) => e.type === "execution.uncertain")).toBe(true);
489
+ });
490
+
491
+ it("outbox pending: reconcile leaves it; resumePending dispatches once", async () => {
492
+ const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
493
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
494
+ if (!d.ok) throw new Error("refused");
495
+ // Rewind the row to what a crash between the transaction and the first call leaves behind.
496
+ const armed = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("crash"); } } });
497
+ await expect(armed.engine.execute(d.execution.id)).rejects.toThrow("crash");
498
+ armed.store.updateOutbox(d.execution.idempotency_key, "pending", undefined, "2026-09-23T18:00:00.000Z");
499
+ const fresh = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
500
+ const left = await fresh.engine.reconcile(d.execution.id);
501
+ expect(left.state).toBe("executing");
502
+ expect(fresh.rail.payCount).toBe(0);
503
+ const closed = await fresh.engine.resumePending(d.execution.id);
504
+ expect(closed.state).toBe("settled");
505
+ });
506
+ });
507
+
508
+ describe("multi-item partial failure is named item by item", () => {
509
+ it("first item settles with its receipt, second is refused: execution failed, outcomes and approval name both", async () => {
510
+ const h = harness({ mode: "human", rail: { refusePayees: [MERCADO] } });
511
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }] });
512
+ if (!d.ok) throw new Error("refused");
513
+ const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
514
+ expect(out.state).toBe("failed");
515
+ expect(out.outcomes).toEqual([
516
+ expect.objectContaining({ index: 0, status: "settled", receipt_id: expect.stringMatching(/^rcpt_stub_/) }),
517
+ expect.objectContaining({ index: 1, status: "failed", error: expect.stringContaining("psp_dispatch_failed") }),
518
+ ]);
519
+ expect(readdirSync(join(h.bundle.dir, "receipts"))).toHaveLength(1);
520
+ expect(h.bundle.readApprovals()[0]?.items.map((i) => i.payee)).toEqual([ESCOLA, MERCADO]);
521
+ expect(h.store.getOutbox(out.idempotency_key)?.status).toBe("failed");
522
+ expect(h.store.getOutbox(out.idempotency_key)?.response).toEqual(out.outcomes);
523
+ });
524
+
525
+ it("a refusal in the MIDDLE does not stop the items after it: every attempt is sent and every one is named", async () => {
526
+ const h = harness({ mode: "human", rail: { refusePayees: [MERCADO] } });
527
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
528
+ if (!d.ok) throw new Error("refused");
529
+ const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
530
+ // Section 4.1: the execution closes `failed` because one attempt failed,
531
+ // and `outcomes` names EVERY item — including the one after the refusal.
532
+ expect(out.state).toBe("failed");
533
+ expect(out.outcomes.map((o) => o.status)).toEqual(["settled", "failed", "settled"]);
534
+ expect(h.rail.payCount).toBe(3); // all three reached the rail; the stub records a refusal too
535
+ expect(readdirSync(join(h.bundle.dir, "receipts"))).toHaveLength(2);
536
+ });
537
+
538
+ it("an unknown answer in the MIDDLE still sends the siblings, and the execution stays open until it is resolved", async () => {
539
+ const h = harness({ mode: "human", rail: { uncertainPayees: [MERCADO] } });
540
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
541
+ if (!d.ok) throw new Error("refused");
542
+ const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
543
+ // Open, not closed: one attempt has no outcome at all, and §4.1 says an
544
+ // execution closes only when every attempt has one.
545
+ expect(out.state).toBe("executing");
546
+ expect(out.reason).toBe("rail_uncertain");
547
+ expect(out.outcomes.map((o) => o.index)).toEqual([0, 2]);
548
+ expect(out.detail).toContain("psp_dispatch_uncertain");
549
+ // Reconciliation resolves the unknown one and only then does it close.
550
+ // The stub answers `in_flight` on the first look and finds the provider
551
+ // finished on the second, which is the shape a real timeout has.
552
+ expect((await h.engine.reconcile(d.execution.id)).state).toBe("executing");
553
+ const closed = await h.engine.reconcile(d.execution.id);
554
+ expect(closed.state).toBe("settled");
555
+ expect(closed.outcomes.map((o) => o.status)).toEqual(["settled", "settled", "settled"]);
556
+ });
557
+
558
+ it("an execution with a failed attempt AND an unknown one does not close as failed", async () => {
559
+ const h = harness({ mode: "human", rail: { refusePayees: [ESCOLA], uncertainPayees: [MERCADO] } });
560
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
561
+ if (!d.ok) throw new Error("refused");
562
+ const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
563
+ // "One of them failed and one of them we cannot see" is not a closed
564
+ // outcome: reporting `failed` here would call a batch finished while a
565
+ // payment may still be in flight.
566
+ expect(out.state).toBe("executing");
567
+ expect(out.outcomes.map((o) => o.status)).toEqual(["failed", "settled"]);
568
+ expect((await h.engine.reconcile(d.execution.id)).state).toBe("executing");
569
+ const closed = await h.engine.reconcile(d.execution.id);
570
+ expect(closed.state).toBe("failed");
571
+ expect(closed.outcomes.map((o) => o.status)).toEqual(["failed", "settled", "settled"]);
572
+ });
573
+
574
+ it("why the siblings are sent and not deferred: an attempt that was never sent is recovered by nothing", async () => {
575
+ // This is the measurement the choice rests on. `reconcile` is read-only by
576
+ // contract and never dispatches; `resumePending` dispatches only while the
577
+ // outbox row is still `pending`, and it flips to `sent` before the first
578
+ // rail call. So an attempt skipped at dispatch is not sent later — it is
579
+ // never sent, by any path the runtime has.
580
+ const h = harness({ mode: "human", rail: { uncertainPayees: [MERCADO] } });
581
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
582
+ if (!d.ok) throw new Error("refused");
583
+ const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
584
+ const third = `att_${out.idempotency_key.slice(4)}_2`;
585
+ // Under the fix the third attempt WAS sent, so the rail knows it.
586
+ expect(h.store.stubRailGet(third)).toBeDefined();
587
+ expect(h.store.getOutbox(out.idempotency_key)?.status).toBe("sent");
588
+ // And that is the only reason it is recoverable: `resumePending` on a
589
+ // `sent` row reconciles instead of dispatching, and never pays again.
590
+ const before = h.rail.payCount;
591
+ expect((await h.engine.resumePending(d.execution.id)).state).toBe("executing");
592
+ const resumed = await h.engine.resumePending(d.execution.id);
593
+ expect(h.rail.payCount).toBe(before); // reconciled, never re-sent
594
+ expect(resumed.state).toBe("settled");
595
+ });
596
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { evaluateEscalation, isOutsideHours, localClock } from "../src/escalate.js";
3
+ import { GuardrailsSchema } from "../src/guardrails.js";
4
+ import type { ExecutionItem } from "../src/types.js";
5
+
6
+ const rule = { amount: 150000, new_beneficiary: true, outside_hours: "22:00-07:00" };
7
+ const guardrails = GuardrailsSchema.parse({ approval: "mandate", escalate_above: rule, velocity: { window_hours: 24 } });
8
+ const item = (payee: string, amount: number): ExecutionItem => ({ beneficiary: payee, payee, amount, currency: "BRL" });
9
+ // 15:00 in Sao Paulo (UTC-3) is 18:00Z.
10
+ const afternoon = new Date("2026-09-23T18:00:00Z");
11
+ const ctx = (over: Partial<Parameters<typeof evaluateEscalation>[3]> = {}) => ({ now: afternoon, timezone: "America/Sao_Paulo", knownPayees: new Set(["escola@x"]), recentByPayee: new Map(), ...over });
12
+
13
+ describe("section 4.4: escalate_above only tightens", () => {
14
+ it("amount above the threshold escalates, below it does not", () => {
15
+ expect(evaluateEscalation(rule, guardrails, [item("escola@x", 150001)], ctx())?.trigger).toBe("amount");
16
+ expect(evaluateEscalation(rule, guardrails, [item("escola@x", 150000)], ctx())).toBeUndefined();
17
+ });
18
+
19
+ it("fractioning: parts below the threshold add up inside the velocity window", () => {
20
+ const recent = new Map([["escola@x", 120000]]);
21
+ const out = evaluateEscalation(rule, guardrails, [item("escola@x", 40000)], ctx({ recentByPayee: recent }));
22
+ expect(out?.trigger).toBe("amount");
23
+ expect(out?.detail).toContain("add up");
24
+ });
25
+
26
+ it("first payment to a payee escalates; the second does not", () => {
27
+ expect(evaluateEscalation(rule, guardrails, [item("mercado@x", 1000)], ctx())?.trigger).toBe("new_beneficiary");
28
+ expect(evaluateEscalation(rule, guardrails, [item("escola@x", 1000)], ctx())).toBeUndefined();
29
+ });
30
+
31
+ it("outside hours escalates, and the window crosses midnight", () => {
32
+ const night = new Date("2026-09-24T02:00:00Z"); // 23:00 in Sao Paulo
33
+ expect(localClock(night, "America/Sao_Paulo")).toBe("23:00");
34
+ expect(isOutsideHours("22:00-07:00", night, "America/Sao_Paulo")).toBe(true);
35
+ expect(isOutsideHours("22:00-07:00", afternoon, "America/Sao_Paulo")).toBe(false);
36
+ expect(evaluateEscalation(rule, guardrails, [item("escola@x", 1000)], ctx({ now: night }))?.trigger).toBe("outside_hours");
37
+ });
38
+
39
+ it("no rule, no escalation", () => {
40
+ expect(evaluateEscalation(undefined, guardrails, [item("x", 10_000_000)], ctx())).toBeUndefined();
41
+ });
42
+ });
@@ -0,0 +1 @@
1
+ {"issuer":"did:web:id.codespar.dev","key_namespace":"production","algorithm":"Ed25519","signing_string":"codespar-receipt:v1:<receipt_id>:<chain>","chain_recipe":{"canonicalization":"RFC 8785 (JCS)","digest":"SHA-256 over the UTF-8 bytes of the canonical JSON, lowercase hex","input":"{ \"v\": <chain_version>, \"links\": [ <each link below that applies, in this order> ] }","version":{"field":"chain_version","rule":"4 when `approval` is present; else 3 when `payment.amount_authorized` is present; else 2 when `payment.amount_atomic` is not null; else 1"},"optional_marker":"?","links":[{"link":"mandate","from":"mandate","when":"always","fields_by_version":{"1":["id","nonce","scope","currency","sig"],"2":["id","nonce","scope","currency","sig"],"3":["id","nonce","scope","currency","sig"],"4":["id","nonce","scope","currency","sig_sha256"]}},{"link":"quote","from":"quote","when":"quote is not null","fields":["seller","resource","price_minor","payee","session_id","sig","at"]},{"link":"approval","from":"approval","when":"chain_version is 4","fields":["items_hash","batch_hash"]},{"link":"payment","from":"payment","when":"always","fields":["rail","provider","tx_id","amount_minor","amount_atomic?","amount_authorized?","amount_charged?","amount_refunded?","metering?","sandbox?","attempt_id","money_moved","at"]},{"link":"delivery","from":"delivery","when":"delivery is not null","fields":["result","proof","kind","nfe_chave","at"]}],"notes":["`mandate.sig_sha256` is SHA-256 over the UTF-8 bytes of `mandate.sig`, lowercase hex. A v4 chain seals it in place of `sig`, which must not be shared; v1-v3 seal `sig` itself, so only the tenant, whose read carries `sig`, can recompute those. Anyone can still verify their signature over `chain`.","Recomputing needs `quote.payee` unmasked. Whoever shares a receipt decides whether to hand it over.","Timestamps are RFC 3339 UTC with milliseconds, exactly as read."]},"keys":[{"kid":"did:web:id.codespar.dev#1","kty":"OKP","crv":"Ed25519","x":"fz1svKf1AFlnIEZSseNqH5zCXkKqPmtkjx9mMNBz8pU","alg":"EdDSA","use":"sig","status":"retired","created_at":"2026-07-05T13:29:23.276Z","retired_at":"2026-09-25T02:43:45.971Z"},{"kid":"did:web:id.codespar.dev#production-2","kty":"OKP","crv":"Ed25519","x":"WKVTphnIma44d5T7bugYnLvI6h79LhaSdNDrkOFxmRs","alg":"EdDSA","use":"sig","status":"active","created_at":"2026-09-25T02:43:45.971Z","retired_at":null}]}