@codespar/cli 0.17.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 (480) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/templates/bills-agent/README.md +1 -1
  4. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  5. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  7. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  8. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  9. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  10. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  11. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  12. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  13. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  14. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  15. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  16. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  17. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  18. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  19. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  20. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  21. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  22. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  23. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  24. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  25. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  26. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  27. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  28. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  29. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  30. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  31. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  32. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  33. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  34. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  35. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  36. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  37. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  38. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  39. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  40. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  41. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  42. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  43. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  44. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  45. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  46. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  52. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  53. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  59. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  60. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  61. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  62. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  63. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  64. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  65. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  66. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  67. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  69. package/templates/checkout-agent/LICENSE +21 -0
  70. package/templates/checkout-agent/README.md +28 -0
  71. package/templates/checkout-agent/_gitignore +10 -0
  72. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  73. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  74. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  75. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  76. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  77. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  78. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  79. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  83. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  84. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  108. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  109. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  110. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  111. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  112. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  113. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  141. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  146. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  147. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  156. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  157. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  158. package/templates/checkout-agent/package.json +38 -0
  159. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  160. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  161. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  162. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  163. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  167. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  168. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  169. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  170. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  171. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  172. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  173. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  174. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  175. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  176. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  177. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  178. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  179. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  180. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  181. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  182. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  184. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  185. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  186. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  187. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  188. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  189. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  190. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  191. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  192. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  193. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  195. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  196. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  197. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  198. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  199. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  200. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  201. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  202. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  203. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  204. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  205. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  206. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  208. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  209. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  210. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  211. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  212. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  213. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  216. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  217. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  218. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  219. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  220. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  221. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  222. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  223. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  224. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  260. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  261. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  267. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  268. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  269. package/templates/checkout-agent/tsconfig.base.json +16 -0
  270. package/templates/checkout-agent/vitest.config.ts +12 -0
  271. package/templates/collections-agent/README.md +6 -1
  272. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  273. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  274. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  276. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  277. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  278. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  279. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  280. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  281. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  283. package/templates/collections-agent/package.json +1 -0
  284. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  285. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  286. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  287. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  288. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  289. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  290. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  291. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  292. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  293. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  294. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  295. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  296. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  297. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  298. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  299. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  300. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  301. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  302. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  303. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  304. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  305. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  306. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  307. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  308. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  309. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  310. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  311. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  312. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  313. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  314. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  315. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  316. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  317. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  318. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  319. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  320. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  321. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  322. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  328. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  329. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  335. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  336. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  337. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  338. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  339. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  340. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  341. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  342. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  343. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  345. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  346. package/templates/hello-agent/README.md +1 -1
  347. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  348. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  349. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  350. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  351. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  352. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  353. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  354. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  355. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  356. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  357. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  358. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  359. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  360. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  361. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  362. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  363. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  364. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  365. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  366. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  367. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  368. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  369. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  370. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  371. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  372. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  373. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  374. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  375. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  376. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  377. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  378. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  379. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  380. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  381. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  382. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  383. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  384. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  385. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  391. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  392. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  398. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  399. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  400. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  401. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  402. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  403. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  404. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  405. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  406. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  408. package/templates/kits.lock.json +29 -6
  409. package/templates/supplier-payments-agent/README.md +1 -1
  410. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  411. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  416. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  417. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  418. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  420. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  424. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  425. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  426. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  427. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  428. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  429. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  430. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  431. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  432. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  433. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  435. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  437. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  438. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  439. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  440. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  441. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  442. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  443. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  446. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  447. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  451. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  452. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  453. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  473. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  474. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  477. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  478. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  479. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -2,8 +2,10 @@
2
2
  * The CodeSpar sandbox rail. With the signed envelope the consent returned
3
3
  * (`mandate.canonical` + `mandate.signature`) it spends through
4
4
  * `POST /v1/consumer-payments/execute`; without it, by mandate id through
5
- * `POST /v1/consumers/mandates/{id}/spend`. Both are idempotent on
6
- * `attempt_id`. The by-id route answered `bad_signature` on staging for a
5
+ * `POST /v1/consumers/mandates/{id}/spend`. Idempotence on both is OPT-IN
6
+ * and keyed on an explicit `attempt_id`, which every payment here carries: a
7
+ * spend without one is a fresh payment on every request (ent#1671, #1683).
8
+ * What a repeat of an attempt answers is in `lookup` below. The by-id route answered `bad_signature` on staging for a
7
9
  * mandate carrying `periodic_cap` (OPEN_QUESTIONS §14), which is why the
8
10
  * envelope is preferred when it exists. Receipts come back from
9
11
  * `GET /v1/consumers/receipts/{id}`.
@@ -14,19 +16,19 @@
14
16
  */
15
17
  import type { ApiClient } from "@codespar/sdk";
16
18
  import type { Mandate } from "../mandate.js";
17
- import type { PaymentRail, RailLookup, RailOutcome, RailPayment, RailReceipt } from "../rail.js";
19
+ import { checkQuote } from "../quote.js";
20
+ import { checkSpendApproval, type PaymentRail, type RailLookup, type RailOutcome, type RailPayment, type RailReceipt } from "../rail.js";
18
21
  import type { Actor } from "../types.js";
19
- import { describeApiError, isUncertain } from "./client.js";
22
+ import { describeApiError, isUncertain, type SpendErrorCode } from "./client.js";
20
23
 
21
- /** The asymmetric seal as the API serves it, or nulls when this deployment
22
- * does not serve one. Never throws on a shape it does not recognise: a
23
- * receipt is evidence of a payment that already happened, and refusing to
24
- * record it over an unexpected field would trade the record for the proof. */
25
- function readEd25519Seal(body: unknown): { receipt_sig_ed25519: string | null; receipt_sig_kid: string | null } {
26
- const r = body as { receipt_sig_ed25519?: unknown; receipt_sig_kid?: unknown };
27
- const str = (v: unknown): string | null => (typeof v === "string" && v.length > 0 ? v : null);
28
- return { receipt_sig_ed25519: str(r.receipt_sig_ed25519), receipt_sig_kid: str(r.receipt_sig_kid) };
29
- }
24
+ /** Another presentation of this attempt is claimed and has no recorded outcome yet: it may be moving money right now. */
25
+ const ATTEMPT_IN_FLIGHT: SpendErrorCode = "psp_attempt_in_flight";
26
+ /** This attempt failed, moved no money and was compensated; the API will never run this id again. */
27
+ const ATTEMPT_SPENT: SpendErrorCode = "psp_attempt_conflict";
28
+ /** This attempt id was already used for a DIFFERENT payment (checked before its state); nothing was held or sent. */
29
+ const ATTEMPT_ID_CONFLICT: SpendErrorCode = "attempt_id_conflict";
30
+ /** Another project of this organization holds this attempt id; opaque by design, nothing was held or sent. */
31
+ const ATTEMPT_ID_UNAVAILABLE: SpendErrorCode = "attempt_id_unavailable";
30
32
 
31
33
  export class CodeSparRail implements PaymentRail {
32
34
  readonly name = "codespar" as const;
@@ -37,6 +39,12 @@ export class CodeSparRail implements PaymentRail {
37
39
  ) {}
38
40
 
39
41
  async pay(payment: RailPayment): Promise<RailOutcome> {
42
+ // Without the quote the sealed receipt names no payee (OPEN_QUESTIONS §18), and the API pays a quote that disagrees. Neither goes out.
43
+ const quoted = checkQuote(payment);
44
+ if (!quoted.ok) return { status: "failed", code: quoted.code, message: `${quoted.detail}; nothing was sent` };
45
+ // Without it the receipt seals no link to what was approved (OPEN_QUESTIONS §3), and the API refuses a malformed one with 400 anyway.
46
+ const approval = checkSpendApproval(payment);
47
+ if (!approval.ok) return { status: "failed", code: approval.code, message: `${approval.detail}; nothing was sent` };
40
48
  try {
41
49
  const outcome =
42
50
  this.envelope?.canonical && this.envelope.signature
@@ -49,11 +57,13 @@ export class CodeSparRail implements PaymentRail {
49
57
  agent_id: payment.agent_id,
50
58
  payee: payment.payee,
51
59
  attempt_id: payment.attempt_id,
60
+ quote: quoted.quote,
61
+ approval: approval.approval,
52
62
  },
53
63
  })
54
64
  : await this.api.post("/v1/consumers/mandates/{id}/spend", {
55
65
  path: { id: payment.mandate_id },
56
- body: { amount_minor: payment.amount_minor, payee: payment.payee, agent_id: payment.agent_id, attempt_id: payment.attempt_id },
66
+ body: { amount_minor: payment.amount_minor, payee: payment.payee, agent_id: payment.agent_id, attempt_id: payment.attempt_id, quote: quoted.quote, approval: approval.approval },
57
67
  });
58
68
  return {
59
69
  status: "settled",
@@ -61,24 +71,50 @@ export class CodeSparRail implements PaymentRail {
61
71
  receipt_id: outcome.receipt?.id ?? null,
62
72
  money_moved: outcome.payment.moneyMoved,
63
73
  sandbox: !outcome.payment.moneyMoved,
74
+ ...(outcome.idempotent_replay ? { replayed: true as const } : {}),
64
75
  raw: outcome,
65
76
  };
66
77
  } catch (err) {
67
78
  const failure = describeApiError(err);
79
+ // Not a refusal: the other presentation may already have reached the provider, so "nothing moved" would be a claim nobody can make.
80
+ if (failure.code === ATTEMPT_IN_FLIGHT) return { status: "uncertain", code: failure.code, message: failure.message };
68
81
  if (isUncertain(failure)) return { status: "uncertain", code: failure.code, message: failure.message };
82
+ if (failure.code === ATTEMPT_SPENT) return { status: "failed", code: failure.code, message: failure.message, spent: true };
83
+ // Held, never spent: the id belongs to another payment or another project, so no generation may be derived past it.
84
+ if (failure.code === ATTEMPT_ID_CONFLICT) return { status: "failed", code: failure.code, message: failure.message, held: "conflict" };
85
+ if (failure.code === ATTEMPT_ID_UNAVAILABLE) return { status: "failed", code: failure.code, message: failure.message, held: "unavailable" };
69
86
  return { status: "failed", code: failure.code, message: failure.message };
70
87
  }
71
88
  }
72
89
 
73
90
  /**
74
- * There is no read route for an attempt. The documented way is to present
75
- * the same `attempt_id` again: the lifecycle is idempotent on it and
76
- * answers the state it reached. `psp_attempt_in_flight` means the first
77
- * presentation is still running.
91
+ * There is no read route for an attempt: the way to learn what became of
92
+ * one is to present the same `attempt_id`, with the same payment, again.
93
+ * Against the deployed API (ent#1671, #1683) that call never moves money
94
+ * for an attempt it already holds, and each answer maps to one reading:
95
+ *
96
+ * - settled: `200` with the ORIGINAL body verbatim (same `transactionId`,
97
+ * same stored `receipt.id`, which the receipt route resolves) plus
98
+ * `idempotent_replay: true`. Read as `settled`, exactly as the first
99
+ * answer, and marked `replayed`.
100
+ * - `psp_attempt_in_flight` (409): claimed, no outcome yet, on every rail.
101
+ * Read as `in_flight`; the next reconcile asks again with the SAME id.
102
+ * - `psp_attempt_uncertain` (409): pinned, dispatch outcome unknown. Read as
103
+ * `uncertain`, which leaves the execution open for a person.
104
+ * - `psp_attempt_conflict` (409): failed, compensated, no money. Read as
105
+ * `failed` and `spent`.
106
+ * - `attempt_id_conflict` / `attempt_id_unavailable` (409): the id is held for
107
+ * a different payment, or by another project. Read as `failed` and `held`,
108
+ * never `spent`: nothing was held or sent by this call. Neither can come from re-presenting the
109
+ * payment this execution sent, whose tuple is the one the id was claimed
110
+ * with, so seeing one here is a defect worth its readable failure.
111
+ * - an attempt the API never took: there is no record to answer from, so
112
+ * this call IS the payment. Reconcile runs only after the outbox row
113
+ * flipped to `sent`, so an attempt it looks up was presented once already.
78
114
  */
79
115
  async lookup(_attemptId: string, payment: RailPayment): Promise<RailLookup> {
80
116
  const outcome = await this.pay(payment);
81
- if (outcome.status === "failed" && outcome.code === "psp_attempt_in_flight") return { status: "in_flight" };
117
+ if (outcome.status === "uncertain" && outcome.code === ATTEMPT_IN_FLIGHT) return { status: "in_flight" };
82
118
  return outcome;
83
119
  }
84
120
 
@@ -88,7 +124,9 @@ export class CodeSparRail implements PaymentRail {
88
124
  return {
89
125
  receipt_id: r.receipt_id,
90
126
  state: r.state,
91
- mandate: { id: r.mandate.id },
127
+ mandate: { id: r.mandate.id, sig_sha256: r.mandate.sig_sha256 },
128
+ chain_version: r.chain_version,
129
+ approval: r.approval ?? null,
92
130
  payment: {
93
131
  amount_minor: r.payment.amount_minor,
94
132
  payee: r.quote?.payee ?? null,
@@ -99,12 +137,10 @@ export class CodeSparRail implements PaymentRail {
99
137
  },
100
138
  chain: r.chain,
101
139
  receipt_sig: r.receipt_sig,
102
- // ent#1633 landed after `@codespar/sdk@0.16.6` was generated, so the
103
- // typed response does not carry these two yet. Read off the body
104
- // defensively rather than pinning a new SDK for two nullable strings:
105
- // a deployment that predates the change answers null, which is the
106
- // same answer the columns hold and reads as `unsigned`.
107
- ...readEd25519Seal(r),
140
+ // `||`, not `??`: a deployment older than ent#1633 omits both fields,
141
+ // and an empty string is no signature either. Both read as `unsigned`.
142
+ receipt_sig_ed25519: r.receipt_sig_ed25519 || null,
143
+ receipt_sig_kid: r.receipt_sig_kid || null,
108
144
  actor,
109
145
  raw: r,
110
146
  };
@@ -12,27 +12,12 @@
12
12
  * `fetch`, see docs/OPEN_QUESTIONS.md section 31c. The key is checked for
13
13
  * the `csk_test_` prefix before a client exists, like every other call.
14
14
  */
15
- import { ApiClient } from "@codespar/sdk";
15
+ import { ApiClient, type ApiOperation, type ApiSuccess } from "@codespar/sdk";
16
16
  import type { ApiFailure } from "./client.js";
17
17
  import { createCodeSparClient, describeApiError } from "./client.js";
18
18
 
19
- export interface SandboxPaidState {
20
- charge_id: string;
21
- status: "paid";
22
- local_status: string;
23
- currency: string;
24
- quoted_minor: number;
25
- paid_minor: number;
26
- payment: "full" | "partial" | "over";
27
- paid_via: string;
28
- wallet_id: string;
29
- ledger_entry_id: string | null;
30
- event: { id: string; type: string } | null;
31
- simulated: boolean;
32
- settled_against: string | null;
33
- money_moved: false;
34
- idempotent_replay: boolean;
35
- }
19
+ /** What the payer route answers, as the SDK's generated OpenAPI types it. */
20
+ export type SandboxPaidState = ApiSuccess<ApiOperation<"/v1/test/charges/{chargeId}/pay", "post">>;
36
21
 
37
22
  export type SandboxPayResult = { ok: true; state: SandboxPaidState } | { ok: false; failure: ApiFailure };
38
23
 
@@ -13,7 +13,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
13
13
  import { dirname, join } from "node:path";
14
14
  import { canonicalJson, hexEqual, hmacSha256Hex, itemsHash } from "./hash.js";
15
15
  import type { Execution } from "./state-machine.js";
16
- import type { Actor, ApprovalArtifact, EscalationTrigger, ExecutionBatch } from "./types.js";
16
+ import type { Actor, ApprovalArtifact, EscalationTrigger, ExecutionBatch, ExecutionComposition } from "./types.js";
17
17
  import { newId } from "./ids.js";
18
18
 
19
19
  export const APPROVAL_KEY_ID = "local-dev-stub";
@@ -78,6 +78,8 @@ export function createApprovalArtifact(signer: ApprovalSigner, input: CreateAppr
78
78
  // null — on an execution that is not part of a batch, so the payload a
79
79
  // bills-agent artifact signs is unchanged by this field existing.
80
80
  ...(execution.batch ? { batch: execution.batch } : {}),
81
+ // What the single amount was composed of, when it was composed of lines. Same rule as `batch`: omitted, never null.
82
+ ...(execution.composition ? { composition: execution.composition } : {}),
81
83
  ...(input.escalation ? { escalation: input.escalation } : {}),
82
84
  actor: input.actor,
83
85
  };
@@ -89,7 +91,7 @@ export function createApprovalArtifact(signer: ApprovalSigner, input: CreateAppr
89
91
 
90
92
  export type ApprovalCheck =
91
93
  | { ok: true }
92
- | { ok: false; problem: "signature_invalid" | "items_hash_mismatch" | "expired" | "wrong_execution" | "wrong_mandate" | "batch_mismatch" };
94
+ | { ok: false; problem: "signature_invalid" | "items_hash_mismatch" | "expired" | "wrong_execution" | "wrong_mandate" | "batch_mismatch" | "composition_mismatch" };
93
95
 
94
96
  /** Two batch bindings are the same binding, or one is absent and so is the other. */
95
97
  function sameBatch(a: ExecutionBatch | undefined, b: ExecutionBatch | undefined): boolean {
@@ -97,6 +99,12 @@ function sameBatch(a: ExecutionBatch | undefined, b: ExecutionBatch | undefined)
97
99
  return a.ref === b.ref && a.batch_hash === b.batch_hash && a.index === b.index && a.count === b.count;
98
100
  }
99
101
 
102
+ /** Two compositions are the same binding, or one is absent and so is the other. */
103
+ function sameComposition(a: ExecutionComposition | undefined, b: ExecutionComposition | undefined): boolean {
104
+ if (a === undefined || b === undefined) return a === b;
105
+ return a.ref === b.ref && a.composition_hash === b.composition_hash && a.line_count === b.line_count;
106
+ }
107
+
100
108
  /**
101
109
  * The rule that makes the artifact proof: before `executing`, recompute the
102
110
  * hash of what is about to be executed and compare it with the artifact.
@@ -118,6 +126,10 @@ export function checkApprovalArtifact(
118
126
  // line was one OF. An execution that gained, lost or moved its place in a
119
127
  // batch after approval is not the one that was approved.
120
128
  if (!sameBatch(artifact.batch, execution.batch)) return { ok: false, problem: "batch_mismatch" };
129
+ // The item's amount is attested above; this is what that amount was made of.
130
+ // A cart recomposed at a constant total leaves `items_hash` unchanged and
131
+ // this hash changed, and that is the only thing that sees it.
132
+ if (!sameComposition(artifact.composition, execution.composition)) return { ok: false, problem: "composition_mismatch" };
121
133
  if (new Date(artifact.expires_at).getTime() <= now.getTime()) return { ok: false, problem: "expired" };
122
134
  return { ok: true };
123
135
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { basename, join } from "node:path";
8
+ import { canonicalJson } from "./hash.js";
8
9
  import type { Mandate } from "./mandate.js";
9
10
  import type { RailReceipt } from "./rail.js";
10
11
  import type { ApprovalArtifact } from "./types.js";
@@ -97,11 +98,18 @@ export class ProofBundle {
97
98
  writeFileSync(join(this.dir, "mandate.snapshot.json"), JSON.stringify({ ...safe, beneficiaries: safe.beneficiaries.map((b) => ({ ...b, payee: maskPayee(b.payee) })), merchant_allowlist: safe.merchant_allowlist.map(maskPayee) }, null, 2) + "\n");
98
99
  }
99
100
 
100
- /** Returns the path INSIDE the bundle (`receipts/<id>.json`): a bundle travels, and an absolute path names the machine that wrote it. */
101
- receipt(receipt: RailReceipt): string {
101
+ /**
102
+ * Returns the path INSIDE the bundle (`receipts/<id>.json`): a bundle travels, and an absolute path names the machine that wrote it.
103
+ *
104
+ * `approval_id` names the artifact in `approval.json` whose hashes the spend carried, so `npm run verify` holds the receipt's sealed
105
+ * approval link against THAT artifact, found by identity and never by looking for one whose hash happens to match. The copy stays
106
+ * masked: binding its body to the chain needs the unmasked read, which `verify --from-api` takes from the API and writes nowhere.
107
+ */
108
+ receipt(receipt: RailReceipt, context: { approval_id?: string | undefined } = {}): string {
102
109
  const relative = join("receipts", `${receipt.receipt_id}.json`);
103
110
  const { raw: _raw, ...safe } = receipt;
104
- writeFileSync(join(this.dir, relative), JSON.stringify({ ...safe, payment: { ...safe.payment, payee: safe.payment.payee ? maskPayee(safe.payment.payee) : null } }, null, 2) + "\n");
111
+ const copy = { ...safe, payment: { ...safe.payment, payee: safe.payment.payee ? maskPayee(safe.payment.payee) : null }, ...(context.approval_id ? { approval_id: context.approval_id } : {}) };
112
+ writeFileSync(join(this.dir, relative), JSON.stringify(copy, null, 2) + "\n");
105
113
  return relative;
106
114
  }
107
115
 
@@ -149,6 +157,30 @@ function readdirSorted(dir: string): string[] {
149
157
  .sort();
150
158
  }
151
159
 
160
+ /**
161
+ * Where the bundle's masked copy of a receipt says something the API's read of the same receipt does not. The copy is not what the
162
+ * signature covers, so a copy edited after the fact still verifies at the signature level; this is what catches it once the read is
163
+ * in hand. The payee is compared masked, the only form the copy holds. An empty list means the copy is a faithful mask of the read.
164
+ */
165
+ export function copyDisagreesWithRead(copy: Record<string, unknown>, read: Record<string, unknown>): string[] {
166
+ const field = (o: unknown, k: string): unknown => (o && typeof o === "object" ? (o as Record<string, unknown>)[k] : undefined);
167
+ const readPayee = field(read["quote"], "payee");
168
+ const expected: Array<[string, unknown, unknown]> = [
169
+ ["receipt_id", copy["receipt_id"], read["receipt_id"]],
170
+ ["chain", copy["chain"], read["chain"]],
171
+ ["mandate.id", field(copy["mandate"], "id"), field(read["mandate"], "id")],
172
+ ["payment.amount_minor", field(copy["payment"], "amount_minor"), field(read["payment"], "amount_minor")],
173
+ ["payment.attempt_id", field(copy["payment"], "attempt_id"), field(read["payment"], "attempt_id")],
174
+ ["payment.money_moved", field(copy["payment"], "money_moved"), field(read["payment"], "money_moved")],
175
+ ["payment.at", field(copy["payment"], "at"), field(read["payment"], "at")],
176
+ ["payment.payee", field(copy["payment"], "payee"), typeof readPayee === "string" ? maskPayee(readPayee) : null],
177
+ ];
178
+ // Written onto the copy since the rail reads them through the SDK's types (0.16.10); a copy from before carries neither, and is not faulted for it.
179
+ if ("chain_version" in copy) expected.push(["chain_version", copy["chain_version"], read["chain_version"]]);
180
+ if ("approval" in copy) expected.push(["approval", canonicalJson(copy["approval"] ?? null), canonicalJson(read["approval"] ?? null)]);
181
+ return expected.filter(([, a, b]) => a !== b).map(([name]) => name);
182
+ }
183
+
152
184
  /** `escola@exemplo.com.br` -> `es***@exemplo.com.br`; `+5511999998888` -> `+55***8888`; a UUID keeps its first and last 4. */
153
185
  export function maskPayee(payee: string): string {
154
186
  if (payee === "*") return payee;
@@ -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}`);