@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
@@ -8,8 +8,8 @@ import { mkdtempSync } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { describe, expect, it } from "vitest";
11
- import { fixedClock, type Execution, type ToolContext } from "@codespar/agent-core";
12
- import { assembleTimeline, setup, type Setup } from "@codespar/agent-runtime";
11
+ import { StateStore, batchAttemptId, batchHash, fixedClock, maskPayee, type Execution, type StubRail, type StubRailOptions, type ToolContext } from "@codespar/agent-core";
12
+ import { assembleTimeline, handleExecution, parseBatchGesture, presentBatch, setup, type BatchGestures, type Setup } from "@codespar/agent-runtime";
13
13
  import { agent } from "../src/kit.js";
14
14
  import { runBatch } from "../src/modules/batch-payout.js";
15
15
  import { findBatch, type Batch } from "../src/payables.js";
@@ -27,15 +27,20 @@ function approveAndRun(s: Setup): ToolContext["onExecution"] {
27
27
  };
28
28
  }
29
29
 
30
- function open(stateDir: string, options: { refusePayees?: string[] } = {}): Setup {
30
+ function open(stateDir: string, options: { refusePayees?: string[]; uncertainPayees?: string[]; ledger?: StateStore; now?: string } = {}): Setup {
31
+ const stubRail: StubRailOptions = {
32
+ ...(options.refusePayees ? { refusePayees: options.refusePayees } : {}),
33
+ ...(options.uncertainPayees ? { uncertainPayees: options.uncertainPayees } : {}),
34
+ ...(options.ledger ? { ledger: options.ledger } : {}),
35
+ };
31
36
  return setup(agent, {
32
37
  mode: "human",
33
38
  rail: "stub",
34
39
  provider: "replay",
35
40
  runsDir,
36
41
  stateDir,
37
- now: fixedClock("2026-09-23T14:00:00-03:00"),
38
- ...(options.refusePayees ? { stubRail: { refusePayees: options.refusePayees } } : {}),
42
+ now: fixedClock(options.now ?? "2026-09-23T14:00:00-03:00"),
43
+ ...(Object.keys(stubRail).length > 0 ? { stubRail } : {}),
39
44
  say: () => undefined,
40
45
  });
41
46
  }
@@ -84,6 +89,23 @@ describe("batch-payout: a batch is a loop of executions", () => {
84
89
  }
85
90
  });
86
91
 
92
+ it("every line presents the quote of its OWN approved line, and its receipt carries the payee that quote sealed (OPEN_QUESTIONS §18)", async () => {
93
+ const s = open(mkdtempSync(join(tmpdir(), "supplier-batch-q-")));
94
+ try {
95
+ await run(s, "folha-2026-10");
96
+ const artifacts = s.bundle.readApprovals();
97
+ for (const e of s.engine.list()) {
98
+ const line = artifacts.find((a) => a.execution_id === e.id)!.items[0]!;
99
+ const outcome = e.outcomes[0]!;
100
+ const sent = s.store.stubRailGet(outcome.attempt_id)!.request as { quote?: { price_minor: number; payee: string; seller: string } };
101
+ expect(sent.quote).toMatchObject({ seller: line.beneficiary, price_minor: line.amount, payee: line.payee });
102
+ expect(s.bundle.readReceipt(`${outcome.receipt_id}.json`)?.["payment"]).toMatchObject({ payee: maskPayee(line.payee) });
103
+ }
104
+ } finally {
105
+ s.close();
106
+ }
107
+ });
108
+
87
109
  it("one refusal does not stop the others: the lines after the refused one still dispatch", async () => {
88
110
  const s = open(mkdtempSync(join(tmpdir(), "supplier-batch-b-")), { refusePayees: ["contas@insumos-atlantico.example.com.br"] });
89
111
  try {
@@ -382,3 +404,480 @@ describe("batch-payout: the approved list is bound as a set", () => {
382
404
  }
383
405
  });
384
406
  });
407
+
408
+ /**
409
+ * The interactive terminal's two hooks, driven by scripted answers: one
410
+ * question for the list, then every line decided by what was answered.
411
+ */
412
+ function gestureChannel(s: Setup, answers: string[]): { ctx: ToolContext; asked: string[] } {
413
+ const gestures: BatchGestures = new Map();
414
+ const asked: string[] = [];
415
+ const ask = async (question: string) => {
416
+ asked.push(question);
417
+ const answer = answers.shift();
418
+ if (answer === undefined) throw new Error(`asked more than scripted: ${question}`);
419
+ return answer;
420
+ };
421
+ const options = { setup: s, approver: APPROVER, ask, say: () => undefined, gestures };
422
+ return {
423
+ asked,
424
+ ctx: {
425
+ engine: s.engine,
426
+ onExecution: (execution) => handleExecution(execution, options),
427
+ onBatch: (batch) => presentBatch(batch, options),
428
+ },
429
+ };
430
+ }
431
+
432
+ describe("batch-payout: one gesture approves the list, with a veto per line (§39e)", () => {
433
+ it("reads todas, todas exceto and nenhuma, and refuses to guess at anything else", () => {
434
+ expect(parseBatchGesture("todas", 4)).toEqual({ vetoed: [] });
435
+ expect(parseBatchGesture("all", 4)).toEqual({ vetoed: [] });
436
+ expect(parseBatchGesture("todas exceto 3", 4)).toEqual({ vetoed: [2] });
437
+ expect(parseBatchGesture("all except 4, 1,4", 4)).toEqual({ vetoed: [0, 3] });
438
+ expect(parseBatchGesture("nenhuma", 4)).toEqual({ vetoed: [0, 1, 2, 3] });
439
+ expect(parseBatchGesture("", 4)).toEqual({ vetoed: [0, 1, 2, 3] });
440
+ // A line that does not exist is a typo, and a typo must not veto nothing.
441
+ expect(parseBatchGesture("todas exceto 5", 4)).toBeUndefined();
442
+ expect(parseBatchGesture("todas exceto 0", 4)).toBeUndefined();
443
+ expect(parseBatchGesture("s", 4)).toBeUndefined();
444
+ expect(parseBatchGesture("todas menos 2", 4)).toBeUndefined();
445
+ });
446
+
447
+ it("approve-all is one question, and still mints one artifact per line, each naming the list", async () => {
448
+ const s = open(mkdtempSync(join(tmpdir(), "supplier-gesture-a-")));
449
+ try {
450
+ const batch = fourLines();
451
+ const channel = gestureChannel(s, ["todas"]);
452
+ const report = await runBatch(batch, channel.ctx);
453
+
454
+ expect(channel.asked).toHaveLength(1);
455
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled", "settled"]);
456
+ expect(report.gesture).toEqual({ batch_hash: report.batch_hash, approved: [0, 1, 2, 3], vetoed: [] });
457
+
458
+ const artifacts = s.bundle.readApprovals();
459
+ expect(artifacts).toHaveLength(4);
460
+ expect(new Set(artifacts.map((a) => a.batch!.batch_hash))).toEqual(new Set([report.batch_hash]));
461
+ expect(artifacts.map((a) => a.batch!.index)).toEqual([0, 1, 2, 3]);
462
+ for (const a of artifacts) expect(a.approver).toEqual({ type: "person", id: APPROVER.id, channel: APPROVER.channel });
463
+
464
+ // The gesture itself is in the attested record, next to the artifacts it produced.
465
+ const gestures = s.bundle.readEvents().filter((e) => e["type"] === "batch.gesture");
466
+ expect(gestures).toHaveLength(1);
467
+ expect(gestures[0]!["payload"]).toMatchObject({ batch_ref: batch.ref, batch_hash: report.batch_hash, count: 4, approved: [0, 1, 2, 3], vetoed: [], approver: { type: "human", id: APPROVER.id } });
468
+ } finally {
469
+ s.close();
470
+ }
471
+ });
472
+
473
+ it("approve-except denies exactly the vetoed lines, and the report says which", async () => {
474
+ const s = open(mkdtempSync(join(tmpdir(), "supplier-gesture-b-")));
475
+ try {
476
+ const batch = fourLines();
477
+ const channel = gestureChannel(s, ["todas exceto 2,4"]);
478
+ const report = await runBatch(batch, channel.ctx);
479
+
480
+ expect(channel.asked).toHaveLength(1);
481
+ expect(report.lines.map((l) => [l.index, l.dispatch, l.state])).toEqual([
482
+ [0, "settled", "settled"],
483
+ [1, "refused", "denied"],
484
+ [2, "settled", "settled"],
485
+ [3, "refused", "denied"],
486
+ ]);
487
+ const vetoed = [batch.lines[1]!.alias, batch.lines[3]!.alias];
488
+ expect(report.denied).toEqual(vetoed);
489
+ expect(report.failed).toEqual(vetoed);
490
+ expect(report.gesture).toEqual({ batch_hash: report.batch_hash, approved: [0, 2], vetoed: [1, 3] });
491
+ expect(report.settled_minor).toBe(batch.lines[0]!.amount_minor + batch.lines[2]!.amount_minor);
492
+
493
+ // A vetoed line is a decision: it has an execution, no artifact, and says why.
494
+ const artifacts = s.bundle.readApprovals();
495
+ expect(artifacts.map((a) => a.batch!.index)).toEqual([0, 2]);
496
+ const denied = s.engine.list().filter((e) => e.state === "denied");
497
+ expect(denied.map((e) => e.batch!.index)).toEqual([1, 3]);
498
+ expect(denied.map((e) => e.detail)).toEqual(["vetoed in the batch gesture (line 2 of 4)", "vetoed in the batch gesture (line 4 of 4)"]);
499
+ } finally {
500
+ s.close();
501
+ }
502
+ });
503
+
504
+ it("a line that belongs to another list under the same ref is not approved by the gesture", async () => {
505
+ const s = open(mkdtempSync(join(tmpdir(), "supplier-gesture-c-")));
506
+ try {
507
+ const batch = fourLines();
508
+ const gestures: BatchGestures = new Map();
509
+ const options = { setup: s, approver: APPROVER, ask: async () => "todas", say: () => undefined, gestures };
510
+ // The person approved the four-line list...
511
+ const shown = await runBatch(batch, { engine: s.engine, onExecution: async (e) => e, onBatch: (b) => presentBatch(b, options) });
512
+ expect(shown.gesture?.vetoed).toEqual([]);
513
+ // ...and a line arrives claiming the same ref but a list of three with another hash.
514
+ const edited = await s.engine.draft({
515
+ items: [{ payee: batch.lines[0]!.alias, amount: batch.lines[0]!.amount_minor }],
516
+ batch: { ref: batch.ref, batch_hash: "sha256:" + "0".repeat(64), index: 0, count: 3 },
517
+ });
518
+ if (!edited.ok) throw new Error(edited.reason);
519
+ const decided = await handleExecution(edited.execution, options);
520
+ expect(decided.state).toBe("denied");
521
+ expect(decided.detail).toContain("the list changed after the gesture");
522
+ expect(s.bundle.readApprovals().filter((a) => a.execution_id === edited.execution.id)).toEqual([]);
523
+ } finally {
524
+ s.close();
525
+ }
526
+ });
527
+
528
+ it("an edited list after the gesture is refused before anything is drafted, and nobody is asked again", async () => {
529
+ const stateDir = mkdtempSync(join(tmpdir(), "supplier-gesture-d-"));
530
+ const four = fourLines();
531
+ const first = open(stateDir);
532
+ try {
533
+ const report = await runBatch(four, gestureChannel(first, ["todas exceto 4"]).ctx);
534
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled", "refused"]);
535
+ } finally {
536
+ first.close();
537
+ }
538
+
539
+ // The fourth line's amount moves after the person approved the list.
540
+ const edited: Batch = { ...four, lines: four.lines.map((l, i) => (i === 3 ? { ...l, amount_minor: l.amount_minor + 100 } : l)) };
541
+ const second = open(stateDir);
542
+ try {
543
+ const channel = gestureChannel(second, []);
544
+ const report = await runBatch(edited, channel.ctx);
545
+ expect(channel.asked).toEqual([]);
546
+ expect(report.refused?.reason).toBe("batch_set_changed");
547
+ expect(report.gesture).toBeUndefined();
548
+ expect(second.engine.list()).toHaveLength(4);
549
+ } finally {
550
+ second.close();
551
+ }
552
+ });
553
+
554
+ it("without the hook nothing changes: each line is its own question, as the one-shot and the scenarios expect", async () => {
555
+ const s = open(mkdtempSync(join(tmpdir(), "supplier-gesture-e-")));
556
+ try {
557
+ const batch = fourLines();
558
+ const asked: string[] = [];
559
+ const options = { setup: s, approver: APPROVER, ask: async (q: string) => (asked.push(q), "s"), say: () => undefined };
560
+ const report = await runBatch(batch, { engine: s.engine, onExecution: (e) => handleExecution(e, options) });
561
+ expect(asked).toHaveLength(4);
562
+ expect(report.gesture).toBeUndefined();
563
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled", "settled"]);
564
+ } finally {
565
+ s.close();
566
+ }
567
+ });
568
+ });
569
+
570
+ /**
571
+ * §39c: the claim in `.codespar/state.db` is the first line of defence, and it
572
+ * is local. These run the same batch from several state files against ONE
573
+ * rail ledger that answers a repeated `attempt_id` the way the deployed API
574
+ * does (ent#1671, #1683): replay when settled, `psp_attempt_in_flight` while
575
+ * running, `psp_attempt_conflict` when it failed, `attempt_id_conflict` when
576
+ * the id arrives with a different payment. Another machine has no claim at
577
+ * all, so the only thing between it and a second payment is that it presents
578
+ * the same attempt, with the same quote.
579
+ */
580
+ describe("batch-payout: a batch is idempotent across machines (§39c)", () => {
581
+ const INSUMOS = "contas@insumos-atlantico.example.com.br";
582
+
583
+ function railOf(s: Setup): StubRail {
584
+ return s.rail as StubRail;
585
+ }
586
+
587
+ function attemptsOf(s: Setup): string[] {
588
+ return s.engine.list().flatMap((e) => e.outcomes.map((o) => o.attempt_id));
589
+ }
590
+
591
+ function receiptsOf(s: Setup): Array<string | undefined> {
592
+ return s.engine.list().flatMap((e) => e.outcomes.map((o) => o.receipt_id));
593
+ }
594
+
595
+ function payloadsOf(s: Setup, type: string): Array<Record<string, unknown>> {
596
+ return s.store
597
+ .listEvents()
598
+ .filter((e) => e.type === type)
599
+ .map((e) => e.payload as Record<string, unknown>);
600
+ }
601
+
602
+ function newLedger(): StateStore {
603
+ return new StateStore(join(mkdtempSync(join(tmpdir(), "supplier-rail-ledger-")), "rail.db"));
604
+ }
605
+
606
+ function machine(prefix: string, options: Parameters<typeof open>[1]): Setup {
607
+ return open(mkdtempSync(join(tmpdir(), `supplier-machine-${prefix}-`)), options);
608
+ }
609
+
610
+ it("derives the attempt id from mandate, list, position and generation, and from nothing a run invents", () => {
611
+ const hash = "sha256:" + "1".repeat(64);
612
+ const id = batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 0);
613
+ expect(id).toMatch(/^ska_[0-9a-f]{64}$/);
614
+ expect(id.length).toBeLessThanOrEqual(128);
615
+ expect(batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 0)).toBe(id);
616
+ expect(batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 0, 0)).toBe(id);
617
+ const others = [
618
+ batchAttemptId("mdt_b", { batch_hash: hash, index: 2 }, 0),
619
+ batchAttemptId("mdt_a", { batch_hash: "sha256:" + "2".repeat(64), index: 2 }, 0),
620
+ batchAttemptId("mdt_a", { batch_hash: hash, index: 3 }, 0),
621
+ batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 1),
622
+ batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 0, 1),
623
+ batchAttemptId("mdt_a", { batch_hash: hash, index: 2 }, 0, 2),
624
+ ];
625
+ expect(new Set([id, ...others]).size).toBe(others.length + 1);
626
+ });
627
+
628
+ it("the same batch approved on a second machine, at another time, lands on the same attempts and pays each line once", async () => {
629
+ const ledger = newLedger();
630
+ const first = machine("a", { ledger });
631
+ let firstAttempts: string[];
632
+ let firstReceipts: Array<string | undefined>;
633
+ try {
634
+ const report = await run(first, "folha-2026-10");
635
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled"]);
636
+ expect(railOf(first).payCount).toBe(3);
637
+ firstAttempts = attemptsOf(first);
638
+ firstReceipts = receiptsOf(first);
639
+ } finally {
640
+ first.close();
641
+ }
642
+
643
+ // Another machine: no state.db, so no claim, a new run id, new execution
644
+ // ids, and a person approving an hour later. The approval time is not in
645
+ // the quote a batch line presents, so the payment is the same payment.
646
+ const second = machine("b", { ledger, now: "2026-09-23T15:00:00-03:00" });
647
+ try {
648
+ const report = await run(second, "folha-2026-10");
649
+ expect(railOf(second).payCount).toBe(0);
650
+ expect(attemptsOf(second)).toEqual(firstAttempts);
651
+ expect(receiptsOf(second)).toEqual(firstReceipts);
652
+ // Recorded as the line's outcome — settled, with the first run's receipt — and never as a failure that invites a retry.
653
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled"]);
654
+ expect(payloadsOf(second, "rail.outcome").map((p) => [p["status"], p["idempotent_replay"]])).toEqual([
655
+ ["settled", true],
656
+ ["settled", true],
657
+ ["settled", true],
658
+ ]);
659
+ expect(second.engine.list().flatMap((e) => e.outcomes.map((o) => o.replayed))).toEqual([true, true, true]);
660
+ } finally {
661
+ second.close();
662
+ ledger.close();
663
+ }
664
+ });
665
+
666
+ it("a list that differs in one line is a different list, and none of its attempt ids is reused", async () => {
667
+ const ledger = newLedger();
668
+ const batch = findBatch("folha-2026-10")!;
669
+ const changed: Batch = { ...batch, lines: batch.lines.map((l, i) => (i === 2 ? { ...l, amount_minor: l.amount_minor + 100 } : l)) };
670
+ const first = machine("c", { ledger });
671
+ const second = machine("d", { ledger });
672
+ try {
673
+ const a = await runBatch(batch, { engine: first.engine, onExecution: approveAndRun(first) });
674
+ const b = await runBatch(changed, { engine: second.engine, onExecution: approveAndRun(second) });
675
+ expect(b.batch_hash).not.toBe(a.batch_hash);
676
+ const shared = attemptsOf(first).filter((id) => attemptsOf(second).includes(id));
677
+ expect(shared).toEqual([]);
678
+ // Unchanged lines of a changed list are new attempts: the id names a line of THIS list.
679
+ expect(railOf(second).payCount).toBe(3);
680
+ } finally {
681
+ first.close();
682
+ second.close();
683
+ ledger.close();
684
+ }
685
+ });
686
+
687
+ it("a line the provider refused is paid once by the next machine, under the next generation, and a third machine replays it", async () => {
688
+ const ledger = newLedger();
689
+ const first = machine("e", { ledger, refusePayees: [INSUMOS] });
690
+ let refusedAttempt: string;
691
+ try {
692
+ const report = await run(first, "fornecedores-2026-10");
693
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "refused", "settled"]);
694
+ refusedAttempt = first.engine.list()[1]!.outcomes[0]!.attempt_id;
695
+ } finally {
696
+ first.close();
697
+ }
698
+
699
+ const second = machine("f", { ledger });
700
+ let paidAttempt: string;
701
+ try {
702
+ const report = await run(second, "fornecedores-2026-10");
703
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled"]);
704
+ // One new payment: the refused line. The other two replayed.
705
+ expect(railOf(second).payCount).toBe(1);
706
+ const line = second.engine.list()[1]!;
707
+ paidAttempt = line.outcomes[0]!.attempt_id;
708
+ expect(paidAttempt).toBe(batchAttemptId(line.mandate.id, line.batch!, 0, 1));
709
+ expect(paidAttempt).not.toBe(refusedAttempt);
710
+ expect(line.attempt_generations).toEqual({ 0: 1 });
711
+ expect(payloadsOf(second, "rail.attempt_spent").map((p) => p["attempt_id"])).toEqual([refusedAttempt]);
712
+ } finally {
713
+ second.close();
714
+ }
715
+
716
+ const third = machine("g", { ledger });
717
+ try {
718
+ const report = await run(third, "fornecedores-2026-10");
719
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "settled", "settled"]);
720
+ expect(railOf(third).payCount).toBe(0);
721
+ expect(third.engine.list()[1]!.outcomes[0]!.attempt_id).toBe(paidAttempt);
722
+ } finally {
723
+ third.close();
724
+ ledger.close();
725
+ }
726
+ });
727
+
728
+ it("a reconcile looks up the generation that was actually sent, not the spent one before it", async () => {
729
+ const ledger = newLedger();
730
+ const first = machine("j", { ledger, refusePayees: [INSUMOS] });
731
+ try {
732
+ await run(first, "fornecedores-2026-10");
733
+ } finally {
734
+ first.close();
735
+ }
736
+
737
+ // Generation 0 is spent; generation 1 goes out and its answer is lost.
738
+ const second = machine("k", { ledger, uncertainPayees: [INSUMOS] });
739
+ try {
740
+ const report = await run(second, "fornecedores-2026-10");
741
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "uncertain", "settled"]);
742
+ const line = second.engine.list()[1]!;
743
+ expect(line.attempt_generations).toEqual({ 0: 1 });
744
+ const sent = batchAttemptId(line.mandate.id, line.batch!, 0, 1);
745
+ await second.engine.reconcile(line.id);
746
+ const closed = await second.engine.reconcile(line.id);
747
+ expect(closed.state).toBe("settled");
748
+ expect(closed.outcomes[0]!.attempt_id).toBe(sent);
749
+ expect(payloadsOf(second, "rail.reconcile").map((p) => p["attempt_id"])).toEqual([sent, sent]);
750
+ expect(railOf(second).payCount).toBe(0);
751
+ } finally {
752
+ second.close();
753
+ ledger.close();
754
+ }
755
+ });
756
+
757
+ it("A pays at generation 2 after generation 1 failed; B, still at generation 1, is told it is spent, advances, and gets A's payment as a replay", async () => {
758
+ const ledger = newLedger();
759
+ // A: the provider refuses generation 0.
760
+ const a = mkdtempSync(join(tmpdir(), "supplier-machine-l-"));
761
+ let s = open(a, { ledger, refusePayees: [INSUMOS] });
762
+ try {
763
+ await run(s, "fornecedores-2026-10");
764
+ } finally {
765
+ s.close();
766
+ }
767
+ // B: generation 0 is spent, so it presents generation 1, which the provider also refuses. B is left at generation 1.
768
+ const b = mkdtempSync(join(tmpdir(), "supplier-machine-m-"));
769
+ s = open(b, { ledger, refusePayees: [INSUMOS] });
770
+ let g1: string;
771
+ try {
772
+ await run(s, "fornecedores-2026-10");
773
+ const line = s.engine.list()[1]!;
774
+ expect(line.state).toBe("failed");
775
+ expect(line.attempt_generations).toEqual({ 0: 1 });
776
+ g1 = line.outcomes[0]!.attempt_id;
777
+ } finally {
778
+ s.close();
779
+ }
780
+ // A again, the provider now accepting: 0 and 1 are spent, generation 2 is paid.
781
+ s = open(a, { ledger });
782
+ let g2: string;
783
+ let receipt: string | undefined;
784
+ try {
785
+ const report = await run(s, "fornecedores-2026-10");
786
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["already_settled", "settled", "already_settled"]);
787
+ expect(railOf(s).payCount).toBe(1);
788
+ const line = s.engine.list().find((e) => e.batch?.index === 1 && e.state === "settled")!;
789
+ g2 = line.outcomes[0]!.attempt_id;
790
+ receipt = line.outcomes[0]!.receipt_id;
791
+ expect(g2).toBe(batchAttemptId(line.mandate.id, line.batch!, 0, 2));
792
+ } finally {
793
+ s.close();
794
+ }
795
+ // B again: its claim says the line failed, so it drafts it anew. The new
796
+ // execution walks the same derivation from 0: spent, spent (its own g1),
797
+ // then A's generation 2, which the rail answers from its record.
798
+ s = open(b, { ledger });
799
+ try {
800
+ const rail = railOf(s);
801
+ const answers: Array<{ attempt_id: string; status: string; replay: unknown }> = [];
802
+ const pay = rail.pay.bind(rail);
803
+ rail.pay = async (payment) => {
804
+ const outcome = await pay(payment);
805
+ answers.push({ attempt_id: payment.attempt_id, status: outcome.status, replay: outcome.status === "settled" ? outcome.replayed : undefined });
806
+ return outcome;
807
+ };
808
+ const report = await run(s, "fornecedores-2026-10");
809
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["already_settled", "settled", "already_settled"]);
810
+ expect(answers.map((x) => x.status)).toEqual(["failed", "failed", "settled"]);
811
+ expect(answers[1]!.attempt_id).toBe(g1);
812
+ expect(answers[2]).toEqual({ attempt_id: g2, status: "settled", replay: true });
813
+ expect(railOf(s).payCount).toBe(0);
814
+ const replayed = s.engine.list().find((e) => e.batch?.index === 1 && e.state === "settled")!.outcomes[0]!;
815
+ expect(replayed).toMatchObject({ receipt_id: receipt, replayed: true });
816
+ expect(payloadsOf(s, "rail.outcome").filter((p) => p["attempt_id"] === g2)).toEqual([expect.objectContaining({ status: "settled", idempotent_replay: true })]);
817
+ } finally {
818
+ s.close();
819
+ ledger.close();
820
+ }
821
+ });
822
+
823
+ it("a line whose id is held for another payment is reported attempt_id_conflict, never moved to another id, and never re-drafted", async () => {
824
+ const ledger = newLedger();
825
+ // The books hold the line's generation-0 id for a DIFFERENT payment, so the rail answers attempt_id_conflict.
826
+ const s = machine("n", { ledger });
827
+ const batch = findBatch("fornecedores-2026-10")!;
828
+ const presented = batchHash(s.engine.preview(batch.lines.map((l) => ({ payee: l.alias, amount: l.amount_minor, description: `${batch.label}: ${l.reference}`, due_date: batch.due }))));
829
+ const g0 = batchAttemptId(s.engine.mandate.id, { batch_hash: presented, index: 1 }, 0);
830
+ ledger.stubRailPut(g0, { attempt_id: g0, mandate_id: s.engine.mandate.id, amount_minor: 1, currency: "BRL", payee: INSUMOS }, { status: "settled", transaction_id: "tx_other", receipt_id: null, money_moved: false, sandbox: true, raw: {} }, "2026-09-23T17:00:00.000Z");
831
+ try {
832
+ const first = await run(s, "fornecedores-2026-10");
833
+ expect(first.lines.map((l) => l.dispatch)).toEqual(["settled", "attempt_id_conflict", "settled"]);
834
+ expect(first.failed).toEqual(["insumos"]);
835
+ // The next run reads the claim and does not draft the line again: the same id would get the same refusal.
836
+ const second = await run(s, "fornecedores-2026-10");
837
+ expect(second.lines.map((l) => l.dispatch)).toEqual(["already_settled", "attempt_id_conflict", "already_settled"]);
838
+ const tries = s.engine.list().filter((e) => e.batch?.index === 1);
839
+ expect(tries).toHaveLength(1);
840
+ expect(tries[0]!.outcomes).toEqual([expect.objectContaining({ attempt_id: g0, status: "failed", code: "attempt_id_conflict", held: "conflict" })]);
841
+ expect(tries[0]!.attempt_generations).toBeUndefined();
842
+ expect(payloadsOf(s, "rail.attempt_spent")).toEqual([]);
843
+ expect(payloadsOf(s, "rail.dispatch").filter((p) => p["attempt_id"] === g0)).toHaveLength(1);
844
+ } finally {
845
+ s.close();
846
+ ledger.close();
847
+ }
848
+ });
849
+
850
+ it("a line still in flight on one machine is not paid by another: it stays open there, for reconciliation", async () => {
851
+ const ledger = newLedger();
852
+ const first = machine("h", { ledger, uncertainPayees: [INSUMOS] });
853
+ try {
854
+ const report = await run(first, "fornecedores-2026-10");
855
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "uncertain", "settled"]);
856
+ // An unknown answer is kept for reconciliation under the id that was sent: one presentation per line, no generation.
857
+ expect(payloadsOf(first, "rail.dispatch")).toHaveLength(3);
858
+ expect(first.engine.list()[1]!.attempt_generations).toBeUndefined();
859
+ } finally {
860
+ first.close();
861
+ }
862
+
863
+ const second = machine("i", { ledger });
864
+ try {
865
+ const report = await run(second, "fornecedores-2026-10");
866
+ expect(report.lines.map((l) => l.dispatch)).toEqual(["settled", "uncertain", "settled"]);
867
+ expect(railOf(second).payCount).toBe(0);
868
+ const line = second.engine.list()[1]!;
869
+ expect(line.state).toBe("executing");
870
+ expect(payloadsOf(second, "rail.dispatch")).toHaveLength(3);
871
+ expect(line.attempt_generations).toBeUndefined();
872
+ expect(payloadsOf(second, "rail.uncertain").map((p) => p["code"])).toEqual(["psp_attempt_in_flight"]);
873
+ // Reconciling asks the rail about the SAME attempt: in flight once, then the provider's own outcome. Still nothing new paid.
874
+ await second.engine.reconcile(line.id);
875
+ const closed = await second.engine.reconcile(line.id);
876
+ expect(closed.state).toBe("settled");
877
+ expect(railOf(second).payCount).toBe(0);
878
+ } finally {
879
+ second.close();
880
+ ledger.close();
881
+ }
882
+ });
883
+ });
@@ -12,10 +12,23 @@ This package is the decisions. The RUNNER around them — the terminal channel,
12
12
 
13
13
  `ExecutionEngine.policy()` runs at draft, at a human's approval and again immediately before `executing`, inside the transaction that writes the outbox row. Mandate status, allowlist, per-transaction cap, the window cap (counting settled, in flight AND awaiting executions as reservations), `escalate_above` and the artifact's `items_hash` are all re-checked at the last gate. A person's approval satisfies `escalate_above` and nothing else.
14
14
 
15
+ ## What the artifact binds besides the items
16
+
17
+ `items_hash` covers the execution's items: payee, amount, currency and a receivable's due date. Two optional bindings cover what those fields cannot see, and each is **omitted, never null**, when it does not apply, so an artifact without them signs the same bytes it signed before they existed (`test/composition.test.ts` pins that against an artifact hashed on `main` before `composition` was added):
18
+
19
+ - `batch` — the set a line was approved inside (`batch_hash` over the ordered lines, the line's `index`, the `count`). One execution per line, N lines.
20
+ - `composition` — what ONE amount is made of (`{ ref, composition_hash, line_count }`, with `compositionHash` over the resolved lines). One execution, N lines behind it: a sale is one charge, so a cart of three lines is one item whose amount is the order's total. Two units at 100 and one unit at 200 are the same item and the same `items_hash`; they are not the same `composition_hash`. `compositionHash` uses `itemsHash`'s canonicalisation over `ref`, `quantity`, `unit_amount`, `amount` and `currency`, not `itemsHash` itself, because ten units at a 10% discount and nine at list price have the same line amount.
21
+
22
+ `checkApprovalArtifact` compares both, and the last gate sends a mismatch back to `awaiting_approval` with `items_hash_mismatch`, like a changed list. `ExecutionEngine.restate()` is how an OPEN execution (`awaiting_approval`, `approved`) follows the proposal behind it — a customer who changes the cart after the order was approved — without a transition: items, total, `items_hash` and composition are replaced, the state, the artifact and the idempotency key are kept, and the last gate does the refusing. The payees of a restatement cannot change.
23
+
24
+ ## What of the artifact reaches the receipt
25
+
26
+ Every spend carries `approval: { items_hash, batch_hash? }`, the artifact's own hashes (`spendApprovalOf`), and the API seals them into the receipt's chain as a link of their own (chain v4, ent#1670). Both rails refuse a spend without them before any call. `composition` is not sent: the API takes the two hashes and nothing else, and only charges carry a composition today. `receipt-chain.ts` recomputes a chain from the receipt read under the `chain_recipe` the key document publishes (RFC 8785 implemented locally, `node:crypto` only), and `verifyReceiptRead` holds the signature, the body and the sealed approval link against the artifact, in that order. It proves WHAT was approved to anybody holding the read; WHO approved stays in the artifact, under the local key. `docs/OPEN_QUESTIONS.md` §3 and §47.
27
+
15
28
  ## Reconcile never re-dispatches
16
29
 
17
30
  An execution left in `executing` is looked up on the rail, attempt by attempt. A recorded settled or failed outcome closes it; `in_flight`, `uncertain` or unknown leaves it in `executing` with `reason: rail_uncertain` for a human. The single exception is an outbox row still `pending` (the state changed, no call was ever made): those attempts go out once, under the same ids.
18
31
 
19
32
  ## Stubs
20
33
 
21
- `stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, and a read that does not answer is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
34
+ `stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, `org_paused: true` (the organization kill switch, ent#1648) refuses it whatever `status` says, and a read that does not answer, or answers without `org_paused`, is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@anthropic-ai/sdk": "0.128.0",
20
- "@codespar/sdk": "0.16.6",
20
+ "@codespar/sdk": "0.16.10",
21
21
  "yaml": "2.9.1",
22
22
  "zod": "3.25.76"
23
23
  }
@@ -16,6 +16,42 @@ export interface ToolContext {
16
16
  engine: ExecutionEngine;
17
17
  /** Fires when a payment tool created an execution; the channel decides what to do with it (ask, or run). */
18
18
  onExecution: (execution: Execution) => Promise<Execution>;
19
+ /**
20
+ * Fires ONCE for a batch, before its first line is drafted, when the channel
21
+ * can take one gesture for the whole list. Absent on a channel that decides
22
+ * line by line (the one-shot, the scenario runner), and then nothing changes:
23
+ * each line reaches `onExecution` and is decided there. The gesture never
24
+ * approves anything itself; every line is still drafted, still reaches
25
+ * `onExecution`, and still mints its own artifact.
26
+ */
27
+ onBatch?: (batch: BatchPresentation) => Promise<BatchGesture | undefined>;
28
+ }
29
+
30
+ /** What a person sees when a whole batch is put in front of them: the list, its total, and the hash of the list. */
31
+ export interface BatchPresentation {
32
+ ref: string;
33
+ label: string;
34
+ /** `batchHash` of the ordered lines. The gesture is a decision about THIS hash and no other. */
35
+ batch_hash: string;
36
+ count: number;
37
+ total_minor: number;
38
+ /** The total as the kit writes money. */
39
+ total: string;
40
+ lines: Array<{
41
+ index: number;
42
+ beneficiary: string;
43
+ amount_minor: number;
44
+ amount: string;
45
+ /** A line a previous run already covers is shown, and does not run again whatever the gesture says. */
46
+ status: "open" | "already_settled" | "in_progress" | "attempt_id_conflict";
47
+ }>;
48
+ }
49
+
50
+ /** The decision a person took on a presented batch: which positions they approved and which they vetoed. */
51
+ export interface BatchGesture {
52
+ batch_hash: string;
53
+ approved: number[];
54
+ vetoed: number[];
19
55
  }
20
56
 
21
57
  export type ToolHandler = (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
@@ -28,6 +64,7 @@ export interface AgentLoopOptions {
28
64
  bundle: ProofBundle;
29
65
  engine: ExecutionEngine;
30
66
  onExecution: ToolContext["onExecution"];
67
+ onBatch?: ToolContext["onBatch"] | undefined;
31
68
  /** A hard stop on tool round-trips per turn, so a looping model cannot run forever. */
32
69
  maxSteps?: number;
33
70
  clock?: () => Date;
@@ -91,7 +128,7 @@ export class AgentLoop {
91
128
  }
92
129
 
93
130
  private async dispatch(call: ToolCall, executions: Execution[]): Promise<{ refused: boolean; content: unknown }> {
94
- const { bundle, handlers, engine, onExecution } = this.options;
131
+ const { bundle, handlers, engine, onExecution, onBatch } = this.options;
95
132
  bundle.transcript({ at: this.now(), kind: "tool_call", tool_call_id: call.id, name: call.name, input: call.input });
96
133
 
97
134
  if (!this.allowed.has(call.name)) {
@@ -110,6 +147,7 @@ export class AgentLoop {
110
147
  executions.push(settledOrNot);
111
148
  return settledOrNot;
112
149
  },
150
+ ...(onBatch ? { onBatch } : {}),
113
151
  });
114
152
  return { refused: false, content };
115
153
  } catch (err) {