@codespar/cli 0.17.0 → 0.18.1

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 (482) hide show
  1. package/README.md +1 -0
  2. package/dist/surface.js +53 -1
  3. package/dist/surface.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/bills-agent/README.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  7. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  8. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  9. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  10. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  11. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  12. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  13. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  14. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  15. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  16. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  17. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  18. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  19. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  20. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  21. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  22. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  23. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  24. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  25. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  26. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  27. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  28. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  29. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  30. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  31. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  32. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  33. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  34. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  35. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  36. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  37. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  38. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  39. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  40. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  41. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  42. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  43. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  44. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  45. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  46. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  63. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  64. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  66. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  67. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  69. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  70. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  71. package/templates/checkout-agent/LICENSE +21 -0
  72. package/templates/checkout-agent/README.md +28 -0
  73. package/templates/checkout-agent/_gitignore +10 -0
  74. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  75. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  76. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  77. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  78. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  79. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  83. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  110. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  111. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  112. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  113. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  158. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  159. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  160. package/templates/checkout-agent/package.json +38 -0
  161. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  162. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  163. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  167. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  169. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  170. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  171. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  172. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  173. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  174. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  175. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  176. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  177. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  178. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  179. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  180. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  181. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  182. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  184. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  185. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  186. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  187. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  188. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  189. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  190. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  191. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  192. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  193. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  195. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  196. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  197. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  198. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  199. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  200. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  201. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  202. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  203. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  204. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  205. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  206. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  208. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  209. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  210. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  211. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  212. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  213. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  216. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  217. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  218. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  219. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  220. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  221. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  222. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  223. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  224. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  269. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  270. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  271. package/templates/checkout-agent/tsconfig.base.json +16 -0
  272. package/templates/checkout-agent/vitest.config.ts +12 -0
  273. package/templates/collections-agent/README.md +6 -1
  274. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  276. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  277. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  278. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  279. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  280. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  281. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  283. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  284. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  285. package/templates/collections-agent/package.json +1 -0
  286. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  287. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  288. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  289. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  290. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  291. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  292. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  293. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  294. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  295. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  296. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  297. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  298. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  299. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  300. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  301. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  302. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  303. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  304. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  305. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  306. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  307. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  308. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  309. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  310. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  311. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  312. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  313. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  314. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  315. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  316. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  317. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  318. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  319. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  320. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  321. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  322. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  339. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  340. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  342. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  343. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  345. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  346. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  347. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  348. package/templates/hello-agent/README.md +1 -1
  349. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  350. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  351. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  352. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  353. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  354. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  355. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  356. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  357. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  358. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  359. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  360. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  361. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  362. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  363. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  364. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  365. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  366. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  367. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  368. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  369. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  370. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  371. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  372. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  373. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  374. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  375. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  376. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  377. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  378. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  379. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  380. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  381. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  382. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  383. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  384. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  385. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  402. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  403. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  405. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  406. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  408. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  409. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  410. package/templates/kits.lock.json +29 -6
  411. package/templates/supplier-payments-agent/README.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  418. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  420. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  424. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  426. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  427. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  428. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  429. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  430. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  431. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  432. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  433. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  435. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  436. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  437. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  438. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  439. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  440. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  441. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  442. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  446. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  447. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  448. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  449. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  452. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  453. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  477. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  478. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  479. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  481. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -47,16 +47,22 @@ export const ConversationScriptSchema = z
47
47
  * different one, which is the secrecy rule as code rather than as prose.
48
48
  */
49
49
  subject: z.string().min(1).optional(),
50
- /** The person's turns, in order. */
50
+ /**
51
+ * The person's turns, in order: something they TYPED (`text`), or a quick
52
+ * reply they TAPPED (`reply`, the id of a button a declared template
53
+ * offers). Exactly one of the two.
54
+ */
51
55
  turns: z
52
56
  .array(
53
57
  z
54
58
  .object({
55
- text: z.string().min(1),
59
+ text: z.string().min(1).optional(),
60
+ reply: z.object({ id: z.string().min(1), title: z.string().min(1).optional() }).strict().optional(),
56
61
  /** Seconds after the previous inbound turn, on the run's clock. Keeps a scripted run deterministic. */
57
62
  after_seconds: z.number().int().nonnegative().default(0),
58
63
  })
59
- .strict(),
64
+ .strict()
65
+ .refine((t) => (t.text === undefined) !== (t.reply === undefined), { message: "a turn is either text the person typed or a reply they tapped, exactly one" }),
60
66
  )
61
67
  .nonempty(),
62
68
  })
@@ -86,6 +92,26 @@ export const TEMPLATE_REGISTRY_FILE = "templates.json";
86
92
  * match is a `132000` from the Cloud API and there is no reason to learn that
87
93
  * in production.
88
94
  */
95
+ /**
96
+ * A quick-reply button a template offers, and what tapping it MEANS.
97
+ *
98
+ * `intent` is the turn the model receives when the person taps it — written
99
+ * here, by whoever wrote the template, and never by the model. A tap carries
100
+ * an id and a title; the title is copy and can change, the id cannot, and the
101
+ * intent is what the id stands for. An id this registry does not declare is
102
+ * not a turn at all.
103
+ */
104
+ export const QuickReplySchema = z
105
+ .object({
106
+ id: z.string().regex(/^[a-z0-9_]{1,64}$/, "a reply id is lower-case letters, digits and underscores"),
107
+ /** What the button says. Meta caps a quick reply at 20 characters. */
108
+ title: z.string().min(1).max(20),
109
+ intent: z.string().min(1),
110
+ })
111
+ .strict();
112
+
113
+ export type QuickReply = z.infer<typeof QuickReplySchema>;
114
+
89
115
  export const WhatsAppTemplateSchema = z
90
116
  .object({
91
117
  /** Meta's own naming rule for a template. */
@@ -94,6 +120,15 @@ export const WhatsAppTemplateSchema = z
94
120
  language: z.string().regex(/^[a-z]{2}(_[A-Z]{2})?$/, 'expected a language code, e.g. "pt_BR"'),
95
121
  description: z.string().min(1),
96
122
  body: z.string().min(1),
123
+ /** Quick replies the template carries (Meta allows up to three on a template). */
124
+ buttons: z.array(QuickReplySchema).max(3).optional(),
125
+ /**
126
+ * The template a poll sends when the outcome it must tell has no copy of
127
+ * its own: it says there is news and asks the person to answer, and states
128
+ * nothing about the outcome — so it can never be the wrong news. Exactly
129
+ * one per registry, taking no variables (`npm run check`).
130
+ */
131
+ fallback: z.boolean().optional(),
97
132
  })
98
133
  .strict();
99
134
 
@@ -120,12 +155,23 @@ export const TemplateRegistrySchema = z
120
155
  .strict()
121
156
  .superRefine((registry, ctx) => {
122
157
  const seen = new Set<string>();
158
+ const replyIds = new Set<string>();
123
159
  for (const [i, template] of registry.templates.entries()) {
124
160
  if (seen.has(template.name)) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["templates", i, "name"], message: `${template.name} is declared twice` });
125
161
  seen.add(template.name);
162
+ // A tap is resolved by its id alone, so one id means one intent across the whole registry.
163
+ for (const [j, button] of (template.buttons ?? []).entries()) {
164
+ if (replyIds.has(button.id)) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["templates", i, "buttons", j, "id"], message: `reply id ${button.id} is declared twice` });
165
+ replyIds.add(button.id);
166
+ }
126
167
  }
127
168
  });
128
169
 
170
+ /** Every quick reply the registry declares, by id: the only taps that can become a turn. */
171
+ export function declaredReplies(templates: readonly WhatsAppTemplate[]): Map<string, QuickReply> {
172
+ return new Map(templates.flatMap((t) => t.buttons ?? []).map((b) => [b.id, b]));
173
+ }
174
+
129
175
  export type TemplateRegistry = z.infer<typeof TemplateRegistrySchema>;
130
176
 
131
177
  export function parseTemplateRegistry(text: string): TemplateRegistry {
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync, readdirSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { parse as parseYaml } from "yaml";
11
- import { ConversationScriptSchema, TemplateRegistrySchema, TEMPLATE_REGISTRY_FILE, templateArity } from "./channels.js";
11
+ import { ConversationScriptSchema, TemplateRegistrySchema, TEMPLATE_REGISTRY_FILE, declaredReplies, templateArity, type TemplateRegistry } from "./channels.js";
12
12
  import { GuardrailsSchema } from "./guardrails.js";
13
13
  import { loadManifest, ManifestSchema, type LoadedManifest } from "./manifest.js";
14
14
  import { MandateSchema } from "./mandate.js";
@@ -121,12 +121,35 @@ export function checkAgent(agentDir: string): CheckReport {
121
121
  if (!manifest.channels.includes("whatsapp") && scripts.length > 0) {
122
122
  error("channels_undeclared", `channels/whatsapp/ ships ${scripts.length} conversation(s) but agent.yaml does not declare the whatsapp channel`);
123
123
  }
124
+ const registryPath = join(whatsappDir, TEMPLATE_REGISTRY_FILE);
125
+ let registry: TemplateRegistry | undefined;
126
+ if (existsSync(registryPath)) {
127
+ const parsed = TemplateRegistrySchema.safeParse(JSON.parse(readFileSync(registryPath, "utf8")));
128
+ if (parsed.success) registry = parsed.data;
129
+ }
130
+ const replies = declaredReplies(registry?.templates ?? []);
124
131
  for (const file of scripts) {
125
132
  const parsed = ConversationScriptSchema.safeParse(JSON.parse(readFileSync(join(whatsappDir, file), "utf8")));
126
133
  if (!parsed.success) error("channels_script_invalid", `channels/whatsapp/${file}: ${parsed.error.message}`);
127
134
  else if (parsed.data.name !== file.replace(/\.json$/, "")) error("channels_script_invalid", `channels/whatsapp/${file} declares name ${parsed.data.name}`);
135
+ else {
136
+ // A scripted tap must be one a declared template offers: the channel drops any other, so the script would test nothing.
137
+ for (const turn of parsed.data.turns) {
138
+ if (turn.reply && !replies.has(turn.reply.id)) error("channels_script_invalid", `channels/whatsapp/${file} taps ${turn.reply.id}, which no template in ${TEMPLATE_REGISTRY_FILE} offers`);
139
+ }
140
+ }
141
+ }
142
+ // An agent on WhatsApp closes outcomes days later, outside the window, where
143
+ // only a template goes. One it has no copy for still has to reach the
144
+ // person, so the registry declares a fallback that states no outcome.
145
+ if (manifest.channels.includes("whatsapp")) {
146
+ if (!existsSync(registryPath)) error("channels_templates_missing", `agent.yaml declares the whatsapp channel and channels/whatsapp/${TEMPLATE_REGISTRY_FILE} does not exist`);
147
+ else if (registry) {
148
+ const fallbacks = registry.templates.filter((t) => t.fallback);
149
+ if (fallbacks.length !== 1) error("channels_templates_fallback", `channels/whatsapp/${TEMPLATE_REGISTRY_FILE} declares ${fallbacks.length} fallback template(s); it must declare exactly one, for an outcome the agent has no template for`);
150
+ else if (templateArity(fallbacks[0]!.body) !== 0) error("channels_templates_fallback", `the fallback template ${fallbacks[0]!.name} takes variables; it is sent when nothing specific can be said, so it takes none`);
151
+ }
128
152
  }
129
- const registryPath = join(whatsappDir, TEMPLATE_REGISTRY_FILE);
130
153
  if (existsSync(registryPath)) {
131
154
  const parsed = TemplateRegistrySchema.safeParse(JSON.parse(readFileSync(registryPath, "utf8")));
132
155
  if (!parsed.success) error("channels_templates_invalid", `channels/whatsapp/${TEMPLATE_REGISTRY_FILE}: ${parsed.error.message}`);
@@ -12,19 +12,21 @@
12
12
  * meantime is caught at the last gate, which is the only one that matters.
13
13
  */
14
14
  import { checkApprovalArtifact, createApprovalArtifact, type ApprovalSigner } from "./approval.js";
15
- import type { ProofBundle } from "./bundle.js";
15
+ import { maskPayee, type ProofBundle } from "./bundle.js";
16
16
  import { evaluateEscalation, type Escalation } from "./escalate.js";
17
17
  import { CHARGE_CANCELLED, CHARGE_EXPIRED, CHARGE_PAID, PAYMENT_FAILED, PAYMENT_SUCCEEDED, type PublishedEvent } from "./events.js";
18
18
  import type { Guardrails } from "./guardrails.js";
19
- import { itemsHash, sha256Hex } from "./hash.js";
19
+ import { batchAttemptId, itemsHash, sha256Hex } from "./hash.js";
20
20
  import { newId } from "./ids.js";
21
21
  import { mandateExpired, payeeAllowed, resolveBeneficiary, windowCap, windowStart, type Mandate } from "./mandate.js";
22
22
  import type { Manifest } from "./manifest.js";
23
- import type { PaymentRail, RailOutcome, RailPayment } from "./rail.js";
23
+ import { checkQuote, quoteFromApproval } from "./quote.js";
24
+ import { spendApprovalOf, type PaymentRail, type RailOutcome, type RailPayment, type SealedSpendApproval, type SpendApproval } from "./rail.js";
25
+ import { sameApprovalHash } from "./receipt-chain.js";
24
26
  import type { MandateStatusReport, MandateStatusSource } from "./revocation.js";
25
27
  import { isTerminal, transition, type Execution, type ExecutionState } from "./state-machine.js";
26
28
  import type { StateStore } from "./state/store.js";
27
- import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
29
+ import type { Actor, ApprovalArtifact, ApprovalMode, ExecutionBatch, ExecutionComposition, ExecutionItem, ExecutionReason, ItemOutcome } from "./types.js";
28
30
 
29
31
  export interface ProposedItem {
30
32
  /** An alias from the mandate's named payees, or a raw payee key. */
@@ -61,6 +63,13 @@ export interface Proposal {
61
63
  * it. The core never invents one and never fills one in.
62
64
  */
63
65
  batch?: ExecutionBatch;
66
+ /**
67
+ * What the proposal's amount is composed of, when it is composed of lines
68
+ * (a cart behind one order). The kit computes `compositionHash` over the
69
+ * resolved lines; the core stamps it on the execution and every artifact of
70
+ * it, and never computes, fills in or reads the lines.
71
+ */
72
+ composition?: ExecutionComposition;
64
73
  }
65
74
 
66
75
  export type DraftResult =
@@ -94,6 +103,14 @@ type Verdict =
94
103
 
95
104
  type Gate = "draft" | "approve" | "execute";
96
105
 
106
+ /**
107
+ * How many spent generations of one batch line's attempt id a dispatch walks
108
+ * past before it reports the line failed. Each one is a payment the provider
109
+ * already refused for this exact line, so eight is a line that keeps failing,
110
+ * not a line still looking for its id.
111
+ */
112
+ const MAX_ATTEMPT_GENERATION = 8;
113
+
97
114
  /** Open executions reserve the window: a cap is a ceiling on what may be committed, not a balance. */
98
115
  const WINDOW_STATES: ExecutionState[] = ["awaiting_approval", "approved", "executing", "settled"];
99
116
 
@@ -127,6 +144,7 @@ export class ExecutionEngine {
127
144
  const items = proposal.items.map((p) => this.resolveItem(p));
128
145
  const total = items.reduce((sum, i) => sum + i.amount, 0);
129
146
  if (proposal.batch) assertBatchBinding(proposal.batch);
147
+ if (proposal.composition) assertCompositionBinding(proposal.composition);
130
148
  const id = newId("exe");
131
149
  const execution: Execution<"drafted"> = {
132
150
  id,
@@ -140,6 +158,7 @@ export class ExecutionEngine {
140
158
  ...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
141
159
  items_hash: itemsHash(items),
142
160
  ...(proposal.batch ? { batch: proposal.batch } : {}),
161
+ ...(proposal.composition ? { composition: proposal.composition } : {}),
143
162
  mandate: { id: this.deps.mandate.id, version: this.deps.mandate.version },
144
163
  idempotency_key: `idk_${sha256Hex(`${this.deps.runId}:${id}`).slice(0, 32)}`,
145
164
  blocking_reasons: [],
@@ -149,11 +168,58 @@ export class ExecutionEngine {
149
168
  updated_at: now.toISOString(),
150
169
  };
151
170
  this.deps.store.saveExecution(execution);
152
- this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null });
171
+ this.record("execution.drafted", id, { items: execution.items, total, model_claimed_total: proposal.claimed_total ?? null, mode: this.deps.mode, batch: execution.batch ?? null, ...(execution.composition ? { composition: execution.composition } : {}) });
153
172
 
154
173
  return { ok: true, execution: await this.evaluateDraft(execution) };
155
174
  }
156
175
 
176
+ /**
177
+ * The proposal behind an OPEN execution changed, and the execution follows
178
+ * it: a customer who changes the cart after the order was approved is
179
+ * changing the order, not opening a second one. The items, the total, the
180
+ * `items_hash` and the composition are replaced; the state, the approval
181
+ * and the idempotency key are NOT.
182
+ *
183
+ * That is the point. No transition happens here and nothing is judged: an
184
+ * `approved` execution keeps the artifact it was approved with, and the
185
+ * last gate (`execute`) compares the two and sends it back to
186
+ * `awaiting_approval` with `items_hash_mismatch` — the same edge a list
187
+ * tampered after approval takes, reached honestly. An `awaiting_approval`
188
+ * one is judged again by whoever decides it, on what it says now.
189
+ *
190
+ * The payees may not change, in number or in order: a restatement changes
191
+ * what is charged or paid, never to whom. Nothing is dispatched by an open
192
+ * execution, so no attempt can exist under the key it keeps.
193
+ */
194
+ restate(executionId: string, proposal: Proposal): Execution {
195
+ const execution = this.mustGet(executionId);
196
+ if (execution.state !== "awaiting_approval" && execution.state !== "approved") throw new Error(`execution ${executionId} is ${execution.state}; only an open execution (awaiting_approval, approved) can be restated`);
197
+ if (proposal.batch) throw new Error("a batch line is not restated; the batch is presented again");
198
+ if (proposal.composition) assertCompositionBinding(proposal.composition);
199
+ const items = proposal.items.map((p) => this.resolveItem(p));
200
+ const before = execution.items.map((i) => i.payee);
201
+ if (items.length !== before.length || items.some((item, i) => item.payee !== before[i])) throw new Error(`execution ${executionId}: a restatement may not change the payees`);
202
+ const total = items.reduce((sum, i) => sum + i.amount, 0);
203
+ const { composition: _composition, model_claimed_total: _claimed, ...rest } = execution;
204
+ const restated: Execution = {
205
+ ...rest,
206
+ items,
207
+ total,
208
+ ...(proposal.claimed_total !== undefined ? { model_claimed_total: proposal.claimed_total } : {}),
209
+ items_hash: itemsHash(items),
210
+ ...(proposal.composition ? { composition: proposal.composition } : {}),
211
+ updated_at: this.clock().toISOString(),
212
+ };
213
+ this.deps.store.saveExecution(restated);
214
+ this.record("execution.restated", execution.id, {
215
+ state: execution.state,
216
+ items_hash: { from: execution.items_hash, to: restated.items_hash },
217
+ total: { from: execution.total, to: total },
218
+ composition: { from: execution.composition ?? null, to: restated.composition ?? null },
219
+ });
220
+ return restated;
221
+ }
222
+
157
223
  // ---- deterministic policy ---------------------------------------------
158
224
 
159
225
  /**
@@ -179,6 +245,14 @@ export class ExecutionEngine {
179
245
  return { kind: "deny", reason: "beneficiary_not_allowed", detail };
180
246
  }
181
247
 
248
+ // A receivable closed `charge_reference_ambiguous` was issued and may still be paid. Another one to the same payee is how a debtor pays twice.
249
+ const unreconciled = this.deps.store
250
+ .listExecutions({ state: "failed", mandate_id: mandate.id })
251
+ .find((e) => e.id !== execution.id && e.reason === "charge_reference_ambiguous" && e.items.some((i) => execution.items.some((mine) => mine.payee === i.payee)));
252
+ if (unreconciled) {
253
+ return { kind: "deny", reason: "charge_reference_ambiguous", detail: `execution ${unreconciled.id} issued a receivable to this payee whose reference turned ambiguous; reconcile it by the charge id before issuing another` };
254
+ }
255
+
182
256
  const over = execution.items.find((i) => i.amount > mandate.per_tx_cap_minor);
183
257
  if (over) return { kind: "deny", reason: "per_tx_cap_exceeded", detail: `${over.beneficiary}: ${over.amount} is above the per-payment cap ${mandate.per_tx_cap_minor}` };
184
258
 
@@ -295,7 +369,7 @@ export class ExecutionEngine {
295
369
  if (!artifact) throw new Error(`execution ${executionId} is approved without an approval artifact`);
296
370
 
297
371
  // Everything below is one transaction: the last policy run, the artifact check, the outbox row and the state change land together or not at all.
298
- const payments = this.paymentsFor(execution);
372
+ const payments = this.paymentsFor(execution, artifact);
299
373
  const decided = this.deps.store.transaction((): Execution => {
300
374
  // The approval was given under one mandate; a re-signed one (new version) is a new authorization, and the person decides again under it.
301
375
  const current = { id: this.deps.mandate.id, version: this.deps.mandate.version };
@@ -310,6 +384,9 @@ export class ExecutionEngine {
310
384
  }
311
385
  return this.persist(transition(this.withoutApproval(approved), "awaiting_approval", { at, actor: this.agentActor, reason: "items_hash_mismatch", detail: `approval ${artifact.approval_id} does not match what would be executed (${check.problem})` }));
312
386
  }
387
+ // The API only records a quote that disagrees with the spend, and pays anyway. The kit does not send one.
388
+ const unquoted = payments.map(checkQuote).find((q) => !q.ok);
389
+ if (unquoted && !unquoted.ok) return this.persist(transition(approved, "denied", { at, actor: this.agentActor, reason: "quote_mismatch", detail: `${unquoted.code}: ${unquoted.detail}; nothing was sent` }));
313
390
 
314
391
  const verdict = this.policy(execution, now, "execute", artifact.approver.type === "person");
315
392
  if (verdict.kind === "deny" || verdict.kind === "block") {
@@ -343,14 +420,33 @@ export class ExecutionEngine {
343
420
  private async dispatch(execution: Execution<"executing">, payments: RailPayment[]): Promise<Execution> {
344
421
  const outcomes: ItemOutcome[] = [...execution.outcomes];
345
422
  const unknown: string[] = [];
423
+ const sealMismatches: string[] = [];
346
424
  this.deps.store.updateOutbox(execution.idempotency_key, "sent", undefined, this.clock().toISOString());
347
425
 
348
- for (const [index, payment] of payments.entries()) {
426
+ const generations: Record<number, number> = { ...execution.attempt_generations };
427
+ for (const [index, presented] of payments.entries()) {
349
428
  if (outcomes.some((o) => o.index === index)) continue;
429
+ let payment = presented;
350
430
  // The dispatch line is the request. `idempotency_key` is what makes a retry the same payment, so the bundle names it next to the attempt.
351
431
  this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
352
- const outcome = await this.deps.rail.pay(payment);
432
+ let outcome = await this.deps.rail.pay(payment);
353
433
  this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
434
+ // A batch line whose derived id the rail holds as failed-with-no-money
435
+ // moves to the next generation of it, and only on that answer (see
436
+ // `batchAttemptId`). The generation is saved before the next id goes
437
+ // out, so a crash or an unknown answer is reconciled against the id
438
+ // that was actually sent.
439
+ while (execution.batch && outcome.status === "failed" && outcome.spent && (generations[index] ?? 0) < MAX_ATTEMPT_GENERATION) {
440
+ const generation = (generations[index] ?? 0) + 1;
441
+ const next = batchAttemptId(execution.mandate.id, execution.batch, index, generation);
442
+ this.record("rail.attempt_spent", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, next_attempt_id: next, generation });
443
+ generations[index] = generation;
444
+ this.deps.store.saveExecution({ ...execution, outcomes, attempt_generations: { ...generations }, updated_at: this.clock().toISOString() });
445
+ payment = { ...payment, attempt_id: next };
446
+ this.record("rail.dispatch", execution.id, { attempt_id: payment.attempt_id, payee: payment.payee, amount: payment.amount_minor, rail: this.deps.rail.name, idempotency_key: execution.idempotency_key });
447
+ outcome = await this.deps.rail.pay(payment);
448
+ this.record("rail.outcome", execution.id, { attempt_id: payment.attempt_id, status: outcome.status, ...railAnswer(outcome) });
449
+ }
354
450
  if (outcome.status === "uncertain") {
355
451
  this.record("rail.uncertain", execution.id, { attempt_id: payment.attempt_id, code: outcome.code, message: outcome.message });
356
452
  // No outcome is recorded because there is none. The siblings are still
@@ -360,7 +456,7 @@ export class ExecutionEngine {
360
456
  continue;
361
457
  }
362
458
  if (outcome.status === "failed") {
363
- outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}` });
459
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "failed", code: outcome.code, error: `${outcome.code}: ${outcome.message}`, ...(outcome.held ? { held: outcome.held } : {}) });
364
460
  continue;
365
461
  }
366
462
  if (outcome.status === "accepted") {
@@ -369,10 +465,13 @@ export class ExecutionEngine {
369
465
  this.recordInstrument(execution.id, payment.attempt_id, outcome.transaction_id, outcome.instrument);
370
466
  continue;
371
467
  }
372
- outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}) });
373
- await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
468
+ outcomes.push({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: outcome.transaction_id, ...(outcome.receipt_id ? { receipt_id: outcome.receipt_id } : {}), ...(outcome.replayed ? { replayed: true as const } : {}) });
469
+ const mismatch = await this.ingestSettlement(execution, index, payment, outcome.receipt_id, PAYMENT_SUCCEEDED);
470
+ if (mismatch) sealMismatches.push(mismatch);
374
471
  }
375
- return this.close({ ...execution, outcomes }, payments.length, unknown.join("; ") || undefined);
472
+ const closed = this.close({ ...execution, outcomes, ...(Object.keys(generations).length > 0 ? { attempt_generations: generations } : {}) }, payments.length, unknown.join("; ") || undefined);
473
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
474
+ return closed;
376
475
  }
377
476
 
378
477
  /**
@@ -395,10 +494,13 @@ export class ExecutionEngine {
395
494
  if (execution.outcomes.length < attempts) {
396
495
  return this.leaveUnresolved(updated, unknownDetail ?? `${execution.outcomes.length} of ${attempts} attempt(s) have an outcome`);
397
496
  }
398
- const failed = execution.outcomes.find((o) => o.status === "failed");
497
+ // An ambiguous receivable outranks any other failure: it is the one outcome that must be reconciled rather than issued again.
498
+ const failed = execution.outcomes.find((o) => o.status === "failed" && o.code === "charge_reference_ambiguous") ?? execution.outcomes.find((o) => o.status === "failed");
399
499
  if (failed) {
400
500
  this.deps.store.updateOutbox(execution.idempotency_key, "failed", execution.outcomes, at);
401
- const reason: ExecutionReason = failed.code === "charge_expired" || failed.code === "charge_cancelled" ? failed.code : "rail_failed";
501
+ // `org_paused` is the API refusing the spend itself: the kill switch was pressed after the gate read the status. Nothing moved.
502
+ const reason: ExecutionReason =
503
+ failed.code === "charge_expired" || failed.code === "charge_cancelled" || failed.code === "org_paused" || failed.code === "charge_reference_ambiguous" ? failed.code : "rail_failed";
402
504
  return this.persist(transition(updated, "failed", { at, actor: this.agentActor, reason, detail: failed.error ?? "rail refused" }));
403
505
  }
404
506
  if (execution.outcomes.filter((o) => o.status === "settled").length === attempts) {
@@ -439,7 +541,8 @@ export class ExecutionEngine {
439
541
  if (stored) this.deps.bundle.event({ ...stored, actor: this.agentActor });
440
542
  }
441
543
 
442
- private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<void> {
544
+ /** Records a settled attempt and fetches its receipt; answers the seal mismatch, if the receipt disagrees with what was paid. */
545
+ private async ingestSettlement(execution: Execution, index: number, payment: RailPayment, receiptId: string | null, eventType: PublishedEvent): Promise<string | undefined> {
443
546
  // The rail's answer is the settlement event the API publishes (section 4.3): `commerce.payment.succeeded` for a payout, `commerce.charge.paid` for a receivable. Keyed by attempt so a replay is a no-op.
444
547
  const appended = this.deps.store.appendEvent({
445
548
  run_id: this.deps.runId,
@@ -450,15 +553,38 @@ export class ExecutionEngine {
450
553
  at: this.clock().toISOString(),
451
554
  });
452
555
  if (appended) this.deps.bundle.event({ ...appended, actor: this.agentActor, item_index: index });
453
- if (receiptId) await this.saveReceipt(execution.id, receiptId);
556
+ return receiptId ? this.saveReceipt(execution.id, receiptId, payment) : undefined;
454
557
  }
455
558
 
456
- private async saveReceipt(executionId: string, receiptId: string): Promise<void> {
559
+ /**
560
+ * The bundle's copy is the receipt as it was sealed, payee included. A
561
+ * payment receipt whose sealed payee is not the payee this attempt paid —
562
+ * including one that sealed no payee — is recorded and answered as a
563
+ * mismatch, which the caller raises once the execution's outcome is saved:
564
+ * the money is where the rail says it is, and it is the EVIDENCE that
565
+ * disagrees. The same holds for the approval link (§3): a receipt that
566
+ * seals other hashes than the ones this attempt sent, or none, does not
567
+ * say this payment was made against the approved list. A rail that cannot
568
+ * report the link leaves it `undefined`, and nothing is compared. A paid
569
+ * charge carries no seal, so there is nothing to compare at all.
570
+ */
571
+ private async saveReceipt(executionId: string, receiptId: string, payment: RailPayment): Promise<string | undefined> {
457
572
  const receipt = await this.deps.rail.receipt(receiptId, this.agentActor);
458
- if (receipt) {
459
- const path = this.deps.bundle.receipt(receipt);
460
- this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
573
+ if (!receipt) return undefined;
574
+ const path = this.deps.bundle.receipt(receipt, { approval_id: this.deps.store.getExecution(executionId)?.approval_id });
575
+ this.record("receipt.saved", executionId, { receipt_id: receiptId, path });
576
+ if (receipt.kind === "charge") return undefined;
577
+ const mismatches: string[] = [];
578
+ if (receipt.payment.payee !== payment.payee) {
579
+ const sealed = receipt.payment.payee === null ? null : maskPayee(receipt.payment.payee);
580
+ this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_payee: sealed, paid_payee: maskPayee(payment.payee) });
581
+ mismatches.push(`receipt ${receiptId} seals payee ${sealed ?? "none"}; attempt ${payment.attempt_id} paid ${maskPayee(payment.payee)}`);
461
582
  }
583
+ if (receipt.approval !== undefined && payment.approval && !sealsApproval(receipt.approval, payment.approval)) {
584
+ this.record("receipt.seal_mismatch", executionId, { receipt_id: receiptId, attempt_id: payment.attempt_id, sealed_approval: receipt.approval, sent_approval: payment.approval });
585
+ mismatches.push(`receipt ${receiptId} seals approval ${receipt.approval ? receipt.approval.items_hash : "none"}; attempt ${payment.attempt_id} sent ${payment.approval.items_hash}`);
586
+ }
587
+ return mismatches.length > 0 ? mismatches.join("; ") : undefined;
462
588
  }
463
589
 
464
590
 
@@ -470,12 +596,17 @@ export class ExecutionEngine {
470
596
  async collectReceipts(executionId: string): Promise<string[]> {
471
597
  const execution = this.mustGet(executionId);
472
598
  const held = new Set(this.deps.bundle.listReceipts().map((f) => f.replace(/\.json$/, "")));
599
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
473
600
  const fetched: string[] = [];
601
+ const sealMismatches: string[] = [];
474
602
  for (const outcome of execution.outcomes) {
475
- if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id)) continue;
476
- await this.saveReceipt(execution.id, outcome.receipt_id);
603
+ const payment = payments[outcome.index];
604
+ if (outcome.status !== "settled" || !outcome.receipt_id || held.has(outcome.receipt_id) || !payment) continue;
605
+ const mismatch = await this.saveReceipt(execution.id, outcome.receipt_id, payment);
606
+ if (mismatch) sealMismatches.push(mismatch);
477
607
  fetched.push(outcome.receipt_id);
478
608
  }
609
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
479
610
  return fetched;
480
611
  }
481
612
 
@@ -492,13 +623,14 @@ export class ExecutionEngine {
492
623
  async reconcile(executionId: string): Promise<Execution> {
493
624
  const execution = this.mustGet(executionId);
494
625
  if (execution.state !== "executing") return execution;
495
- const payments = this.paymentsFor(execution);
626
+ const payments = this.paymentsFor(execution, this.approvalOf(execution));
496
627
  const outbox = this.deps.store.getOutbox(execution.idempotency_key);
497
628
  if (outbox?.status === "pending") {
498
629
  return this.leaveUnresolved(execution as Execution<"executing">, "outbox pending: nothing was ever sent; `resume` dispatches it, reconcile does not");
499
630
  }
500
631
 
501
632
  const outcomes: ItemOutcome[] = [...execution.outcomes];
633
+ const sealMismatches: string[] = [];
502
634
  let unresolved: string | undefined;
503
635
  for (const [index, payment] of payments.entries()) {
504
636
  const prior = outcomes.find((o) => o.index === index);
@@ -536,15 +668,17 @@ export class ExecutionEngine {
536
668
  continue;
537
669
  }
538
670
  if (seen.status === "failed") {
539
- replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}` });
671
+ replace({ index, attempt_id: payment.attempt_id, status: "failed", code: seen.code, error: `${seen.code}: ${seen.message}`, ...(seen.held ? { held: seen.held } : {}) });
540
672
  continue;
541
673
  }
542
- replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}) });
543
- await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
674
+ replace({ index, attempt_id: payment.attempt_id, status: "settled", transaction_id: seen.transaction_id, ...(seen.receipt_id ? { receipt_id: seen.receipt_id } : {}), ...(seen.replayed ? { replayed: true as const } : {}) });
675
+ const mismatch = await this.ingestSettlement(execution, index, payment, seen.receipt_id, prior ? CHARGE_PAID : PAYMENT_SUCCEEDED);
676
+ if (mismatch) sealMismatches.push(mismatch);
544
677
  }
545
678
  const updated: Execution<"executing"> = { ...(execution as Execution<"executing">), outcomes };
546
- if (unresolved) return this.leaveUnresolved(updated, unresolved);
547
- return this.close(updated, payments.length);
679
+ const reconciled = unresolved ? this.leaveUnresolved(updated, unresolved) : this.close(updated, payments.length);
680
+ if (sealMismatches.length > 0) throw new ReceiptSealMismatchError(execution.id, sealMismatches);
681
+ return reconciled;
548
682
  }
549
683
 
550
684
  /**
@@ -559,7 +693,7 @@ export class ExecutionEngine {
559
693
  const outbox = this.deps.store.getOutbox(execution.idempotency_key);
560
694
  if (outbox?.status !== "pending") return this.reconcile(executionId);
561
695
  this.record("rail.resume", execution.id, { detail: "outbox pending: nothing was ever sent; dispatching once under the same attempt ids" });
562
- return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution));
696
+ return this.dispatch(execution as Execution<"executing">, this.paymentsFor(execution, this.approvalOf(execution)));
563
697
  }
564
698
 
565
699
  /** Time-based exits: an open execution past its approval TTL closes as `expired`. */
@@ -770,21 +904,44 @@ export class ExecutionEngine {
770
904
  };
771
905
  }
772
906
 
773
- private paymentsFor(execution: Execution): RailPayment[] {
774
- return execution.items.map((item, index) => ({
775
- attempt_id: `att_${execution.idempotency_key.slice(4)}_${index}`,
776
- mandate_id: execution.mandate.id,
777
- amount_minor: item.amount,
778
- currency: item.currency,
779
- payee: item.payee,
780
- beneficiary: item.beneficiary,
781
- purpose: this.deps.mandate.purpose,
782
- agent_id: this.deps.mandate.agent_id,
783
- consumer_id: this.deps.mandate.consumer_id,
784
- ...(item.description ? { description: item.description } : {}),
785
- ...(item.due_date ? { due_date: item.due_date } : {}),
786
- actor: this.agentActor,
787
- }));
907
+ /**
908
+ * The attempts of an execution. Each carries the quote of the SAME line of
909
+ * the approval artifact — what was approved, not what the execution says
910
+ * now — so a line that drifted after approval is a quote that disagrees,
911
+ * refused before the call. Each also carries the artifact's hashes, which
912
+ * the API seals into the receipt (OPEN_QUESTIONS §3): the artifact is fixed
913
+ * per execution, so a reconcile presents the same approval the first
914
+ * dispatch did, and a batch line's hashes are the same on every machine.
915
+ */
916
+ private paymentsFor(execution: Execution, artifact: ApprovalArtifact): RailPayment[] {
917
+ return execution.items.map((item, index) => {
918
+ // A batch line is the same attempt, with the same quote, on every run of the same list, wherever it runs (§39c); anything else is its execution's own.
919
+ const quote = quoteFromApproval(artifact, index, this.deps.mandate.purpose, { stable: execution.batch !== undefined });
920
+ return {
921
+ attempt_id: execution.batch
922
+ ? batchAttemptId(execution.mandate.id, execution.batch, index, execution.attempt_generations?.[index] ?? 0)
923
+ : `att_${execution.idempotency_key.slice(4)}_${index}`,
924
+ mandate_id: execution.mandate.id,
925
+ amount_minor: item.amount,
926
+ currency: item.currency,
927
+ payee: item.payee,
928
+ beneficiary: item.beneficiary,
929
+ purpose: this.deps.mandate.purpose,
930
+ agent_id: this.deps.mandate.agent_id,
931
+ consumer_id: this.deps.mandate.consumer_id,
932
+ ...(item.description ? { description: item.description } : {}),
933
+ ...(item.due_date ? { due_date: item.due_date } : {}),
934
+ ...(quote ? { quote } : {}),
935
+ approval: spendApprovalOf(artifact),
936
+ actor: this.agentActor,
937
+ };
938
+ });
939
+ }
940
+
941
+ private approvalOf(execution: Execution): ApprovalArtifact {
942
+ const artifact = execution.approval_id ? this.deps.store.getApproval(execution.approval_id) : undefined;
943
+ if (!artifact) throw new Error(`execution ${execution.id} is ${execution.state} without an approval artifact`);
944
+ return artifact;
788
945
  }
789
946
 
790
947
  private withoutApproval(execution: Execution<"approved">): Execution<"approved"> {
@@ -827,7 +984,7 @@ export class ExecutionEngine {
827
984
  private storeApproval(artifact: ApprovalArtifact): void {
828
985
  this.deps.store.saveApproval(artifact);
829
986
  this.deps.bundle.approval(artifact);
830
- this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, escalation: artifact.escalation ?? null });
987
+ this.record("approval.created", artifact.execution_id, { approval_id: artifact.approval_id, approver: artifact.approver, items_hash: artifact.items_hash, batch: artifact.batch ?? null, ...(artifact.composition ? { composition: artifact.composition } : {}), escalation: artifact.escalation ?? null });
831
988
  }
832
989
 
833
990
  private persist<S extends ExecutionState>(execution: Execution<S>): Execution<S> {
@@ -868,6 +1025,13 @@ function assertBatchBinding(batch: ExecutionBatch): void {
868
1025
  }
869
1026
  }
870
1027
 
1028
+ /** A composition binding is the kit's to compute and the core's to refuse when it cannot be true. */
1029
+ function assertCompositionBinding(composition: ExecutionComposition): void {
1030
+ if (!composition.ref.trim()) throw new Error("composition.ref must name what was composed");
1031
+ if (!/^sha256:[0-9a-f]{64}$/.test(composition.composition_hash)) throw new Error("composition.composition_hash must be the compositionHash of the resolved lines");
1032
+ if (!Number.isInteger(composition.line_count) || composition.line_count < 1) throw new Error(`composition.line_count must be a positive integer, got ${composition.line_count}`);
1033
+ }
1034
+
871
1035
  /**
872
1036
  * What the rail answered about one attempt, as the bundle records it: the
873
1037
  * identifiers a reader follows the money by, and never `raw`, which carries
@@ -878,8 +1042,33 @@ function railAnswer(outcome: RailOutcome): Record<string, unknown> {
878
1042
  case "accepted":
879
1043
  return { transaction_id: outcome.transaction_id, sandbox: outcome.sandbox };
880
1044
  case "settled":
881
- return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox };
1045
+ return { transaction_id: outcome.transaction_id, receipt_id: outcome.receipt_id, money_moved: outcome.money_moved, sandbox: outcome.sandbox, ...(outcome.replayed ? { idempotent_replay: true } : {}) };
1046
+ case "failed":
1047
+ return { code: outcome.code, message: outcome.message, ...(outcome.spent ? { spent: true } : {}), ...(outcome.held ? { held: outcome.held } : {}) };
882
1048
  default:
883
1049
  return { code: outcome.code, message: outcome.message };
884
1050
  }
885
1051
  }
1052
+
1053
+ /** The approval link a receipt sealed is the one a spend sent: the same hashes, in either spelling of the `sha256:` prefix. */
1054
+ function sealsApproval(sealed: SealedSpendApproval | null, sent: SpendApproval): boolean {
1055
+ return sealed !== null && sameApprovalHash(sealed.items_hash, sent.items_hash) && sameApprovalHash(sealed.batch_hash, sent.batch_hash ?? null);
1056
+ }
1057
+
1058
+ /**
1059
+ * A payment settled and the receipt CodeSpar sealed for it names a different
1060
+ * payee, or none, or seals another approval than the one the spend sent.
1061
+ * Raised after the execution's outcome is saved, never instead of it: the
1062
+ * rail's answer is the record of where the money went, and this error is
1063
+ * about the evidence. Not a warning, because a receipt that does not name
1064
+ * what was paid proves nothing about that payment.
1065
+ */
1066
+ export class ReceiptSealMismatchError extends Error {
1067
+ constructor(
1068
+ readonly executionId: string,
1069
+ readonly mismatches: string[],
1070
+ ) {
1071
+ super(`execution ${executionId}: the sealed receipt does not seal what was paid — ${mismatches.join("; ")}`);
1072
+ this.name = "ReceiptSealMismatchError";
1073
+ }
1074
+ }