@runcofactor/cli 0.57.0 → 0.58.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 (248) hide show
  1. package/README.md +40 -0
  2. package/dist/{account-S7VHLHYX.js → account-CWSADXJD.js} +5 -5
  3. package/dist/{alert-DZUI3JEU.js → alert-VEZZJPIH.js} +8 -8
  4. package/dist/{api-catalog-WP6AQYUG.js → api-catalog-OBE6ITCJ.js} +5 -5
  5. package/dist/{api-key-U57EC2YF.js → api-key-265DBPSR.js} +5 -5
  6. package/dist/{attachment-MICVVXUW.js → attachment-QE65VNDG.js} +5 -5
  7. package/dist/{auth-UDD77PG6.js → auth-DG6RD4UA.js} +3 -3
  8. package/dist/{benchmark-Q5BVGQ3I.js → benchmark-3R75GENH.js} +5 -5
  9. package/dist/billing-AGY6VUCA.js +64 -0
  10. package/dist/billing-AGY6VUCA.js.map +1 -0
  11. package/dist/{blackcat-TZNSDQAC.js → blackcat-EKZS3TBD.js} +3 -3
  12. package/dist/{chunk-AHS2HMEH.js → chunk-4KFJQPJF.js} +3 -3
  13. package/dist/{chunk-LP4X7TWI.js → chunk-56WV7CFM.js} +2 -2
  14. package/dist/{chunk-3EKIGWMN.js → chunk-A6VDLUMH.js} +3 -3
  15. package/dist/{chunk-XQZJWDM4.js → chunk-B7G4TOYC.js} +5 -5
  16. package/dist/{chunk-U3MXJTTD.js → chunk-BDPNJKUZ.js} +10 -10
  17. package/dist/{chunk-IIDQM4MY.js → chunk-BMFD4OPC.js} +2 -2
  18. package/dist/{chunk-EYHJ5FIA.js → chunk-C6SNLHVE.js} +10 -1
  19. package/dist/chunk-C6SNLHVE.js.map +1 -0
  20. package/dist/{chunk-T5MU76RA.js → chunk-C77XIO2W.js} +4 -4
  21. package/dist/{chunk-FBEQNW2P.js → chunk-DFN2NH37.js} +2 -2
  22. package/dist/{chunk-4SQKKD3R.js → chunk-EDX47DXT.js} +2 -2
  23. package/dist/{chunk-BSCEOIYX.js → chunk-FQ6P522C.js} +2 -2
  24. package/dist/{chunk-6IMNRYC3.js → chunk-GQSW6ZY3.js} +5 -5
  25. package/dist/{chunk-4GDEKB5S.js → chunk-GVNSCSIC.js} +4 -4
  26. package/dist/{chunk-W4P6BGJ7.js → chunk-IN3AF6LI.js} +4 -4
  27. package/dist/{chunk-CZOYFKTY.js → chunk-IVJU3LP6.js} +3 -3
  28. package/dist/{chunk-H2K3ABPO.js → chunk-K54XL64A.js} +6 -6
  29. package/dist/{chunk-T6ZNIMI4.js → chunk-LRIP5IQD.js} +3 -3
  30. package/dist/chunk-LRIP5IQD.js.map +1 -0
  31. package/dist/{chunk-T6KTBHWD.js → chunk-LZLSRN5Q.js} +6 -6
  32. package/dist/{chunk-YILSW5ZY.js → chunk-MCJOF3JX.js} +4 -4
  33. package/dist/{chunk-SBQLD7X6.js → chunk-PN5IK7CJ.js} +2 -2
  34. package/dist/{chunk-K3MG2ZMH.js → chunk-RIBSXEAM.js} +2 -2
  35. package/dist/{chunk-DTB5HUD2.js → chunk-SPTLZJOF.js} +5 -4
  36. package/dist/chunk-SPTLZJOF.js.map +1 -0
  37. package/dist/{chunk-K2USFSTX.js → chunk-T7NZHHC5.js} +3 -3
  38. package/dist/{chunk-5MFHNT56.js → chunk-TWPCN4DV.js} +2 -2
  39. package/dist/chunk-UPP4F2BE.js +519 -0
  40. package/dist/chunk-UPP4F2BE.js.map +1 -0
  41. package/dist/{chunk-KD7VGZX7.js → chunk-UZZ5OHXT.js} +7 -7
  42. package/dist/{chunk-N55RKCGR.js → chunk-V22CGGHO.js} +6 -1
  43. package/dist/chunk-V22CGGHO.js.map +1 -0
  44. package/dist/{chunk-YFXTMFYB.js → chunk-V6OC2WQL.js} +5 -5
  45. package/dist/{chunk-YV4RASGX.js → chunk-W43YGZOP.js} +18 -6
  46. package/dist/chunk-W43YGZOP.js.map +1 -0
  47. package/dist/{chunk-TG32WC4J.js → chunk-W6TPKE5S.js} +2 -2
  48. package/dist/{chunk-72Y65NWI.js → chunk-WY4R4IL5.js} +3 -3
  49. package/dist/{chunk-XGGHOHZS.js → chunk-XD3PSGFG.js} +3 -3
  50. package/dist/chunk-XD3PSGFG.js.map +1 -0
  51. package/dist/{chunk-ETXIGTS7.js → chunk-XKR275VJ.js} +3 -3
  52. package/dist/{credential-HM4THJOS.js → credential-DPUCDGYC.js} +5 -5
  53. package/dist/{credential-template-JBV3FZBY.js → credential-template-67RQWMH2.js} +5 -5
  54. package/dist/{dev-D2HS5XXN.js → dev-MU45HWAN.js} +456 -170
  55. package/dist/dev-MU45HWAN.js.map +1 -0
  56. package/dist/{digest-54VJ7KTF.js → digest-4PABSK2F.js} +8 -8
  57. package/dist/{docs-PPQSNRP6.js → docs-ZL5QOHCB.js} +5 -5
  58. package/dist/{doctor-CXFGSPWH.js → doctor-J5X2HSLJ.js} +3 -3
  59. package/dist/{environment-6MXRQV5L.js → environment-7Q6WDKMM.js} +7 -7
  60. package/dist/{eval-HCDXL4QT.js → eval-37P36M53.js} +51 -358
  61. package/dist/eval-37P36M53.js.map +1 -0
  62. package/dist/{exploration-U2WMYBZ3.js → exploration-7PHVC753.js} +5 -5
  63. package/dist/{exploratory-test-FD7VGUKE.js → exploratory-test-NE7ECFWS.js} +6 -6
  64. package/dist/{external-run-THSD26B7.js → external-run-KZGHVSHY.js} +134 -11
  65. package/dist/external-run-KZGHVSHY.js.map +1 -0
  66. package/dist/{feature-flag-D5IZ6OEG.js → feature-flag-YFWCPWIR.js} +5 -5
  67. package/dist/{feedback-6YCQMQQ3.js → feedback-HVR7LMFL.js} +6 -6
  68. package/dist/{flow-OVEA7273.js → flow-M6QBA4WR.js} +5 -5
  69. package/dist/{github-L2AFIEVN.js → github-HUBAWWFS.js} +6 -6
  70. package/dist/index.js +87 -80
  71. package/dist/index.js.map +1 -1
  72. package/dist/{init-ADOE2D6R.js → init-5A7VDZU4.js} +5 -5
  73. package/dist/{install-SB6NLXDQ.js → install-HM3USHVU.js} +5 -5
  74. package/dist/{issues-V2DSZXAN.js → issues-W6WIQ4RX.js} +8 -8
  75. package/dist/{knobs-MR4H55EB.js → knobs-NR52HEAL.js} +5 -5
  76. package/dist/{landing-KTYDL7QJ.js → landing-FHE7RWCR.js} +7 -7
  77. package/dist/{list-DFBHWU77.js → list-6J6BCU2L.js} +5 -5
  78. package/dist/{local-QXSTGDFN.js → local-DNRUTJBN.js} +7 -7
  79. package/dist/{login-KGIZAZCR.js → login-K7CNO4QS.js} +5 -5
  80. package/dist/{mcp-EDYHUOFI.js → mcp-G72SEYO5.js} +10 -10
  81. package/dist/mcp-eval-X4AGPFWW.js +450 -0
  82. package/dist/mcp-eval-X4AGPFWW.js.map +1 -0
  83. package/dist/{metrics-JUONV7EL.js → metrics-WXQPVDQY.js} +5 -5
  84. package/dist/{monitor-F7F7EJAL.js → monitor-URPOJOER.js} +5 -5
  85. package/dist/{perf-L2YL4Z2L.js → perf-WG6DAZBT.js} +11 -11
  86. package/dist/{product-UBLNRF5Y.js → product-TGOCFPCC.js} +5 -5
  87. package/dist/{product-map-7K5I5TVU.js → product-map-PKDPF2FB.js} +7 -7
  88. package/dist/{prompts-3JNN4M46.js → prompts-63KKMXLZ.js} +5 -5
  89. package/dist/{proofs-MJ7MGBCW.js → proofs-XRQ3BMM2.js} +9 -9
  90. package/dist/{property-TCDBREZL.js → property-O3PMHQLJ.js} +6 -6
  91. package/dist/{psql-YD54GQMP.js → psql-SUE2TORV.js} +5 -5
  92. package/dist/{record-OWBURAZ4.js → record-C245ESQA.js} +6 -6
  93. package/dist/{redis-3SIKQULP.js → redis-CAOX2ODS.js} +5 -5
  94. package/dist/{release-DMCBRVA5.js → release-ALUGJZTN.js} +5 -5
  95. package/dist/{runbook-ZRKWCLZE.js → runbook-63HBD7WH.js} +5 -5
  96. package/dist/{sandbox-HMZR6SKM.js → sandbox-IC5E53CC.js} +9 -9
  97. package/dist/{scenarios-2QHT5A4F.js → scenarios-JR2H7SZC.js} +5 -5
  98. package/dist/{schedule-4L7LOUDB.js → schedule-FHMCSKPL.js} +8 -8
  99. package/dist/{session-JHLYECKM.js → session-BKHVVZS3.js} +9 -9
  100. package/dist/{skill-ARJSVWCZ.js → skill-TZE27TIN.js} +3 -3
  101. package/dist/{slack-T7CR4SAU.js → slack-3BQ2T3ZS.js} +6 -6
  102. package/dist/{spec-RDNTNUE2.js → spec-PCAK2BX5.js} +5 -5
  103. package/dist/{src-DJETLMXP.js → src-BCJZMHSQ.js} +3 -3
  104. package/dist/{src-H7IBLPDW.js → src-BPKBSXKX.js} +4 -4
  105. package/dist/{src-DPJGVNYG.js → src-PNV4HGUY.js} +3 -3
  106. package/dist/{src-UKPMEXXT.js → src-ZJGBNWYB.js} +5 -5
  107. package/dist/{start-NR7W2LIP.js → start-BA5UEI3W.js} +5 -5
  108. package/dist/{status-UDNXR5GK.js → status-TUXK57H4.js} +5 -5
  109. package/dist/{tag-ADP2MZ3V.js → tag-IBDEJZM7.js} +6 -6
  110. package/dist/{task-KQOUTIJU.js → task-WXZQM4BW.js} +6 -6
  111. package/dist/{teams-D4QXSVDA.js → teams-656T2IHZ.js} +7 -7
  112. package/dist/{test-suite-DYMY4YAG.js → test-suite-ETXWE5HB.js} +12 -12
  113. package/dist/{tickets-BX3LFWUS.js → tickets-LWP3XIHG.js} +5 -5
  114. package/dist/{trace-MBRU6DLE.js → trace-RNEY6XCJ.js} +8 -8
  115. package/dist/{triage-IXAU73VL.js → triage-VZIIN7KB.js} +6 -6
  116. package/dist/{trigger-CHSWCUV3.js → trigger-KTTSNSRU.js} +10 -10
  117. package/dist/{update-check-LLETICM4.js → update-check-SLGKNOUC.js} +4 -4
  118. package/dist/{upgrade-35WVNUEG.js → upgrade-2KSYA4GE.js} +5 -5
  119. package/dist/{usage-YUYXUNF2.js → usage-SC4NWQ5A.js} +99 -14
  120. package/dist/usage-SC4NWQ5A.js.map +1 -0
  121. package/dist/{utilities-MOHIKBBF.js → utilities-O4VVADHM.js} +6 -6
  122. package/dist/{visual-RNYERKX4.js → visual-ZWQTVXTM.js} +5 -5
  123. package/dist/{workflow-NM5IC7ZQ.js → workflow-7HKA2WKO.js} +30 -30
  124. package/dist/{workflow-cache-JNN7BS2K.js → workflow-cache-FDN2N5HH.js} +18 -18
  125. package/dist/{workflow-cache-convergence-resolve-4SO2MP7T.js → workflow-cache-convergence-resolve-BGEUM7WV.js} +5 -5
  126. package/dist/{workflow-debug-E7BXSSOG.js → workflow-debug-KNE7SVXQ.js} +11 -11
  127. package/dist/{workflow-feedback-EWAK45AB.js → workflow-feedback-YY2FZTV4.js} +5 -5
  128. package/dist/{workflow-fork-NO5IFBD4.js → workflow-fork-FH5KBVWP.js} +5 -5
  129. package/dist/{workflow-promotion-NWIZNAR4.js → workflow-promotion-RRI53FB6.js} +5 -5
  130. package/dist/{workflow-proposals-XMAZJT3X.js → workflow-proposals-5XGCOTEU.js} +5 -5
  131. package/dist/{workflow-revisions-4EQMLZGY.js → workflow-revisions-M6WPGXAT.js} +5 -5
  132. package/dist/{workflow-status-AZMXQZLC.js → workflow-status-I5BSSWHY.js} +5 -5
  133. package/package.json +2 -2
  134. package/dist/chunk-DTB5HUD2.js.map +0 -1
  135. package/dist/chunk-EYHJ5FIA.js.map +0 -1
  136. package/dist/chunk-N55RKCGR.js.map +0 -1
  137. package/dist/chunk-T6ZNIMI4.js.map +0 -1
  138. package/dist/chunk-XGGHOHZS.js.map +0 -1
  139. package/dist/chunk-YV4RASGX.js.map +0 -1
  140. package/dist/dev-D2HS5XXN.js.map +0 -1
  141. package/dist/eval-HCDXL4QT.js.map +0 -1
  142. package/dist/external-run-THSD26B7.js.map +0 -1
  143. package/dist/mcp-eval-YT2RFIC5.js +0 -196
  144. package/dist/mcp-eval-YT2RFIC5.js.map +0 -1
  145. package/dist/usage-YUYXUNF2.js.map +0 -1
  146. /package/dist/{account-S7VHLHYX.js.map → account-CWSADXJD.js.map} +0 -0
  147. /package/dist/{alert-DZUI3JEU.js.map → alert-VEZZJPIH.js.map} +0 -0
  148. /package/dist/{api-catalog-WP6AQYUG.js.map → api-catalog-OBE6ITCJ.js.map} +0 -0
  149. /package/dist/{api-key-U57EC2YF.js.map → api-key-265DBPSR.js.map} +0 -0
  150. /package/dist/{attachment-MICVVXUW.js.map → attachment-QE65VNDG.js.map} +0 -0
  151. /package/dist/{auth-UDD77PG6.js.map → auth-DG6RD4UA.js.map} +0 -0
  152. /package/dist/{benchmark-Q5BVGQ3I.js.map → benchmark-3R75GENH.js.map} +0 -0
  153. /package/dist/{blackcat-TZNSDQAC.js.map → blackcat-EKZS3TBD.js.map} +0 -0
  154. /package/dist/{chunk-AHS2HMEH.js.map → chunk-4KFJQPJF.js.map} +0 -0
  155. /package/dist/{chunk-LP4X7TWI.js.map → chunk-56WV7CFM.js.map} +0 -0
  156. /package/dist/{chunk-3EKIGWMN.js.map → chunk-A6VDLUMH.js.map} +0 -0
  157. /package/dist/{chunk-XQZJWDM4.js.map → chunk-B7G4TOYC.js.map} +0 -0
  158. /package/dist/{chunk-U3MXJTTD.js.map → chunk-BDPNJKUZ.js.map} +0 -0
  159. /package/dist/{chunk-IIDQM4MY.js.map → chunk-BMFD4OPC.js.map} +0 -0
  160. /package/dist/{chunk-T5MU76RA.js.map → chunk-C77XIO2W.js.map} +0 -0
  161. /package/dist/{chunk-FBEQNW2P.js.map → chunk-DFN2NH37.js.map} +0 -0
  162. /package/dist/{chunk-4SQKKD3R.js.map → chunk-EDX47DXT.js.map} +0 -0
  163. /package/dist/{chunk-BSCEOIYX.js.map → chunk-FQ6P522C.js.map} +0 -0
  164. /package/dist/{chunk-6IMNRYC3.js.map → chunk-GQSW6ZY3.js.map} +0 -0
  165. /package/dist/{chunk-4GDEKB5S.js.map → chunk-GVNSCSIC.js.map} +0 -0
  166. /package/dist/{chunk-W4P6BGJ7.js.map → chunk-IN3AF6LI.js.map} +0 -0
  167. /package/dist/{chunk-CZOYFKTY.js.map → chunk-IVJU3LP6.js.map} +0 -0
  168. /package/dist/{chunk-H2K3ABPO.js.map → chunk-K54XL64A.js.map} +0 -0
  169. /package/dist/{chunk-T6KTBHWD.js.map → chunk-LZLSRN5Q.js.map} +0 -0
  170. /package/dist/{chunk-YILSW5ZY.js.map → chunk-MCJOF3JX.js.map} +0 -0
  171. /package/dist/{chunk-SBQLD7X6.js.map → chunk-PN5IK7CJ.js.map} +0 -0
  172. /package/dist/{chunk-K3MG2ZMH.js.map → chunk-RIBSXEAM.js.map} +0 -0
  173. /package/dist/{chunk-K2USFSTX.js.map → chunk-T7NZHHC5.js.map} +0 -0
  174. /package/dist/{chunk-5MFHNT56.js.map → chunk-TWPCN4DV.js.map} +0 -0
  175. /package/dist/{chunk-KD7VGZX7.js.map → chunk-UZZ5OHXT.js.map} +0 -0
  176. /package/dist/{chunk-YFXTMFYB.js.map → chunk-V6OC2WQL.js.map} +0 -0
  177. /package/dist/{chunk-TG32WC4J.js.map → chunk-W6TPKE5S.js.map} +0 -0
  178. /package/dist/{chunk-72Y65NWI.js.map → chunk-WY4R4IL5.js.map} +0 -0
  179. /package/dist/{chunk-ETXIGTS7.js.map → chunk-XKR275VJ.js.map} +0 -0
  180. /package/dist/{credential-HM4THJOS.js.map → credential-DPUCDGYC.js.map} +0 -0
  181. /package/dist/{credential-template-JBV3FZBY.js.map → credential-template-67RQWMH2.js.map} +0 -0
  182. /package/dist/{digest-54VJ7KTF.js.map → digest-4PABSK2F.js.map} +0 -0
  183. /package/dist/{docs-PPQSNRP6.js.map → docs-ZL5QOHCB.js.map} +0 -0
  184. /package/dist/{doctor-CXFGSPWH.js.map → doctor-J5X2HSLJ.js.map} +0 -0
  185. /package/dist/{environment-6MXRQV5L.js.map → environment-7Q6WDKMM.js.map} +0 -0
  186. /package/dist/{exploration-U2WMYBZ3.js.map → exploration-7PHVC753.js.map} +0 -0
  187. /package/dist/{exploratory-test-FD7VGUKE.js.map → exploratory-test-NE7ECFWS.js.map} +0 -0
  188. /package/dist/{feature-flag-D5IZ6OEG.js.map → feature-flag-YFWCPWIR.js.map} +0 -0
  189. /package/dist/{feedback-6YCQMQQ3.js.map → feedback-HVR7LMFL.js.map} +0 -0
  190. /package/dist/{flow-OVEA7273.js.map → flow-M6QBA4WR.js.map} +0 -0
  191. /package/dist/{github-L2AFIEVN.js.map → github-HUBAWWFS.js.map} +0 -0
  192. /package/dist/{init-ADOE2D6R.js.map → init-5A7VDZU4.js.map} +0 -0
  193. /package/dist/{install-SB6NLXDQ.js.map → install-HM3USHVU.js.map} +0 -0
  194. /package/dist/{issues-V2DSZXAN.js.map → issues-W6WIQ4RX.js.map} +0 -0
  195. /package/dist/{knobs-MR4H55EB.js.map → knobs-NR52HEAL.js.map} +0 -0
  196. /package/dist/{landing-KTYDL7QJ.js.map → landing-FHE7RWCR.js.map} +0 -0
  197. /package/dist/{list-DFBHWU77.js.map → list-6J6BCU2L.js.map} +0 -0
  198. /package/dist/{local-QXSTGDFN.js.map → local-DNRUTJBN.js.map} +0 -0
  199. /package/dist/{login-KGIZAZCR.js.map → login-K7CNO4QS.js.map} +0 -0
  200. /package/dist/{mcp-EDYHUOFI.js.map → mcp-G72SEYO5.js.map} +0 -0
  201. /package/dist/{metrics-JUONV7EL.js.map → metrics-WXQPVDQY.js.map} +0 -0
  202. /package/dist/{monitor-F7F7EJAL.js.map → monitor-URPOJOER.js.map} +0 -0
  203. /package/dist/{perf-L2YL4Z2L.js.map → perf-WG6DAZBT.js.map} +0 -0
  204. /package/dist/{product-UBLNRF5Y.js.map → product-TGOCFPCC.js.map} +0 -0
  205. /package/dist/{product-map-7K5I5TVU.js.map → product-map-PKDPF2FB.js.map} +0 -0
  206. /package/dist/{prompts-3JNN4M46.js.map → prompts-63KKMXLZ.js.map} +0 -0
  207. /package/dist/{proofs-MJ7MGBCW.js.map → proofs-XRQ3BMM2.js.map} +0 -0
  208. /package/dist/{property-TCDBREZL.js.map → property-O3PMHQLJ.js.map} +0 -0
  209. /package/dist/{psql-YD54GQMP.js.map → psql-SUE2TORV.js.map} +0 -0
  210. /package/dist/{record-OWBURAZ4.js.map → record-C245ESQA.js.map} +0 -0
  211. /package/dist/{redis-3SIKQULP.js.map → redis-CAOX2ODS.js.map} +0 -0
  212. /package/dist/{release-DMCBRVA5.js.map → release-ALUGJZTN.js.map} +0 -0
  213. /package/dist/{runbook-ZRKWCLZE.js.map → runbook-63HBD7WH.js.map} +0 -0
  214. /package/dist/{sandbox-HMZR6SKM.js.map → sandbox-IC5E53CC.js.map} +0 -0
  215. /package/dist/{scenarios-2QHT5A4F.js.map → scenarios-JR2H7SZC.js.map} +0 -0
  216. /package/dist/{schedule-4L7LOUDB.js.map → schedule-FHMCSKPL.js.map} +0 -0
  217. /package/dist/{session-JHLYECKM.js.map → session-BKHVVZS3.js.map} +0 -0
  218. /package/dist/{skill-ARJSVWCZ.js.map → skill-TZE27TIN.js.map} +0 -0
  219. /package/dist/{slack-T7CR4SAU.js.map → slack-3BQ2T3ZS.js.map} +0 -0
  220. /package/dist/{spec-RDNTNUE2.js.map → spec-PCAK2BX5.js.map} +0 -0
  221. /package/dist/{src-DJETLMXP.js.map → src-BCJZMHSQ.js.map} +0 -0
  222. /package/dist/{src-H7IBLPDW.js.map → src-BPKBSXKX.js.map} +0 -0
  223. /package/dist/{src-DPJGVNYG.js.map → src-PNV4HGUY.js.map} +0 -0
  224. /package/dist/{src-UKPMEXXT.js.map → src-ZJGBNWYB.js.map} +0 -0
  225. /package/dist/{start-NR7W2LIP.js.map → start-BA5UEI3W.js.map} +0 -0
  226. /package/dist/{status-UDNXR5GK.js.map → status-TUXK57H4.js.map} +0 -0
  227. /package/dist/{tag-ADP2MZ3V.js.map → tag-IBDEJZM7.js.map} +0 -0
  228. /package/dist/{task-KQOUTIJU.js.map → task-WXZQM4BW.js.map} +0 -0
  229. /package/dist/{teams-D4QXSVDA.js.map → teams-656T2IHZ.js.map} +0 -0
  230. /package/dist/{test-suite-DYMY4YAG.js.map → test-suite-ETXWE5HB.js.map} +0 -0
  231. /package/dist/{tickets-BX3LFWUS.js.map → tickets-LWP3XIHG.js.map} +0 -0
  232. /package/dist/{trace-MBRU6DLE.js.map → trace-RNEY6XCJ.js.map} +0 -0
  233. /package/dist/{triage-IXAU73VL.js.map → triage-VZIIN7KB.js.map} +0 -0
  234. /package/dist/{trigger-CHSWCUV3.js.map → trigger-KTTSNSRU.js.map} +0 -0
  235. /package/dist/{update-check-LLETICM4.js.map → update-check-SLGKNOUC.js.map} +0 -0
  236. /package/dist/{upgrade-35WVNUEG.js.map → upgrade-2KSYA4GE.js.map} +0 -0
  237. /package/dist/{utilities-MOHIKBBF.js.map → utilities-O4VVADHM.js.map} +0 -0
  238. /package/dist/{visual-RNYERKX4.js.map → visual-ZWQTVXTM.js.map} +0 -0
  239. /package/dist/{workflow-NM5IC7ZQ.js.map → workflow-7HKA2WKO.js.map} +0 -0
  240. /package/dist/{workflow-cache-JNN7BS2K.js.map → workflow-cache-FDN2N5HH.js.map} +0 -0
  241. /package/dist/{workflow-cache-convergence-resolve-4SO2MP7T.js.map → workflow-cache-convergence-resolve-BGEUM7WV.js.map} +0 -0
  242. /package/dist/{workflow-debug-E7BXSSOG.js.map → workflow-debug-KNE7SVXQ.js.map} +0 -0
  243. /package/dist/{workflow-feedback-EWAK45AB.js.map → workflow-feedback-YY2FZTV4.js.map} +0 -0
  244. /package/dist/{workflow-fork-NO5IFBD4.js.map → workflow-fork-FH5KBVWP.js.map} +0 -0
  245. /package/dist/{workflow-promotion-NWIZNAR4.js.map → workflow-promotion-RRI53FB6.js.map} +0 -0
  246. /package/dist/{workflow-proposals-XMAZJT3X.js.map → workflow-proposals-5XGCOTEU.js.map} +0 -0
  247. /package/dist/{workflow-revisions-4EQMLZGY.js.map → workflow-revisions-M6WPGXAT.js.map} +0 -0
  248. /package/dist/{workflow-status-AZMXQZLC.js.map → workflow-status-I5BSSWHY.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../content/src/sandbox-v2-guide/support.ts","../../content/src/sandbox-v2-guide/overview.txt","../../content/src/sandbox-v2-guide/feasibility.txt","../../content/src/sandbox-v2-guide/definition.txt","../../content/src/sandbox-v2-guide/environments.txt","../../content/src/sandbox-v2-guide/seeds-and-actions.txt","../../content/src/sandbox-v2-guide/data-fast-forward.txt","../../content/src/sandbox-v2-guide/personas.txt","../../content/src/sandbox-v2-guide/authentication.txt","../../content/src/sandbox-v2-guide/env.txt","../../content/src/sandbox-v2-guide/secrets.txt","../../content/src/sandbox-v2-guide/emulation.txt","../../content/src/sandbox-v2-guide/object-storage.txt","../../content/src/sandbox-v2-guide/ports.txt","../../content/src/sandbox-v2-guide/verify.txt","../../content/src/sandbox-v2-guide/cli.txt","../../content/src/sandbox-v2-guide/upload.txt","../../content/src/sandbox-v2-guide/local.txt","../../content/src/sandbox-v2-guide/index.ts","../../content/src/cli-version-filter.ts","../../content/src/dev-guide/setup.txt","../../content/src/dev-guide/index.ts","../../content/src/product-map-guide/voice.txt","../../content/src/product-map-guide/structure.txt","../../content/src/product-map-guide/naming.txt","../../content/src/product-map-guide/bodies.txt","../../content/src/product-map-guide/codes.txt","../../content/src/product-map-guide/coverage.txt","../../content/src/product-map-guide/asking-well.txt","../../content/src/product-map-guide/testability.txt","../../content/src/product-map-guide/personas.txt","../../content/src/product-map-guide/index.ts","../../content/src/workflow-guide/overview.txt","../../content/src/workflow-guide/node-types.txt","../../content/src/workflow-guide/spec-grammar.txt","../../content/src/workflow-guide/action-nodes.txt","../../content/src/workflow-guide/playbook.txt","../../content/src/workflow-guide/best-practices.txt","../../content/src/workflow-guide/macros.txt","../../content/src/workflow-guide/seed-workflows.txt","../../content/src/workflow-guide/seed-users.txt","../../content/src/workflow-guide/data-lifecycle.txt","../../content/src/workflow-guide/assertions.txt","../../content/src/workflow-guide/api-sequences.txt","../../content/src/workflow-guide/cli-seeds.txt","../../content/src/workflow-guide/cli-api-sequences.txt","../../content/src/workflow-guide/triage-results.txt","../../content/src/workflow-guide/write-a-test.txt","../../content/src/workflow-guide/visual-checkpoints.txt","../../content/src/workflow-guide/index.ts","../../content/src/guides.ts","../src/guide.ts"],"sourcesContent":["/**\n * What a Sandboxes v2 sandbox supports: one row per dependency an app may\n * have, with the sandbox's verdict and what to do about it. The guide's\n * `feasibility` topic renders it (its verdict legend is in feasibility.txt).\n *\n * @module\n */\n\ntype SupportVerdict = 'built-in' | 'emulated' | 'native' | 'real' | 'unsupported';\n\ninterface SupportEntry {\n area: string;\n item: string;\n verdict: SupportVerdict;\n /** What to do, naming the guide topic that explains it. */\n how: string;\n}\n\nexport const SANDBOX_V2_SUPPORT_MATRIX: readonly SupportEntry[] = [\n // Databases\n {\n area: 'Databases',\n item: 'Postgres',\n verdict: 'native',\n how: 'Install it in `setup` (or the image) and run it as a service. The only engine whose restored dump `database.fast-forward` can move forward: declare it under `databases` (`data-fast-forward`).',\n },\n {\n area: 'Databases',\n item: 'MySQL / MariaDB',\n verdict: 'native',\n how: 'Run it as a service. No `database.fast-forward`: a restored dump keeps its original dates.',\n },\n { area: 'Databases', item: 'SQLite', verdict: 'native', how: 'A file in the checkout or image; nothing to run.' },\n {\n area: 'Databases',\n item: 'MongoDB',\n verdict: 'native',\n how: 'Run `mongod` as a service; size `resources` for its memory.',\n },\n { area: 'Databases', item: 'Redis / Valkey', verdict: 'native', how: 'Run it as a service.' },\n {\n area: 'Databases',\n item: 'Elasticsearch / OpenSearch',\n verdict: 'native',\n how: 'Run it as a service and give it 1-2 GiB of memory; check it is healthy in a claimed copy.',\n },\n {\n area: 'Databases',\n item: 'DynamoDB',\n verdict: 'native',\n how: 'Run DynamoDB Local (needs Java) as a service and set `AWS_ENDPOINT_URL_DYNAMODB`, a region and dummy credentials in `env`.',\n },\n {\n area: 'Databases',\n item: 'Managed-only data services (Aurora Data API, Snowflake, BigQuery)',\n verdict: 'real',\n how: 'No local equivalent: calls reach the real service. Use a test account through `secrets`, or leave those journeys out.',\n },\n\n // Queues and events\n {\n area: 'Queues and events',\n item: 'Amazon SQS',\n verdict: 'real',\n how: 'Not emulated: calls reach real AWS (a dummy key fails, a real one reaches your account). Run an SQS-compatible server such as ElasticMQ (a JVM image or native binary) as a service with `AWS_ENDPOINT_URL_SQS`, or leave those journeys out.',\n },\n {\n area: 'Queues and events',\n item: 'Amazon SNS',\n verdict: 'real',\n how: 'Not emulated: calls reach real AWS, and no stand-in is named for it: ask the user, who can leave those journeys out or name an SNS-compatible server to run as a service.',\n },\n {\n area: 'Queues and events',\n item: 'Kafka / Redpanda',\n verdict: 'native',\n how: 'Run it as a service (Redpanda is a single binary); check it comes back healthy in a claimed copy.',\n },\n {\n area: 'Queues and events',\n item: 'RabbitMQ',\n verdict: 'native',\n how: 'Run it as a service. Erlang binds to the node name: check it comes back healthy in a claimed copy.',\n },\n { area: 'Queues and events', item: 'NATS, Redis streams', verdict: 'native', how: 'Run it as a service.' },\n {\n area: 'Queues and events',\n item: 'Google Pub/Sub',\n verdict: 'native',\n how: 'Run the Pub/Sub emulator (needs Java) as a service and set `PUBSUB_EMULATOR_HOST` in `env`.',\n },\n {\n area: 'Queues and events',\n item: 'Kinesis, Step Functions',\n verdict: 'real',\n how: 'Not emulated: calls reach real AWS. Run kinesalite (Kinesis, unmaintained) as a service and point the app at it, or leave those journeys out. Step Functions Local is no longer maintained by AWS and calls real Lambda for Task states: ask the user before relying on it.',\n },\n {\n area: 'Queues and events',\n item: 'Azure Service Bus, Amazon EventBridge',\n verdict: 'real',\n how: 'Not emulated: calls reach the real service, and no stand-in is named for them: ask the user, who can leave those journeys out or stub the calls in the app.',\n },\n\n // Functions\n {\n area: 'Functions',\n item: 'AWS Lambda',\n verdict: 'real',\n how: 'Not emulated: invokes reach real AWS. No stand-in is named for it: ask the user, who can leave those journeys out or have the app call the handler code as a service directly (a change to its code path).',\n },\n {\n area: 'Functions',\n item: \"The app's own HTTP API served by API Gateway and Lambda\",\n verdict: 'unsupported',\n how: \"Ask the user: serve the app's HTTP API from its own server process in the sandbox, or leave those journeys out.\",\n },\n {\n area: 'Functions',\n item: 'Google Cloud Functions / Azure Functions',\n verdict: 'native',\n how: 'Run the Functions Framework or Core Tools as a service; cloud triggers are not delivered, so call the function over HTTP.',\n },\n\n // Object storage\n {\n area: 'Object storage',\n item: 'Amazon S3, Google Cloud Storage',\n verdict: 'built-in',\n how: 'Declare `object_storage` and its buckets; the sandbox runs the server (`object-storage`).',\n },\n { area: 'Object storage', item: 'Azure Blob Storage', verdict: 'native', how: 'Run Azurite as a service.' },\n\n // Secrets and config\n {\n area: 'Secrets and config',\n item: 'AWS Secrets Manager, KMS, SSM Parameter Store',\n verdict: 'emulated',\n how: \"Declare them under `emulation.aws`; the app's unmodified SDK reads the seeded values (`emulation`).\",\n },\n {\n area: 'Secrets and config',\n item: 'Vault, Doppler, 1Password',\n verdict: 'real',\n how: 'Calls reach the real service. The stand-in is sandbox secrets: set the values as secrets and read them from the environment (`secrets`); ask the user if the app must call the service itself.',\n },\n\n // Email\n {\n area: 'Email',\n item: 'SES, SendGrid, Resend, Postmark, Mailgun (HTTP APIs); Mailosaur reads',\n verdict: 'emulated',\n how: 'Declare `emulation.email`; sent mail is captured and readable. SES is intercepted in us-east-1 unless you list its regions (`emulation`).',\n },\n {\n area: 'Email',\n item: 'SMTP',\n verdict: 'native',\n how: \"Not intercepted: run an SMTP catcher such as Mailpit as a service and point the app's SMTP host at 127.0.0.1 (`emulation`).\",\n },\n\n // SMS and voice\n {\n area: 'SMS and voice',\n item: 'Twilio, Vonage',\n verdict: 'real',\n how: 'Calls reach the real provider: set its test credentials as secrets (`secrets`).',\n },\n\n // Sign-in\n {\n area: 'Sign-in',\n item: 'Password or session sign-in',\n verdict: 'built-in',\n how: 'Declare personas and an `authentication` step (`personas`, `authentication`).',\n },\n {\n area: 'Sign-in',\n item: 'Google sign-in',\n verdict: 'emulated',\n how: 'Declare `emulation.google-oauth` (`authentication`).',\n },\n {\n area: 'Sign-in',\n item: 'OIDC providers (Auth0, Okta, Cognito, Keycloak, Clerk)',\n verdict: 'emulated',\n how: 'Declare `emulation.oidc` (`authentication`).',\n },\n {\n area: 'Sign-in',\n item: 'Firebase Auth',\n verdict: 'native',\n how: 'Run the Firebase Auth emulator (needs Java) as a service.',\n },\n {\n area: 'Sign-in',\n item: 'SAML, Google One Tap / FedCM / popup sign-in, hosted identity intermediaries',\n verdict: 'unsupported',\n how: 'Sign personas in another way the app supports (a password or session login), or leave the sign-in journey itself out.',\n },\n\n // Third-party APIs\n {\n area: 'Third-party APIs',\n item: 'Jira, Slack, Esri, Maximo',\n verdict: 'emulated',\n how: 'Declare `emulation.jira`, `emulation.slack`, `emulation.esri` or `emulation.maximo-stub` (`emulation`).',\n },\n {\n area: 'Third-party APIs',\n item: 'APIs with a test mode (Stripe, OpenAI, ...)',\n verdict: 'real',\n how: 'Calls reach the real provider: set its test key as a secret, never a live one (`secrets`).',\n },\n\n // Platform\n {\n area: 'Platform',\n item: 'Background workers and crons',\n verdict: 'native',\n how: 'Run each as another service.',\n },\n {\n area: 'Platform',\n item: 'Docker, compose or Kubernetes inside the sandbox',\n verdict: 'unsupported',\n how: \"A sandbox is one machine with no Docker: run each container's process as a service (`upload`).\",\n },\n {\n area: 'Platform',\n item: 'arm64-only images',\n verdict: 'unsupported',\n how: 'Sandboxes are linux/amd64: build for that platform (`upload`).',\n },\n {\n area: 'Platform',\n item: 'Windows, .NET Framework, IIS',\n verdict: 'unsupported',\n how: 'Linux only. .NET (Core) on Linux runs as a service.',\n },\n {\n area: 'Platform',\n item: 'GPU; more than 32 vCPUs or 64 GiB of memory',\n verdict: 'unsupported',\n how: '`resources` allows 1-32 vCPUs and 1-64 GiB, with no GPU (`definition`).',\n },\n];\n\n/** The verdict column (the longest verdict plus a gap) and the guide's line width. */\nconst VERDICT_WIDTH = 'unsupported'.length + 2;\nconst LINE_WIDTH = 74;\n\n/** The matrix as the guide prints it: one block per area, one entry per item, wrapped to the guide's width. */\nexport function renderSupportMatrix(): string {\n const areas = [...new Set(SANDBOX_V2_SUPPORT_MATRIX.map((entry) => entry.area))];\n return areas\n .map((area) => {\n const rows = SANDBOX_V2_SUPPORT_MATRIX.filter((entry) => entry.area === area).map((entry) =>\n [\n ...wrap(entry.item, ` ${entry.verdict.padEnd(VERDICT_WIDTH)}`),\n ...wrap(entry.how, ' '.repeat(VERDICT_WIDTH + 2)),\n ].join('\\n'),\n );\n return `${area.toUpperCase()}\\n\\n${rows.join('\\n')}`;\n })\n .join('\\n\\n');\n}\n\n/** `text` as lines of at most LINE_WIDTH, the first starting with `first`, the rest indented to match. */\nfunction wrap(text: string, first: string): string[] {\n const indent = ' '.repeat(first.length);\n const lines: string[] = [];\n let line = first;\n for (const word of text.split(' ')) {\n if (line.length > indent.length && line.length + word.length + 1 > LINE_WIDTH) {\n lines.push(line);\n line = indent + word;\n } else {\n line += line.length > indent.length ? ` ${word}` : word;\n }\n }\n lines.push(line);\n return lines;\n}\n","A Sandboxes v2 sandbox is a VM restored from a stack of snapshot layers,\nnot a container stood up from scratch each time. That's what makes claims\nfast: the work (installing the toolchain and dependencies, building the\napp, seeding its data) happens once per commit and environment and is\nbaked into a snapshot, and every claim restores its own copy.\n\nEvery example in this guide uses one invented app: a project-tracking\nSaaS with workspaces, their members (roles: owner, member, viewer),\nprojects and tasks, a REST API with cookie sessions, and a Postgres\ndatabase. Substitute your own app's objects, roles and endpoints.\n\nSETTING UP A NEW APPLICATION: FEASIBILITY FIRST\n\n The goal is a hermetic sandbox: the app and everything it depends on\n (its databases, queues, workers and third-party services, as services,\n built-ins or emulators) run inside the sandbox, seeded and reproducible\n at every commit. Only the calls the app has to make to a real provider\n (see the `real` verdict) leave it. A hermetic sandbox is what the agent\n builds, without asking, unless it isn't feasible or the user says\n otherwise.\n\n Before writing a cofactor.sandbox.yaml, check what the app needs against\n what a sandbox supports (`cofactor sandbox guide feasibility`): list its\n databases, queues, functions, third-party services, sign-in and platform\n needs, give each its verdict, tell the user the gaps, and stop and ask\n about anything unsupported (the topic lists when else to stop, and\n when a procedure may batch its questions). Only if a hermetic sandbox\n isn't feasible, or the user asks for it, does the same topic's escape\n hatch apply: a frontend-only sandbox pointed at an existing\n non-production backend,\n explained to the user with its pros and cons before it is built. Then\n write the definition (`definition`), validate it (`cofactor sandbox\n validate`), connect or push it, claim a sandbox and check its logins\n (`verify`). The steps for a GitHub repository, from the first claim on a\n local files or a branch through proof before merge, are in `cli`, SETTING UP A NEW GITHUB APPLICATION.\n\n For a GitHub-delivered application, never build or run the app in local\n Docker (or any local container) to try a definition. A sandbox is a VM\n built from the definition, not that container, so a local build proves\n nothing about it and costs minutes. `validate` is the instant local\n check; a claim is the real test. (A pushed application's image is built\n locally by design: see `upload`.)\n\n[cli >= 0.56.0]\n Setup and proof do not require a merge. Connect with --at <branch-sha>,\n or claim --from-local to use local files without a user commit. Iterate\n with sync --build, then verify-env --at the final source SHA. Merge only\n when the user wants the definition on the default branch.\n[/cli]\n\nLAYERS\n\n platform Cofactor's sandbox helper, which runs your services,\n prepare commands and actions. Rebuilt only when the\n helper's contract with Cofactor changes.\n\n toolchain `setup` from cofactor.sandbox.yaml, plus `resources` and the\n declared `ports`. Rebuilt only when those change — this is\n the expensive layer (apt packages, language runtimes,\n database clusters).\n\n repo base The repository checked out and built at a commit:\n `lifecycle.build` steps, each rerun only when the git\n pathspecs in its `inputs` changed since the layer this one\n is stacked on. This is what makes \"the same commit, again\"\n nearly free.\n\n ready One commit and one environment: the repo base moved to the\n snapshot commit, prepared, and the environment's seeds run — then\n captured while its services are running. Built once, the\n first time anything asks for that commit and environment;\n every claim is a copy of it.\n\nA GitHub-delivered sandbox is identified by **repository + definition\npath**, not by repository alone, so a monorepo can hold several (see\nbelow). Pushed images have no repository or build layers: the image is\nthe base, and each push gets its own ready snapshots (see the `upload`\ntopic).\n\nTHE CLAIM LIFECYCLE\n\n 1. snapshot The claim needs the ready snapshot for its commit and\n environment. If there isn't one yet, it is built first\n (once, however many claims are waiting on it):\n - the nearest layer (toolchain or repo base) is\n restored;\n - the outstanding `lifecycle.build` steps bring it to\n the commit (only the steps whose `inputs` changed);\n - `prepare` runs (see step 3);\n - the environment's seed chain runs, in order (parent's\n seeds before its own — see the `environments` topic);\n each step is an action or an emulator operation,\n and its output can be referenced by later steps;\n - the VM is captured with its services running.\n 2. restore A copy of the ready snapshot is restored. Nothing is\n built or seeded: the copy starts exactly where the\n snapshot left off, data included.\n 3. identity A copy whose port params are all live, with stored snapshot\n prepare results, receives public port URLs through one\n copy-values exec. The sandbox materializes\n /var/lib/cofactor/live-env.json for the\n running app; lifecycle.prepare does NOT run. There is no\n service restart, emulator reconfiguration or bucket creation.\n Secrets, persona passwords, emulator token/config and seeded\n storage stay exactly as captured. A user claim receives its\n private sync key by a separate exec, never in the copy-values\n exec or its file. Non-user copies cannot be synced.\n Legacy prepare runs instead when any port param is non-live,\n an env key mixes live/non-live params, or an emulator config\n references a port param; it keeps per-copy prepare and restart.\n Pinned PR-preview captures without stored prepare results\n also keep the existing copy prepare path.\n 4. ready The sandbox reports its port URLs, persona logins, and the\n seed outputs (the same for every copy of the snapshot).\n From here it behaves like a real running instance of the\n app until it's destroyed or its lease expires. A copy is\n never reused.\n\n`cofactor sandbox show` breaks the claim's time down into these phases.\n\nA failure while building the snapshot (a build step, `prepare` or a seed)\nfails the claims waiting on it; the next claim of that commit and\nenvironment builds the snapshot again, so a failure that happens every\ntime needs a fix and a new source version. See the `verify` topic for what to do\nwhen that happens.\n\nA failure of the sandbox platform itself (a worker that is slow to restore,\na capture that is still running) is retried: the snapshot's build tries\nagain, resuming where it stopped, and so does claiming the copy when the\nfleet has no room. Every claim has a time limit — 2 hours unless\n`cofactor sandbox claim --timeout` sets another. A claim not ready by then\nfails with the last problem it met, and a sandbox is never handed over after\nit: a copy still being made at the limit is destroyed when it finishes. That\ncan be up to about 10 minutes of waiting for room plus the copy's prepare\nafter the limit, so the failure may arrive later than the limit itself.\n\nAPPLICATIONS, AND WHERE THEIR CODE COMES FROM\n\n A sandbox is always a sandbox of a Cofactor application. How the\n application's code reaches its sandboxes is its own setting:\n\n - GitHub (`cofactor sandbox connect --application A --repo owner/name\n [--dir D]`): the `cofactor.sandbox.yaml` under `D` at each commit\n installs (`setup`) and builds (`lifecycle.build`) a checkout. A\n monorepo can connect one application per definition directory.\n Commands run **from that definition's directory** — an action's\n `run:` path, build steps and services resolve relative to where the\n `cofactor.sandbox.yaml` lives, not the repo root.\n - Pushed images (`cofactor sandbox push`), for a team that gives\n Cofactor no repository access: see the `upload` topic.\n\n Either way, each environment in the definition is one sandbox\n environment of the application, and each persona one of its personas, so\n test runs on that environment sign in with the sandbox's own logins.\n There is no need to create the application first: `connect` or `push`\n with a new application name creates it, and its sandbox `default`\n environment is its default environment.\n\nHOW A TEST AGENT USES THIS\n\n 1. Browse what's available: `cofactor sandbox envs --application <name>`\n lists the definition's environments, actions, emulator operations,\n ports and personas.\n 2. Claim one: `cofactor sandbox claim --application <name>\n [--environment <name>] --wait`. Without `--environment` you get\n `default`.\n 3. Write the test against the claimed sandbox: drive the app through its\n port URLs, call actions to set up data the test needs\n (`cofactor sandbox run <id> <action>`), and call emulator operations\n to inspect or drive third parties (`cofactor sandbox run <id>\n <plugin.op>`).\n 4. Try uncommitted changes against it: `cofactor sandbox sync <id>`\n writes your working tree into the sandbox, then `cofactor workflow\n run <flow> --sandbox <id>` runs a flow on it (see the `local` topic); `cofactor sandbox claims` lists your live claims.\n 5. Destroy it when done: `cofactor sandbox destroy <id>`.\n","Check what the app needs before you write its cofactor.sandbox.yaml.\n\nTHE GOAL: A HERMETIC SANDBOX\n\n The app and everything it depends on run inside the sandbox: its\n databases, queues and workers as services, object storage and sign-in\n as built-ins, third parties as emulators. Every copy is seeded the same\n way at every commit and has its own data, so a test can't be flaked by\n anything shared, a pull request's backend changes are exercised, and a\n result at a commit means what it says.\n\n Build a hermetic sandbox unless this check shows it isn't feasible or\n the user tells you not to. Don't ask first, and don't offer the\n frontend-only escape hatch below as an equal choice.\n\n A sandbox is one Linux amd64 machine with no Docker inside, open egress,\n and emulators for some third parties. A dependency that can't run there\n is far cheaper to find now than after the first failed snapshot.\n\nTHE CHECK\n\n 1. List the app's dependencies from its own files, not from memory:\n - compose files (each `image:` is a dependency) and Dockerfiles\n (`FROM`: base OS and architecture);\n - package manifests (`package.json`, `requirements.txt`,\n `pyproject.toml`, `Gemfile`, `go.mod`, `*.csproj`; database\n drivers, queue clients, cloud SDKs such as `@aws-sdk/client-*` or\n `boto3`, payment, email and SMS libraries);\n - `.env.example` or the env schema (`*_URL`, `*_QUEUE_URL`,\n `*_TOPIC_ARN`, `AWS_*`, `SMTP_*`, provider keys);\n - infrastructure code (Terraform, CloudFormation or SAM, Serverless,\n CDK) for managed services the code reaches at runtime;\n - how users sign in.\n 2. Give each one its verdict from SUPPORT below:\n built-in Cofactor runs it for you.\n emulated An `emulation` plugin answers it inside the sandbox.\n native Nothing in the sandbox stops it, but Cofactor doesn't\n provide it: install it in `setup` (or the image) and\n run it as one of your `services`. Prove it comes up in\n a claimed copy.\n real The call leaves the sandbox for the real provider.\n Egress is open: a test key reaches the provider's test\n mode, a real credential acts on the real account.\n unsupported No way to run it in a sandbox today.\n Anything not listed is unknown: say so, and say what you would try.\n 3. Report it to the user before building anything: one line per\n dependency with its verdict, the file that shows it, and your plan\n (install it, declare the emulator, set a test key, leave the journey\n out). Put the same list in your final report and in the pull request\n that adds the definition.\n 4. Stop and ask the user when anything is unsupported, unknown on a\n journey they care about, or real with no test mode and no stand-in\n named for it in SUPPORT (a call to it acts on the real service:\n never give the sandbox real credentials for one). A `real` item with\n a stand-in named for it, such as ElasticMQ for SQS, is no blocker:\n run the stand-in as a service and point the app at it. Each blocker\n needs a decision: leave the journey out, disable the feature in the\n sandbox, stub it in the app, sign in another way, or, when the gaps\n make a hermetic sandbox infeasible, the escape hatch below. A\n procedure that batches its questions, such as the onboarding skill,\n may defer these per-item questions to one later question; it still\n stops before building when a hermetic sandbox isn't feasible.\n 5. A gap blocks a hermetic sandbox only when the app can't start or\n users can't sign in without it. A gap confined to some journeys, a\n feature or a test mode takes one of the step 4 decisions instead, and\n the sandbox is still hermetic. If nothing blocks, build it: go on to\n `definition`. If something does, explain to the user why, show them\n the table under HERMETIC OR FRONTEND-ONLY with your recommendation,\n and build only once they decide.\n\n AWS deserves a second look. `emulation.aws` answers only the services\n the definition gives a section, `emulation.email` answers SES, and\n `object_storage` answers S3 for a client that reads the standard\n endpoint settings. Every other AWS SDK client the app creates reaches\n real AWS: list each one in the report.\n\nTHE ESCAPE HATCH: A FRONTEND-ONLY SANDBOX\n\n Only when the check shows a hermetic sandbox isn't feasible (the app\n can't start or users can't sign in without an unsupported dependency:\n step 5), or the user explicitly asks for it, run only\n the frontend in the sandbox and point it at an existing backend: a\n staging or dev API. Never a production backend: tests and seeds create,\n change and delete data there. Ask the user which backend, and have them\n confirm it is not production and holds no real customer data, before you\n write it into the definition.\n\n It needs a backend the public internet can reach. Sandbox egress is\n open, but a backend only reachable over a VPN or a private network, or\n one that only accepts listed IP addresses, can't be reached.\n\n Serve the frontend and proxy the backend from the same origin. Each\n sandbox copy has its own public origin, and the browser sends a\n backend's cookies only to the host they were set for: a frontend on the\n copy's origin calling the backend's own origin is a cross-site call, its\n session cookies are third-party, and the backend's CORS would have to\n allow every copy's origin. So the sandbox's web service serves the\n frontend and forwards one fixed API path to the backend, and the\n browser only ever talks to the copy's own origin. Rewrite only the\n cookie's domain if the backend sets one (nginx `proxy_cookie_domain`, a\n dev server proxy's `cookieDomainRewrite`); keep Secure and SameSite as\n they are, since the copy is https. Hard-code the target: a proxy that\n takes it from the request is an open proxy on a public URL. Never have\n the proxy add a credential of its own (a service token, an\n Authorization header): anyone with the copy's URL would inherit it. The\n frontend's own API base URL becomes that same-origin path, and the\n backend's URL goes to the proxy. Both are fixed values in the\n definition's top-level `env` (with no credentials in the URL): secrets\n never reach build steps, so a bundle that bakes its API URL at build\n time reads it from there. The parts that differ from a hermetic\n definition (add `version`, `resources` and `environments` as in\n `definition`; install nginx and gettext-base in `setup`; for a pushed\n image, put them and the built bundle in the image instead, and leave\n `lifecycle.build` out):\n ports:\n web: { port: 3000, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n API_BASE_URL: /api\n BACKEND_URL: https://api.staging.example.com\n BACKEND_HOST: api.staging.example.com\n services:\n web:\n run: sandbox/serve-web.sh\n health: { http: 3000, path: / }\n after_build: true # starts once the build made the bundle\n reload_on_prepare: true # sees the copy's PUBLIC_URL and secrets\n personas:\n owner: { email: qa-owner@example.com }\n authentication:\n run: sandbox/sign-in.sh\n lifecycle:\n build:\n - { name: frontend, run: npm ci && npm run build, inputs: [package-lock.json, \"src/**\"] }\n prepare: []\n where serve-web.sh renders the proxy config from $BACKEND_URL and\n $BACKEND_HOST with `envsubst` (nginx doesn't read the environment;\n pass envsubst the variable list so nginx's own `$host` is kept), then\n serves the built frontend on 3000 with:\n location /api/ {\n proxy_pass ${BACKEND_URL}/; # the trailing / strips /api\n proxy_ssl_server_name on;\n proxy_cookie_domain ${BACKEND_HOST} $host;\n proxy_redirect ${BACKEND_URL}/ /api/;\n }\n (BACKEND_URL is an origin with no trailing slash or path.)\n An app that sends a bearer token instead of a cookie can call the\n backend directly if the backend's CORS already allows the copy's\n origin. Don't loosen a shared backend's CORS for it (never `*` with\n credentials): use the proxy.\n A backend that checks the Origin header (CSRF protection, an origin\n allowlist) also sees the copy's origin through the proxy: have the\n proxy set Origin to the backend's own, or allow the copy's origin there.\n\n Personas are existing accounts on that backend: dedicated test accounts\n with only the access a test needs, never a person's own account or an\n administrator. The user stores each one's password as a secret from\n their own shell (`read -rs PASSWORD; printf %s \"$PASSWORD\" | cofactor\n sandbox secret set OWNER_PASSWORD --application <app>`), never in the\n definition or a committed file. Give the user that command to run; don't\n run it yourself. Name one secret per persona, `<PERSONA>_PASSWORD` with\n the persona's name upper-cased and `-` turned into `_` (OWNER_PASSWORD\n for `owner`); a secret is 8 to 8192 bytes. The user sets them before the\n first claim (after `push` or `connect` creates the application, since\n neither builds a snapshot): a snapshot built without them keeps failing\n sign-in until a new image push, or a new default-branch commit followed\n by `cofactor sandbox refresh --wait`, replaces it (`secrets`).\n `authentication.run` signs in through $PUBLIC_URL\n (see `authentication`) with the persona's email from COFACTOR_PERSONAS\n and the password from the secret for $COFACTOR_AUTH_PERSONA, which\n reaches the script as an environment variable; have the script stop with\n an error when that variable is empty. In POSIX sh:\n var=\"$(printf %s \"$COFACTOR_AUTH_PERSONA\" | tr 'a-z-' 'A-Z_')_PASSWORD\"\n password=\"$(printenv \"$var\" || true)\"\n [ -n \"$password\" ] || { echo \"$var is not set\" >&2; exit 1; }\n Never use the `password` in COFACTOR_PERSONAS here, as the\n `authentication` example does: it is the snapshot's generated one, which\n the backend doesn't know. This is what lets Cofactor's test runs and\n `cofactor sandbox verify-env` sign in (`validate` doesn't run the\n script). Every test run and agent acts as that account. Leave the\n persona step out of `lifecycle.prepare`: you can't set passwords on a\n shared backend. The password a claim prints for each persona is the\n snapshot's generated one, which that backend doesn't know, so a person\n using a frontend-only sandbox signs in with the account's own password.\n\n Seeds and actions change the shared backend for everyone using it:\n create what a test needs under names a later run can find and remove,\n and never delete or reset data a seed didn't create.\n\n Emulators only cover calls made inside the sandbox. In a frontend-only\n sandbox the backend makes its own third-party calls, from wherever it\n runs.\n\nHERMETIC OR FRONTEND-ONLY\n\n When a hermetic sandbox isn't feasible, show the user this with why and\n what you recommend, and let them decide:\n\n Hermetic Frontend-only\n Setup More work: every dependency Fast: the frontend and a\n runs inside proxy\n Data Every copy has its own, Shared with everyone using\n seeded the same way that backend; tests change\n real data there and need\n cleaning up\n Pull requests Frontend and backend changes Frontend changes only; the\n are both exercised PR's backend changes are not\n Flakiness Nothing shared The backend's uptime, data\n and deploys\n As proof Strong: everything at that Weaker: the backend is\n at a commit commit whatever it runs today\n Credentials Generated for each snapshot Real accounts on that\n backend, kept as secrets\n Backend Inside the sandbox Staging or dev, never\n production\n\nSUPPORT\n\n","Everything about a sandbox lives in one file: `cofactor.sandbox.yaml`, at the\nrepository root or under a definition directory for a monorepo (for pushed\nimages, wherever you keep it — see the `upload` topic). The examples below\nare fragments of one file for the guide's example app: a project tracker\nwith a `web` front end, an `api` server and Postgres.\n`cofactor sandbox validate` checks every field below.\n\n Required: `version`, `resources`, `ports`, `params`, `env`, `services`,\n `lifecycle` and `environments`; write `{}` for one you don't use (e.g.\n `params: {}`, `lifecycle: { prepare: [] }`). `personas`, `actions`, `emulation`, `object_storage`\n and `databases` may be left out.\n Names of ports, services, personas, actions, environments and plugins\n are lowercase letters, digits and `-` (up to 32 characters, starting\n with a letter); a seed step's `id` is lowercase letters, digits and `_`\n (it is used in `${<id>.<output>}`, so no `-`).\n\nversion\n Always `1`. The schema version of the file itself.\n\nresources\n The machine every sandbox of this application gets.\n resources: { vcpus: 2, memory_mib: 4096 }\n\nsetup\n (GitHub-delivered only.) A root shell script that installs the\n toolchain: packages, language runtimes, database clusters. Runs once per\n toolchain layer (keyed by this script's text plus `resources` and\n `ports` — see the `overview` topic).\n setup: |\n apt-get update -qq\n apt-get install -y -qq postgresql-16 nodejs npm jq curl >/dev/null\n\nports\n Named ports the app listens on, each becoming its own public URL. Several\n are normal — bundle web and API together so one sandbox serves both UI\n flows and API sequences. `access: public` gets a routable URL; `private`\n stays internal. At most 8 total, including the implicit `sync` port every\n sandbox has (so at most 7 of your own), the `emulation` port added\n automatically when `emulation` is declared, and `object-storage` when\n `object_storage` is — see the `ports` topic.\n ports:\n web: { port: 3000, access: public }\n api: { port: 3001, access: public }\n\nparams\n Per-sandbox values, filled in by `prepare`, referenced from `env` as\n `${params.name}`. Three shapes:\n - a plain string: a fixed value, same in every sandbox.\n - `{port: <name>}`: the sandbox's own public URL for that port.\n `{port: emulation}` is the emulator's public URL, allowed when\n `emulation` is declared (it is the `oidc` plugin's issuer).\n - `{secret: true}`: a random value made when the version's ready\n snapshot is built, so every sandbox of that version and environment\n shares it (e.g. a session-signing key); not unique per claim.\n params:\n web_url: { port: web }\n api_url: { port: api }\n session_secret: { secret: true }\n A port param may be `live: true`: read its resolved env at request/use time\n from `/var/lib/cofactor/live-env.json`, a JSON object of env-name -> value.\n A fast copy receives its URLs through one copy-values exec, which\n keeps this file current using atomic replacement;\n re-read when the file's inode or modification time changes. No private\n Cofactor package is needed by your app.\n Eligible copies with stored snapshot prepare results skip prepare and keep\n services running: lifecycle.prepare does NOT run. They retain baked secrets\n until the snapshot is rebuilt. A copy is eligible for the fast path only\n when every port param is live.\n Any non-live port param keeps the per-copy prepare and service restart.\n An env key mixing a live port param with a non-live param also requires\n prepare, as does an emulation plugin config that references a port param.\n A real third-party secret (a Stripe or OpenAI key) is not a param: see\n the `secrets` topic.\n\nenv\n Environment variables the services, prepare commands and actions run\n with. `${params.name}` is substituted with that param's value. An\n environment can add its own on top (see the `env` topic). A secret of\n the same name (see the `secrets` topic) overrides the `env` entry.\n (`${env.NAME}` is a different, narrower reference: it appears only as\n the whole `url` of a `databases` entry — see the `databases` field.)\n env:\n DATABASE_URL: postgres://app@127.0.0.1:5432/app\n PUBLIC_URL: ${params.web_url}\n API_URL: ${params.api_url}\n SESSION_SECRET: ${params.session_secret}\n\nservices\n Long-running processes. `cofactor-sync` is reserved: every sandbox runs\n it (the sync server, see the `local` topic). Restart a service by hand\n with `cofactor sandbox exec <id> -- cofactor-sandbox restart <name>`.\n services:\n db:\n run: exec sandbox/start-postgres.sh\n health: { command: pg_isready -h 127.0.0.1 }\n api:\n run: cd api && exec npm start\n health: { http: 3001, path: /health }\n after_build: true # don't start until lifecycle.build has produced its output\n reload_on_prepare: true # restarted by prepare, so it sees per-sandbox env\n web:\n run: cd web && exec npm run serve\n health: { http: 3000, path: / }\n after_build: true\n reload_on_prepare: true # reads PUBLIC_URL, API_URL and FEATURE_FLAGS\n\n `health` is either `{ command: <shell> }` (checked by exit code) or\n `{ http: <port>, path: </...> }`. `after_build` services start once\n `lifecycle.build` has run (GitHub-delivered); in a pushed image they start\n at boot, and `sync --build` restarts them after its build steps.\n\npersonas\n Named login identities declared here; each one's password is generated\n when the snapshot is built and shared by its copies (see the `personas` topic). Only `email` is declared\n in the definition.\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nlifecycle\n build Ordered steps. GitHub-delivered: they produce the repo-base\n layer. Pushed image (already built): they run only on\n `cofactor sandbox sync <id> --build`, never at push or claim. Each has `name`, `run` (a shell command) and\n `inputs` (git pathspecs, relative to the definition's\n directory, or the image's WORKDIR) — a step reruns only when the blobs its `inputs`\n match changed, or an earlier step ran.\n lifecycle:\n build:\n - name: install\n run: npm ci\n inputs: [package-lock.json, \"**/package.json\"]\n\n prepare Shell scripts run every time a sandbox is prepared (a snapshot\n build, a legacy copy claim or an in-place move to a new commit), after services with\n `reload_on_prepare` have restarted with per-sandbox values. Each\n param is also available as `COFACTOR_PARAM_<NAME>`.\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n\nactions\n Typed scripts — the one way data gets into a sandbox (seeds, tests,\n `cofactor sandbox run`). See the `seeds-and-actions` topic for\n what makes a good one; the field shapes are:\n actions:\n create-workspace:\n description: A new workspace, created by the given persona, who becomes its owner.\n run: sandbox/actions/create-workspace.sh\n input:\n name: { type: string, required: true }\n as: { type: persona, required: true, description: Who creates it }\n output:\n workspace_id: { type: string }\n timeout: 60 # seconds, defaults to 60, max 1800\n add-member:\n description: Adds a persona to a workspace with a role.\n run: sandbox/actions/add-member.sh\n input:\n workspace_id: { type: string, required: true }\n persona: { type: persona, required: true, description: Who is added }\n role: { type: string, required: true, enum: [owner, member, viewer] }\n as: { type: persona, required: true, description: Who adds them }\n\n Input properties: `type` (string | number | integer | boolean |\n persona — a persona's name, checked against `personas`), `description`,\n `required`, `default`, `enum`, `sensitive` (masked wherever input is\n logged or returned).\n Output properties: `type`, `description`.\n\nemulation\n Plugin id → its config (often `{}`). Declaring any plugin here adds the\n implicit, public, port-80 `emulation` port automatically — you don't\n declare it yourself. See the `emulation` topic for what each plugin\n intercepts. Plugin ids: `aws`, `email`, `google-oauth`, `jira`, `slack`, `oidc`\n (a standard OIDC provider for the app to sign in against; see the\n `authentication` topic).\n emulation:\n email: {}\n\nobject_storage\n An emulated S3 or GCS server with these buckets in every sandbox, its\n standard client env given to everything that runs, and an implicit\n public `object-storage` port. See the `object-storage` topic.\n object_storage: { provider: s3, buckets: [attachments] }\n\ndatabases\n Postgres databases the built-in `database.fast-forward` operation can\n move forward, by name: `engine` (`postgres`), `url` and `as_of` (when\n the dump was captured, UTC with a `Z`). `url` is exactly one whole-string\n `${env.NAME}` reference — nothing before or after it — resolved inside\n the sandbox against the env actions get, including repository secrets.\n See the `data-fast-forward` topic.\n databases:\n app:\n engine: postgres\n url: ${env.DATABASE_URL}\n as_of: 2026-09-30T00:00:00Z\n\nenvironments\n Named flavors of the same sandbox — different starting data and config.\n Required, must include `default`. See the `environments` topic for the\n full model (`extends`, seed steps, `${id.key}` references, `env`).\n environments:\n default:\n description: |\n Workspace \"Acme\", owned by the owner persona. No projects.\n seeds:\n - { id: acme, action: create-workspace, input: { name: Acme, as: owner } }\n\nMULTI-LINE TEXT\n\n YAML `|` keeps line breaks (use it for multi-paragraph `description`s and\n the `setup` script). YAML `>-` folds lines into one string with no\n trailing newline (use it for a single long sentence, such as a long\n action `description`).\n","An environment (a \"flavor\") is the same sandbox — same code, same toolchain\n— started with different data and config. `default` is required; every\ndefinition must declare it. Previews use `default`, and so does `cofactor\nsandbox claim` when you don't pass `--environment`.\n\nWHEN TO ADD A FLAVOR VS. PASSING INPUTS IN THE TEST\n\n Rule of thumb: add a flavor when many tests need the same starting state.\n Call actions from inside the test when only that test needs the data.\n\n In the example app, `default` seeds one workspace with an owner, a\n member and a viewer, because nearly every test signs in to a workspace\n as one of those roles. A test about archiving a project, on the other\n hand, calls `create-project` itself (see the worked example in\n `seeds-and-actions`) rather than getting a new flavor. If you find\n yourself writing a flavor whose seeds only exist to serve one test,\n that data belongs in the test's own action calls instead.\n\n Add a second flavor only when real tests need a starting state `default`\n can't give them cheaply — typically a different plan or set of feature\n flags (see \"WHAT TO BUILD FIRST\" in `seeds-and-actions`). Don't add one\n speculatively.\n\nDESCRIPTIONS ARE FOR THE READER\n\n `description` is required and can be multi-line (YAML `|`). Write it for\n the agent choosing between environments — say what's already there and\n who can do what, not just a label:\n environments:\n default:\n description: |\n Workspace \"Acme\": owner is its owner, member a member,\n viewer a read-only viewer. No projects yet; create them\n with create-project.\n\n A vague description (\"the default setup\") gives an agent nothing to\n decide with. Say what exists, in terms of the ids and roles a test will\n actually use.\n\nEXTENDS\n\n `extends: <other-environment-name>` composes one environment on top of\n another. The child's snapshot runs the **parent's seeds first, then the\n child's own**, in that order. `env` merges the same way: parent's keys\n first, the child's last (so a child can override a parent's value).\n Chains can be more than two deep; a cycle is rejected by `validate`.\n\n Step ids must be **unique across the whole resolved chain** — a parent\n and its child can't reuse the same id, since the child's steps can\n reference the parent's outputs.\n\n An environment carries its own `description`, `extends`, `seeds` and\n `env`. It has no `params` or `personas` of its own: those belong to the\n definition and are the same in every environment.\n\nSEED STEPS AND `${id.key}`\n\n Each `seeds` step has an `id`, exactly one of `action` or `operation`,\n and an `input`:\n - `id`: unique within the resolved chain; later steps reference this\n step's output as `${id.key}`.\n - `action`: a declared action's slug, or `<plugin>.<op>` for an\n emulator operation (e.g. `email.sent-emails`).\n - `operation`: a platform built-in, today only `database.fast-forward`\n (see `data-fast-forward` and `seeds-and-actions`).\n - `input`: literal values, or `${id.key}` references to an earlier\n step's output.\n\n A reference that is the **entire** value keeps the output's real type:\n input: { workspace_id: \"${acme.workspace_id}\" } # exactly acme's workspace_id\n\n A reference **embedded** in a longer string becomes a string\n interpolation instead:\n input: { name: \"Board for ${acme.workspace_id}\" } # becomes a string\n\n `${id.key}` must point at a step that ran earlier in the chain and at one\n of that step's *declared* output keys — `cofactor sandbox validate`\n checks both and reports the path.\n\n The example app's environments, in full:\n environments:\n default:\n description: |\n Workspace \"Acme\": owner is its owner, member a member,\n viewer a read-only viewer. No projects yet.\n seeds:\n - id: acme\n action: create-workspace\n input: { name: Acme, as: owner }\n - id: add_member\n action: add-member\n input: { workspace_id: \"${acme.workspace_id}\", persona: member, role: member, as: owner }\n - id: add_viewer\n action: add-member\n input: { workspace_id: \"${acme.workspace_id}\", persona: viewer, role: viewer, as: owner }\n free-plan:\n extends: default\n description: Same as default, but Acme is moved to the Free plan (3-project limit).\n seeds:\n - id: downgrade\n action: set-plan\n input: { workspace_id: \"${acme.workspace_id}\", plan: free }\n beta-features:\n extends: default\n description: Same as default, with the timeline view turned on.\n env:\n FEATURE_FLAGS: timeline\n\n `add_member` couldn't run before `acme` — its `workspace_id` input\n depends on `acme`'s output. Seeds always run in the order they're\n written. `free-plan` sets its plan with a seed action because the app\n stores plans in its database; `beta-features` uses `env` because the\n app reads its flags from an environment variable (see the `env` topic\n for which processes see it).\n","An action is a script with a typed input and output schema. It is the one\nway data gets into a sandbox — seeds call it, and so do tests and\n`cofactor sandbox run`. Every environment's `seeds` list is\njust an ordered sequence of action (and emulator operation) calls. For\nthe script contract (`INPUT_<NAME>`, the result line), see the `upload`\ntopic's worked example; it is the same for GitHub-delivered apps.\n\nA SEED STEP THAT IS AN OPERATION\n\n A seed step runs exactly one of `action:` (a declared action, or an\n emulator operation named `<plugin>.<op>`) or `operation:` (a platform\n built-in). Today the only built-in is `database.fast-forward`, which\n moves a restored database's dates forward:\n - { id: fast_forward, operation: database.fast-forward, input: { database: app } }\n A step with both keys, or neither, fails `validate`. See the\n `data-fast-forward` topic.\n\nWHAT TO BUILD FIRST\n\n 1. One persona per role your tests need. The example app has three\n roles, so it declares `owner`, `member` and `viewer`. A persona in\n the definition is only a login (prepare makes it work — see\n `personas`); its role comes from a seed action such as `add-member`,\n or from prepare itself when the persona is an account your app\n already has with the right role. With one persona per role, tests can\n sign in as each and check what that role may and may not do.\n\n 2. One environment per plan or feature-flag combination your tests care\n about: `default`, `free-plan`, `beta-features`. Set each with a seed\n action that goes through your app's admin API or database (the\n example's `set-plan`), or with the environment's `env:` when your app\n reads the flag from an environment variable (the example's\n `FEATURE_FLAGS`; see the `env` topic for which processes see it).\n Params are per definition, not per environment, so they can't vary a\n flag between environments. See the `environments` topic.\n\n 3. Actions for the few core objects nearly every test needs, each taking\n `as: <persona>`: in the example, `create-workspace`, `add-member`,\n `create-project`, `create-task`. Keep the shared seeds thin; tests\n create the rest themselves with these actions.\n\n 4. If your app has a database, include a generic `load-sql` action as\n standard sandbox setup for visual-state fixtures. See `cofactor workflow\n guide visual-checkpoints` for the complete example. Each test owns\n its fixture; never create a seed or action for one screen's data.\n\n 5. If your app stores files, declare `object_storage` and seed the few\n files every test needs (a logo, an import template) with an action;\n see the `object-storage` topic.\n\nSEEDS, ACTIONS, AND VISUAL-STATE FIXTURES\n\n First ask: where must this test run? A test that loads SQL is sandbox-only,\n the usual choice for visual regression at an exact commit. If it must also\n run against staging or prod, seed only through the app's API, generic\n API-backed actions, or the test's own UI steps; only a sandbox exposes its\n database. Checkpoint regions independent of exact data, or rely on masking.\n\n Seeds are shared, thin, generic base data: the handful of things nearly\n every test needs before it can start — a workspace and its members, one\n per role. A shared seed stays thin and generic; it does not try to\n anticipate every test's needs.\n\n Actions are typed, parameterized, generic operations, API-first. A test\n about archiving a project calls `create-project` for its own project,\n then operates on the returned id; the operation is shared, its data is not.\n\n Visual-state fixtures are one SQL file per screen state, beside the test,\n never parameterized or shared. Use fixed, neutral values and timestamps\n relative to now() so relative-time text stays stable. Load them through\n the app's generic SQL action, only for rows with no encryption and no\n side effects. Per-screen data never becomes a named seed or action.\n Encrypted rows and side effects go through the app's API or generic\n actions that call it; interaction states use UI steps.\n\nWHAT MAKES A GOOD ACTION\n\n - Typed inputs and outputs. The input schema is what a test (or an\n agent) uses to call it correctly; the output schema is what the next\n step gets to chain from. Untyped free-form data defeats the ability to\n validate `${id.key}` references and CLI input ahead of time.\n - Fast. Seeds run once per version, while its snapshot is built; calls\n run inline in a test — slow seeds slow every new version's first\n claim, and slow calls slow every test.\n - Returns the ids a test needs next. `create-project` returns\n `project_id`, not just \"ok\" — that id is what the test's next action\n or assertion needs.\n - Safe to call more than once, or unique by its own input. Either the\n action is idempotent (`set-plan` re-run with the same plan changes\n nothing), or a distinct call naturally produces a distinct result\n (`create-project` with a new name makes a new project) rather than\n silently colliding with a previous call's data.\n - Explicit about who acts: the `as` persona pattern. Every action that\n performs a real business operation (creating a project, inviting a\n member) takes an explicit `as: <persona>` input naming which persona\n performs it, rather than assuming \"the admin\" or \"whoever is logged\n in.\" The script signs in as exactly that persona (its login is in\n `COFACTOR_PERSONAS`) and acts in the workspace it was given; the\n persona must already have a role there that allows the operation. So\n an action has **no hidden dependency** on some other seed having run\n first to establish who's \"in charge\" — the caller says so, in the\n input.\n - Goes through the app's own API when the data is encrypted or has side\n effects. `invite-user` calls the real invite endpoint so the\n invitation email is actually sent (and can be observed later with\n `email.sent-emails`); a raw insert would silently skip the send. The\n same goes for anything the app encrypts or hashes on write.\n - For business operations, uses the database only when there is no API,\n and only for rows with no encryption and no side effects (e.g. `set-plan`).\n The generic SQL-loading action is the sanctioned DB-direct action for\n visual fixtures, even when an API exists; the same row restrictions apply.\n\nANTI-PATTERNS\n\n - A giant do-everything seed. One seed step that creates a workspace,\n three members, two projects and their tasks in one script is harder to\n reuse, harder to debug when it fails partway, and forces every test\n that wants only part of it to take all of it. Break it into separate\n actions and chain them with `${id.key}`, as `default` does.\n - An action that silently acts as an admin. If an action doesn't take\n `as`, and instead assumes some fixed identity internally, a test\n writer has no way to know (or override) who actually performed the\n operation, and the action can't be reused for a test that needs a\n viewer to attempt (and be denied) the same thing.\n - Hardcoded ids. An action or seed that references a specific workspace\n or project id baked into the script defeats the whole point of\n chaining `${id.key}` — every id a later step needs should come from an\n earlier step's declared output.\n - An action that depends on seed ordering it doesn't declare. A script\n that assumes some other seed already ran (e.g. that a particular\n workspace exists) without that dependency showing up as an input is a\n hidden coupling — if the environment's seed order ever changes, the\n action breaks with no local sign of why. Take everything you need as\n an explicit input instead (as `create-project` takes `workspace_id`\n and `as`).\n - Writing output anywhere but the last `{`-starting stdout line. The\n action's result is exactly the *last* line of stdout that starts with\n `{`. Send every progress message, debugging `echo` and intermediate\n curl response to stderr — printing one to stdout by mistake silently\n corrupts the parsed output (or an earlier `{`-line gets picked up as a\n decoy if you print the wrong one last).\n\nWORKED EXAMPLE: CREATE THEN ARCHIVE A PROJECT\n\n Workspace \"Acme\" already exists (seeded by `default`'s `acme` step); the\n test supplies its own project rather than relying on a seeded one,\n because creating and archiving a project are the test's own subject.\n\n 1. Claim a sandbox on `default` and note `acme`'s output:\n cofactor sandbox claim --application \"Project Tracker\" --wait\n The claim's seed outputs include `acme.workspace_id`.\n\n 2. Create the project the test will operate on, acting as the member:\n cofactor sandbox run <id> create-project \\\n --input workspace_id=<acme.workspace_id> \\\n --input name=Temp \\\n --input as=member\n Returns `{\"project_id\": \"<id>\"}`.\n\n 3. Drive the app (via its `web`/`api` port URLs) to verify the project\n shows up wherever the product surfaces it, using the `project_id`\n from step 2 to find the right row.\n\n 4. Archive it the way the product's UI or API would — this is what the\n test is actually verifying, so it belongs in the test's own steps,\n not in a seed. Repeating it as `viewer` should be refused.\n\n 5. Destroy the sandbox:\n cofactor sandbox destroy <id>\n\n Nothing about the project this test created is shared with any other\n test, and nothing another seed does could break this test's setup.\n","A database dump keeps the dates of the day it was taken. Restore it into a\nsandbox months later and every trial, token and scheduled item in it has\nalready expired, so the app behaves like a dead system. The built-in\noperation `database.fast-forward` moves a restored Postgres database's\ndates forward to the moment the snapshot was built, so the data looks\nfreshly captured. It changes data only; the sandbox's clock is already\ncorrect (see \"WHAT IS NOT FAKED\" below).\n\nDECLARE THE DATABASE\n\n A top-level `databases` block names each database and the date its dump\n was captured. Like every top-level field, it works in GitHub-delivered\n and pushed definitions alike.\n databases:\n app:\n engine: postgres\n url: ${env.DATABASE_URL}\n as_of: 2026-09-30T00:00:00Z\n\n `engine` `postgres` is the only engine today.\n `url` Exactly one `${env.NAME}` reference and nothing else around\n it (no `postgres://${env.HOST}/db`). It is resolved inside the\n sandbox, against the env an action sees: the definition's\n `env` (with `${params.*}` filled in), the environment's `env`,\n the emulators' env and repository secrets. So a `DATABASE_URL` in `env`, or a\n secret of that name, works, and no credential is committed.\n `as_of` When the dump was captured, in UTC with a trailing `Z`. A date\n without a time, or a local offset, is rejected by `validate`.\n\nADD THE STEP AFTER THE RESTORE\n\n A seed step runs the operation with `operation:` instead of `action:`\n (see `seeds-and-actions`). It must come after the step that restores\n the dump:\n environments:\n default:\n description: A customer database restored from a 2026-09-30 dump,\n then moved forward to the snapshot's build time.\n seeds:\n - { id: restore, action: restore-dump }\n - { id: fast_forward, operation: database.fast-forward, input: { database: app } }\n\n `input.database` is a key of `databases`; `validate` reports an unknown\n one with its path. A restore that recreates the database also clears\n the record of earlier fast-forwards (below), which is right: the data is\n old again. A restore that only replaces tables must also run\n `drop schema if exists cofactor cascade`, or a later fast-forward\n starts from the old record and moves nothing (the worked example's\n dump does this).\n\nWHEN IT RUNS IS WHERE YOU PUT IT\n\n There is no mode flag. Placement is the \"when\":\n - As a seed step it runs once, while the version's snapshot is built,\n so every claim of that snapshot starts with shifted data.\n - On a live sandbox, run the same operation by hand:\n cofactor sandbox run <id> database.fast-forward --input database=app\n It shifts from the date the database recorded last time (below), so\n a sandbox kept for days can be brought current again.\n\nTHE SHIFT RULE\n\n - `as_of` is the anchor. The data moves by the whole days between its\n current date and now, rounded down, so it ends at most a day behind\n now and never in the future. Times of day are kept: a row at 14:30\n still reads 14:30.\n - The first run starts from `as_of`. When it finishes it records the\n data's new date in the database itself, in the table\n `cofactor.fast_forward` (its own schema, so your migration tool does\n not see it). Later runs start from that date instead of `as_of`, and\n the record travels with the data into every snapshot and copy.\n - If less than a day has passed the run changes no table (`offset_days:\n 0`). An `as_of` in the future fails.\n\nWHAT IS SHIFTED\n\n Every `timestamp`, `timestamptz` and `date` column of every base table\n in every schema except the system ones and `cofactor`. `infinity`\n values stay `infinity`.\n - A generated column is recomputed from its shifted inputs.\n - A partitioned table is updated through its parent, so rows move\n between partitions as their dates move.\n - User triggers are disabled while a table is shifted, so a trigger\n such as `updated_at = now()` leaves `updated_at` with its shifted\n value. Afterwards every user trigger on a touched table is enabled\n again, including one your dump had deliberately disabled.\n\nWHAT IS NOT SHIFTED\n\n Integer epoch columns, dates inside JSON, dates stored as text, columns\n of a domain over a date type, array columns, and materialized views.\n Refresh materialized views in a step after the fast-forward:\n - { id: refresh, action: refresh-views }\n\nREQUIREMENTS AND FAILURES\n\n - The role in `url` must own the tables (it is normally the role that\n restored the dump). A role that does not fails with Postgres's own\n \"must be owner of table\" error.\n - The whole shift is one transaction. Any error rolls everything back,\n so nothing is half-shifted, and the error names the table (and the\n constraint, when there is one):\n table public.sessions, constraint sessions_day_key: duplicate key ...\n - A unique constraint over a date column can collide while its rows are\n moving; that fails the run. A foreign key whose columns are shifted\n dates is checked as each table moves and can fail the run with\n Postgres's own error.\n - On a live sandbox the shift locks every table that has a date column\n until it commits. Run it when the app is idle.\n - After a large dump, follow the step with an action that runs\n `VACUUM (ANALYZE)`: the update leaves dead rows and stale statistics\n behind.\n\nWHAT IS NOT FAKED\n\n The sandbox's system clock is not changed, and does not need to be: it\n is correct after every claim. Only data moves. So state minted in memory\n while the snapshot was built (sessions, tokens an app created during a\n seed) is as old as the snapshot. A claim's data is as old as its\n snapshot plus under a day of rounding, and a snapshot is rebuilt on\n every push, so keep pushing (a nightly push is plenty). A preview moved\n in place to a new commit keeps its data and is not shifted again.\n\nOUTPUT\n\n The step (and a `sandbox run`) returns:\n database the key in `databases`\n offset_days whole days the data moved this run (0: nothing to do)\n tables, rows how many tables and rows were updated\n from, to the data's date before and after (ISO-8601, UTC)\n A later step can use them, e.g. `${fast_forward.offset_days}`.\n `cofactor sandbox show` lists each seed step with its time and prints\n these under the seed outputs.\n\nWORKED EXAMPLE\n\n `packages/demo-app-postgres/cofactor.sandbox.yaml` in TreebeardHQ/qa is\n a complete Postgres app: a dump from 2026-09-30, a restore action, the\n `databases` block and the two-step seed above, plus an `aged`\n environment that rewinds the data and fast-forwards it a second time.\n","A persona is a named login identity declared in `cofactor.sandbox.yaml`:\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nDeclare one per role your tests need (see \"WHAT TO BUILD FIRST\" in\n`seeds-and-actions`). Only the email is fixed in the definition.\nEverything else about a persona is assigned per sandbox or per seed:\n\nPASSWORDS — SET WHEN THE SNAPSHOT IS BUILT\n\n Each ready snapshot gets a randomly generated password per persona,\n created by the snapshot's own `prepare` (see the `overview` topic); every\n copy claimed from that snapshot has the same passwords. Those passwords\n never appear in the definition — they're generated fresh for the snapshot\n and handed to\n `lifecycle.prepare` as `COFACTOR_PERSONAS`, a JSON map of persona name to\n `{email, password}`. Your prepare script reads it and makes each login\n work in your app: the example app's `sandbox/personas.sh` creates or\n updates one user per persona (email + password hash), with no workspace\n and no role — just the login itself. The `upload` topic covers the\n details (prepare runs while the app is up; logins must be read per\n sign-in, not once at startup).\n\n A copy's `lifecycle.prepare` still runs, with the snapshot's unchanged\n personas, so make the persona step cheap when nothing changed (skip\n hashing a password the user already has): the example app's personas.sh\n skips when a digest of COFACTOR_PERSONAS and of the users' stored hashes\n both match what its last run left, so a reset or replaced user still\n takes the full path.\n\n If your app gates sign-in behind a first-run step (accepting terms,\n initializing the instance, verifying the email address, finishing\n onboarding), clear it in prepare for each persona, the way you set the\n password: a seed or test signing in through the real login API should\n land signed in, not on the gate.\n\nROLES AND MEMBERSHIPS — SET BY SEEDS\n\n A persona existing (having a login) is separate from what it can do.\n Workspace membership and role are set by seed steps, not by the\n definition: `create-workspace` makes its `as` persona the owner of the\n new workspace, and `add-member` gives a persona a role in it. This split\n matters: the same persona can be \"just a login\" in one environment and\n a workspace owner in another, because that comes from which seeds\n actually ran. If a persona's email is an account your app already has\n with the right role, prepare setting its password is enough.\n\nHOW A SEED OR ACTION SIGNS IN AS A PERSONA\n\n Actions receive `COFACTOR_PERSONAS` too. An action that performs a real\n operation takes the acting persona as an input (`as`, of type\n `persona`, which arrives as `INPUT_AS` holding the persona's name),\n reads that persona's email and password from `COFACTOR_PERSONAS`, and\n signs in through the app's real login endpoint — the same way a user\n would — keeping the session cookie for its next calls:\n login=$(jq -c --arg p \"$INPUT_AS\" '.[$p] | {email, password}' <<<\"$COFACTOR_PERSONAS\")\n jar=$(mktemp)\n curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n -d \"$login\" \"$API_URL/api/auth/login\" >&2\n See the `seeds-and-actions` topic on why `as` is never assumed.\n\n An action runs inside the sandbox, so it can reach the app on\n localhost. If your app's session cookie is `Secure` (or `__Host-`\n prefixed) and your HTTP client won't send it back over plain http, call\n the app through its public port URL (here `API_URL`, from a\n `{port: api}` param) instead.\n\n Keep whatever the login returns that later calls need: the session\n cookie, a bearer token from the response body, or both (some apps bind\n a bearer token to an httpOnly cookie and refuse one without the other).\n\n Every action signs in from the same address, so a seed that runs several\n actions can trip the app's login rate limit (often around 10 attempts a\n minute). If your app has one, raise it for the sandbox through the\n definition's top-level `env:` (it reaches every service) rather than\n adding retries to your actions.\n\nWHERE LOGINS ARE SHOWN\n\n Because copies of a snapshot share persona passwords, anyone who has\n claimed one can sign in to another copy of the same snapshot whose URL\n they learn; keep sensitive data out of sandboxes.\n\n Persona logins (email + generated password) are surfaced by\n `cofactor sandbox show <id>` and, for PR previews, by `cofactor sandbox\n preview` and the preview's Cofactor page (`/sandboxes/previews/<application\n id>/<pr number>`, org members signed in only). They are never sent to\n GitHub — the PR's deployment only links that page, keeping generated\n passwords out of anything that ends up in GitHub's own history.\n Cofactor's test runs sign in with them automatically, as the\n application's persona of the same name.\n\n A person who claims a sandbox signs in the ordinary way: open its URL and\n use your app's own login with the email and password the claim prints.\n There is no CLI command that hands you a signed-in browser session;\n `authentication.run` (below) is only for Cofactor's automated test runs.\n\n\nGOOGLE ACCOUNTS\n\n When the definition declares the `google-oauth` emulator, a persona's\n email is also its Google identity: the Google account chooser lists each\n persona, and `authentication.emulated` signs a persona in through Google\n as that email. `google-oauth` needs at least one persona. See\n `cofactor sandbox guide v2 authentication`.\n\n\nNATIVE WORKFLOW LOGIN NODES\n\n Sandboxes v2 uses `authentication` (`run`, or `emulated` for Google\n sign-in) to mint a fresh browser session for the persona named by each\n native workflow login node. Read\n `cofactor sandbox guide v2 authentication` for the complete script contract,\n cookie and localStorage formats, and how to keep provider-login journeys\n clean and unauthenticated at the start.\n","This script is only for Cofactor's automated test runs: a person who claims a sandbox signs in through your app's own login with the email and password the claim prints, and only `cofactor sandbox verify-env` runs it outside a test (it signs every persona in on its claim to prove the script works). Native workflow login nodes use the persona declared in this sandbox definition. Declare `authentication` when your sandbox needs native persona login: exactly one of `run` (your script) or `emulated` (Cofactor signs the persona in through the Google or OIDC emulator; see GOOGLE SIGN-IN AS A PERSONA and STANDARD OIDC below). Existing definitions can use personas in prepare, seeds and actions without this script; a native login without it fails with `script_not_declared`. Each run gets its own copy, so Cofactor asks the copy's `authentication.run` script to create an application session for the requested persona. The script receives the prepared copy environment and `COFACTOR_AUTH_PERSONA`; it returns Playwright storage state as JSON on stdout. The cookies must be for the copy's public origin (sign in through the public URL, as below), not localhost. The example needs no language runtime: a POSIX shell, `curl`, `jq` and `awk`:\n\n```yaml\npersonas:\n admin: { email: admin@example.test }\n member: { email: member@example.test }\nauthentication:\n run: sandbox/auth.sh\n```\n\n```bash\n#!/bin/sh\nset -eu\n\npersona=${COFACTOR_AUTH_PERSONA:?missing persona}\n# This persona's email and generated password; fails if either is missing.\nlogin=$(printf '%s' \"$COFACTOR_PERSONAS\" | jq -ec --arg p \"$persona\" \\\n '.[$p] | {email, password} | select((.email | type) == \"string\" and (.password | type) == \"string\")')\nemail=$(printf '%s' \"$login\" | jq -r .email)\njar=$(mktemp)\ntrap 'rm -f \"$jar\"' EXIT\n\n# Sign in through the copy's public HTTPS API. This preserves Secure and\n# __Host- cookie behavior; localhost HTTP is not suitable for those cookies.\nprintf '%s' \"$login\" | curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n --data-binary @- \"$API_URL/api/auth/login\" >/dev/null\n\n# Check the resulting identity before giving the session to the browser.\ncurl -fsS -b \"$jar\" \"$API_URL/api/auth/me\" | jq -e --arg email \"$email\" '.user.email == $email' >/dev/null\n\n# Convert the cookie jar to Playwright storage-state JSON. Curl's Netscape\n# format does not carry SameSite, so adapt this field to your app's cookie.\nawk -F '\\t' '\n { httponly = \"false\" }\n /^#HttpOnly_/ { sub(/^#HttpOnly_/, \"\"); httponly = \"true\" }\n /^#/ || NF < 7 { next }\n { print $6 \"\\t\" $7 \"\\t\" $1 \"\\t\" $3 \"\\t\" $5 \"\\t\" httponly \"\\t\" $4 }\n' \"$jar\" | jq -Rn '{\n cookies: [inputs | split(\"\\t\") | {\n name: .[0], value: .[1], domain: .[2], path: .[3], expires: (.[4] | tonumber | if . == 0 then -1 else . end),\n httpOnly: (.[5] == \"true\"), secure: (.[6] == \"TRUE\"), sameSite: \"Lax\"\n }],\n origins: []\n}'\n```\n\nIf the app stores its access token in local storage, include\nan `origins` entry for the exact copy origin, with `{name, value}` pairs in\n`localStorage`. Do not print secrets to stderr, write state to a normal action,\nor use a fixed browser profile. Cofactor validates the returned state against\nthe current copy's public origins and keeps it private to that execution.\n\nWHEN THE SCRIPT FAILS\n\nWrite diagnostics to stderr: stdout is the state and is never shown. When the\nscript exits non-zero (or runs past its 60 s limit), the Login step fails with\nwhich persona, the exit code and the end of the script's stderr, for example:\n\n sandbox authentication failed: script_failed (persona \"admin\": sandbox/auth.sh exited 127). Its stderr ended:\n sandbox/auth.sh: line 4: curl: command not found\n\nEvery org member can read that error. Only the copy's known values (secrets,\nsecret params, persona passwords) are scrubbed from it, verbatim: a session\ncookie, token or login response the script prints is shown as is. So print\nwhat failed, never a response body, `curl -v` or `set -x` trace.\n\nExit 127 is a command the image does not have: install it in the image, or\nuse what it has. `cofactor sandbox verify-env` signs every persona in on a\nfresh claim and reports the same message per persona, so run it after every\npush that changes the image or the script, before a test depends on it.\n\nTHIRD-PARTY IDENTITY PROVIDERS\n\nFor ordinary application tests, create the app's session directly. Use the\nsandbox-only login API, a signed test token accepted only by the copy, an SDK,\nor the provider's emulator-backed token exchange. The auth action should\nfinish with the application's own session cookie or local-storage token. A\nflow that tests login itself must begin clean: retain its application login\nnode with `performLogin: false` to select the app and environment, without\nseeding or preloading an app session. Then navigate to the app and traverse\nits real sign-in UI against the declared provider emulator.\n\nGoogle OAuth requires interception on both sides. Cofactor intercepts the\nbrowser authorization request, including an app HTTP redirect to Google,\nand answers it from the emulator: its account chooser, or a code for the\ncopy's callback when the request's login_hint matches a persona. The\ndeclared sandbox emulator handles the app's token exchange and user-info\nrequests. Verify the complete callback and resulting app identity; an\nintercepted browser redirect alone does not prove the server handshake\nworks.\n\nGOOGLE SIGN-IN AS A PERSONA (authentication.emulated)\n\n When the app's only login is \"Sign in with Google\", there is no password\n endpoint for a script to call. Declare `emulated` instead of `run`, and\n Cofactor signs the persona in itself, inside the copy, with no script:\n authentication:\n emulated:\n provider: google-oauth\n sign_in: ${params.web_url}/sign-in\n\n `authentication` is exactly one of `run` or `emulated`; both, neither key\n inside it, or an unknown key fails validation. `provider` must be\n `google-oauth` or `oidc` (this section describes `google-oauth`; for `oidc`\n see STANDARD OIDC below) and must be declared under `emulation`. `sign_in` is the app URL that starts the Google\n sign-in: `${params.<name>}/path`, where the param is a `{port: ...}`\n param on a PUBLIC port, because the sign-in starts on the copy's public\n origin. Use the port that serves your sign-in route, for example\n `${params.api_url}/auth/google` when the API starts the flow and the\n web app only links to it, or `${params.web_url}/sign-in` for a single-port\n app. A bare path, an undeclared param or a private-port param fails\n validation.\n\n A complete definition that passes `cofactor sandbox validate`:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n setup: |\n apt-get update -qq\n apt-get install -y -qq curl unzip >/dev/null\n curl -fsSL https://bun.sh/install | bash >/dev/null\n ports:\n web: { port: 8300, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n GOOGLE_CLIENT_ID: sandbox\n GOOGLE_CLIENT_SECRET: sandbox\n GOOGLE_REDIRECT_URI: ${params.web_url}/auth/callback\n services:\n app:\n run: bun server.ts\n health: { http: 8300, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n bob: { email: bob@example.test }\n authentication:\n emulated:\n provider: google-oauth\n sign_in: ${params.web_url}/sign-in\n emulation:\n google-oauth: {}\n lifecycle:\n build:\n - name: install\n run: bun install\n inputs: [bun.lock, package.json]\n prepare: []\n environments:\n default: { description: Stock data. }\n\n `setup` is for GitHub-delivered definitions, as in the `definition` topic; a\n pushed image omits it (it is already built).\n\n What happens for a Login step as persona `bob`:\n 1. The helper GETs `sign_in` on the copy's public URL and follows the\n redirects with a cookie jar (at most 10).\n 2. At the hop to accounts.google.com, which resolves to the emulator\n inside the VM, it adds `login_hint=bob@example.test`. The emulator\n completes the authorize request for that persona without showing\n the chooser.\n 3. The emulator redirects to the app's callback; the app exchanges the\n code and sets its session cookie.\n 4. The cookies for the copy's public origin become the Playwright\n storage state, validated like the output of `authentication.run`.\n\n Every redirect hop must stay on the origins of your public `{port}`\n params or on accounts.google.com; a hop anywhere else fails the\n sign-in. Success needs the provider hop AND a cookie set for the copy\n after it, so a pre-provider CSRF or state cookie is not mistaken for a\n session. The\n Login node, persona switching and landing check are the same as for\n `authentication.run`. A failed emulated sign-in shows the Login step\n error `sandbox authentication failed: script_failed (persona \"bob\": the\n sandbox helper reported <reason>)`, where the reason is one of\n `sign_in_unreachable`, `sign_in_rejected`, `sign_in_no_session`,\n `callback_origin` or `invalid_state`. Look for what a\n customer can fix: the app redirects to an origin the definition does not\n declare as a public `{port}` param; `sign_in` never reaches Google; the\n service that calls Google lacks `reload_on_prepare` or does not trust\n /etc/emulation/ca.crt. The Login step can also fail with\n `state_origin_mismatch` when the app sets a cookie for a parent Domain\n outside the copy's public origins. The specific failure category is\n visible to Cofactor support, and the emulator's own log is in the copy at\n /var/lib/cofactor/logs/emulator.log.\n\n Emulated sign-in covers sign-ins the server starts with HTTP redirects.\n It does not cover Google One Tap, FedCM or popup sign-ins done in\n browser JavaScript, and it does not cover a person using the copy in\n their own browser: accounts.google.com is hard-coded in apps and only\n Cofactor's browser (and the VM itself) reaches the emulator for it.\n\n The chooser. When Cofactor's browser agent reaches Google's authorize\n URL, the emulator serves a real account-chooser page listing every\n persona by name and email; clicking one completes the sign-in as that\n persona. A flow that tests the Google journey itself clicks \"Sign in with\n Google\", picks a persona on the chooser, and asserts the signed-in\n identity.\n\n `login_hint` preselects. An authorize request whose `login_hint` equals a\n persona's email (case-insensitive) skips the chooser. This holds even\n when the app also sends `prompt=select_account`, which real Google would\n honor by showing its chooser; that is a deliberate deviation so the\n helper and hint-sending apps sign in without a click. An absent or\n unmatched hint shows the chooser.\n\n Emulator limits. Authorization codes are single use, and are bound to the\n `redirect_uri` of the authorize request, and to its `client_id` when both\n the authorize request and the token request body carry client_id: a\n replayed code, a code the emulator did not issue, or a mismatch is\n `invalid_grant`. Send the same `redirect_uri` on authorize and on the token exchange, as real Google\n requires. The emulator does NOT check that `redirect_uri` is registered\n anywhere; any http(s) URL is accepted, so a wrong callback is caught only\n by your app.\n\nGOOGLE CALLBACK CONFIGURATION\n\n Supply the current copy's callback using the app's existing configuration:\n params:\n api_url: { port: api }\n env:\n GOOGLE_CLIENT_ID: sandbox\n GOOGLE_CLIENT_SECRET: sandbox\n GOOGLE_REDIRECT_URI: ${params.api_url}/auth/callback\n emulation:\n google-oauth: {}\n personas:\n alice: { email: alice@example.test }\n\n This is a fragment: declare the public api port, use your app's actual\n variable names and callback path, and set reload_on_prepare: true on the\n backend calling Google. An app whose callback comes from configuration\n can use this without auth code changes. A hardcoded production callback\n must become configurable. No per-copy Google Console registration is\n needed for the emulated path; real Google login keeps Google's rules.\n\n Do not add live: true merely to avoid service restarts. That optimization\n requires the app to re-read the live value per request; a normal {port}\n param with reload_on_prepare works for apps that read env at startup.\n\n Google needs both browser authorization interception and sandbox-side\n token/user-info emulation. Verify the app callback and a protected request,\n not just that the browser received an emulated code.\n\n Each persona is a Google account: its email is the Google email, and its\n Google `sub` is derived from the persona name, so it is the same in every\n copy. The identity claims are `sub`, `email`, `email_verified`, `name` and\n `given_name` (both the persona name), plus the id_token's standard\n iss/aud/azp/nonce and time claims. No `family_name`, `picture` or `hd`.\n There is no default Google user; a\n definition that declares `google-oauth` with no personas fails validation.\n\n If Google is reached through hosted Auth0, Clerk or another intermediary,\n this Google emulator does not emulate that intermediary. If the app lets\n you configure its own OIDC provider (an issuer or authority setting), use\n the `oidc` emulator instead: see STANDARD OIDC below. Otherwise inspect where\n token exchange occurs. Prove the customer's actual library and trust setup;\n do not disable token verification or alter production auth to pass a test.\n Non-Node clients must trust /etc/emulation/ca.crt through their runtime's\n supported mechanism. This native Login-node contract does not inject\n authentication into an external Playwright suite.\n\nSTANDARD OIDC (Auth0, Okta, Cognito, Keycloak, Clerk, ...)\n\n For an app that signs in through any standard OpenID Connect provider,\n declare the `oidc` emulator and point the app's issuer at it. The emulator\n is a generic OIDC provider, not a copy of one vendor: use it with the\n vendor's own SDK or any OIDC library, as long as the app's issuer (or\n authority, domain or base URL) setting is configurable.\n\n This is a fragment: declare the public web port, use your app's actual\n variable names and callback path, and set reload_on_prepare: true on the\n service that reads the issuer.\n params:\n web_url: { port: web }\n issuer_url: { port: emulation }\n env:\n OIDC_ISSUER: ${params.issuer_url}\n OIDC_CLIENT_ID: sandbox\n OIDC_CLIENT_SECRET: sandbox\n OIDC_REDIRECT_URI: ${params.web_url}/auth/callback\n services:\n app:\n run: bun server.ts\n health: { http: 8300, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n emulation:\n oidc: {}\n\n `issuer_url: { port: emulation }` is the copy's public emulator address and\n is the issuer, exactly: an https origin with no trailing slash, with a real\n TLS certificate. The app needs no CA and no /etc/hosts entry, and a\n library that refuses an http issuer is satisfied. Point the vendor SDK's\n issuer, authority or domain setting at it (`OIDC_ISSUER` above); the SDK\n discovers everything else from\n `<issuer>/.well-known/openid-configuration`. The service that reads the\n issuer needs `reload_on_prepare: true`, because the address is per copy.\n\n Endpoints: /.well-known/openid-configuration, /authorize, /token,\n /userinfo (GET and POST), /jwks.\n\n Client id and secret are accepted as-is: use any values, such as\n `sandbox`. The token endpoint takes client credentials in the body or by\n HTTP Basic and never checks the secret. PKCE is advertised (S256 and\n plain) so libraries that send it proceed, but the challenge is not\n verified.\n\n Redirect rule. The `redirect_uri` on /authorize must be an http(s) URL on\n one of the copy's public origins (any public `{port}` param of the\n definition); anything else is a 400 and nothing is redirected. Use the\n port that serves the callback, as in GOOGLE CALLBACK CONFIGURATION.\n\n Each persona is an account: its email is the account email. Claims, in the\n id_token and from /userinfo, are `sub` (stable per persona, the same in every\n copy), `email`, `email_verified` (true) and `name`, plus the id_token's\n standard iss/aud/azp/nonce and time claims. Nothing else: no groups, roles,\n `picture` or vendor-specific claims, so an app that depends on those needs\n its own handling in the sandbox. Authorization codes are single use and are\n issued only by /authorize; a replayed or never-issued code is `invalid_grant`,\n as is a different `redirect_uri`. The id_token is signed RS256 with a key\n published at /jwks.\n\n The sign-in link. /authorize is locked: signing in needs a browser that has\n opened the copy's sign-in link. Without it, /authorize shows a page saying to\n open the sandbox's sign-in link from Cofactor (HTTP 403) and the app is not\n signed in. The link is `<emulation origin>/oidc/enter?key=...`. Opening it\n sets a browser-session cookie and shows \"Sign-in is unlocked\"; after that the\n app's Sign in button reaches the account chooser, which lists each persona by\n name and email, and picking one signs in as that persona. The unlock lasts\n until that browser session ends; the link itself works for the copy's life,\n so reopen it if you land on the locked page again. `cofactor sandbox claim` and `cofactor sandbox show <id>`\n print the link as `Open sign-in: ...` next to the persona logins, and the\n sandbox API returns it as `signInUrl`. Only organization members who can read\n sandboxes see it. A preview has no sign-in link. Treat the link like the\n persona passwords: it is a credential for that copy. `--json` output includes\n it, so keep that output out of CI logs.\n\n Cofactor's own browser agent and native Login steps pass the gate\n automatically, so a flow that tests the sign-in journey needs no link step.\n\n Login steps. `authentication.emulated` with `provider: oidc` signs a persona\n in with no script:\n authentication:\n emulated:\n provider: oidc\n sign_in: ${params.web_url}/login\n\n `oidc` must be declared under `emulation`, and `sign_in` follows the same\n rules as for Google (`${params.<name>}/path` on a public `{port}` param).\n Inside the copy the helper GETs `sign_in`, follows the redirects with a cookie\n jar, and at the hop to the issuer's /authorize adds the persona's\n `login_hint` and the gate; the emulator redirects to the app's callback, the\n app sets its session, and the cookies become the storage state. Every\n redirect hop must stay on a public `{port}` origin of the definition or the\n issuer, and a session cookie must be set after the issuer hop. Failures are\n reported as for Google, and the emulator's own log is in the copy at\n /var/lib/cofactor/logs/emulator.log. The helper requires the issuer to be\n https.\n\n A complete definition that passes `cofactor sandbox validate`:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n setup: |\n apt-get update -qq\n apt-get install -y -qq curl unzip >/dev/null\n curl -fsSL https://bun.sh/install | bash >/dev/null\n ports:\n web: { port: 8400, access: public }\n params:\n web_url: { port: web }\n issuer_url: { port: emulation }\n session_secret: { secret: true }\n env:\n OIDC_ISSUER: ${params.issuer_url}\n OIDC_CLIENT_ID: sandbox\n OIDC_CLIENT_SECRET: sandbox\n OIDC_REDIRECT_URI: ${params.web_url}/auth/callback\n SESSION_SECRET: ${params.session_secret}\n services:\n app:\n run: bun server.ts\n health: { http: 8400, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n bob: { email: bob@example.test }\n emulation:\n oidc: {}\n authentication:\n emulated:\n provider: oidc\n sign_in: ${params.web_url}/login\n lifecycle:\n build:\n - name: install\n run: bun install\n inputs: [bun.lock, package.json]\n prepare: []\n environments:\n default: { description: Stock data. }\n\n The sample app in the Cofactor repository, packages/demo-app-oidc, is a\n working example using the `openid-client` library.\n\nREFERENCES\n\n https://docs.runcofactor.com/reference/sandbox-definition#authentication\n https://docs.runcofactor.com/reference/sandbox-emulation#google-oauth-and-third-party-login\n cofactor sandbox guide v2 personas\n cofactor sandbox guide v2 emulation\n cofactor sandbox guide v2 ports\n","An environment's `env` block (under `environments.<name>.env`) overrides\nenvironment variables for exactly three things:\n\n - running services that declare `reload_on_prepare: true` — they are\n restarted by `prepare` (and by an in-place move to a new commit), so\n they always see the current chain's merged env;\n - `lifecycle.prepare` commands, run fresh on every prepare;\n - actions, which run with the prepare env plus that call's\n `INPUT_<NAME>` values.\n\n environments:\n beta-features:\n extends: default\n description: Same as default, with the timeline view turned on.\n env:\n FEATURE_FLAGS: timeline\n\nIt does NOT reach:\n\n - `lifecycle.build` steps. Build steps run once into the repo-base layer,\n before any environment is chosen, so an environment's `env` does not\n exist at build time. A value your build bakes into its output (e.g. a\n front-end bundle's API URL or feature flags) can't vary by\n environment: read it at serve time instead, or set it with a seed\n action.\n - services that don't declare `reload_on_prepare`. A service without it\n only ever sees the definition's top-level `env` and layer-time param\n values (from when its layer was built) — it is not restarted when a\n sandbox is prepared, so a per-environment override would never take\n effect for it.\n\nAn environment's `env` values are used as written: `${params.name}` is\nsubstituted only in the definition's top-level `env`. To vary a value per\nenvironment that must not live in the repository (an API key), use a\nsecret scoped to that environment: see the `secrets` topic.\n\n`env` MERGE ORDER\n\n Composition follows the same parent-then-child rule as seeds (see\n `environments`): the resolved chain's env is the parent's `env` merged\n first, then the child's own, so a child environment can override a\n specific key its parent sets without having to repeat the rest. Secrets\n (see the `secrets` topic) merge last, above every `env`.\n\nRELATION TO THE DEFINITION'S TOP-LEVEL `env`\n\n The definition's top-level `env` (outside any environment) is the base:\n fixed values and `${params.name}` substitutions that apply regardless of\n which environment is claimed — `DATABASE_URL`, `PUBLIC_URL` and so on in\n the example app. An environment's `env` is layered on top of that base\n for the reachable surfaces above, not a replacement for it.\n","A secret is a real third-party credential your app needs at run time: a\nStripe or OpenAI key, an email provider's API key, an outside service's\ntoken. Cofactor stores it encrypted per application and hands it to the\nsandbox's processes as an environment variable. It is never written into\nthe definition or your image, and no command prints it back.\n\nWHICH KIND OF VALUE IS IT?\n\n A shared constant (a database URL, a feature name)\n -> `env:` in the definition. See the `env` topic.\n A URL that differs per sandbox (the app's own public origin)\n -> a `{port: <name>}` param. See the `definition` topic.\n A value generated for you (a session-signing key)\n -> a `{secret: true}` param. One random value is made when the ready\n snapshot is built and shared by every copy of that snapshot.\n A real third-party secret\n -> the secret store, described below.\n\n Short non-secret values belong in `env:`: a value under 8 bytes is not a\n credential, and the store refuses it.\n\nUSE THE PROVIDER'S TEST KEYS\n\n Sandbox egress is open, except for the domains the definition's\n `emulation` intercepts (see the `emulation` topic): a call your app makes\n in a sandbox goes out to the real provider. Set the provider's test or\n sandbox key (Stripe's `sk_test_...`, a sandbox API key), never a live\n one. Never put a key in the image, a `.env` file or\n `cofactor.sandbox.yaml`: those end up in layers, snapshots and the\n repository.\n\nSETTING ONE\n\n The value comes from stdin only, so it never lands in shell history or\n `ps`. The CLI refuses to read it from a terminal.\n\n printf %s \"$STRIPE_TEST_KEY\" | cofactor sandbox secret set STRIPE_API_KEY --application shop\n\n That is one secret for all of the application's environments. To give\n one environment a different value, name it:\n\n printf %s \"$STRIPE_STAGING_KEY\" | cofactor sandbox secret set STRIPE_API_KEY --application shop --environment staging\n\n See what is set (names and scopes, never values):\n\n cofactor sandbox secret list --application shop\n\n NAME SCOPE UPDATED\n STRIPE_API_KEY environment staging 2026-10-08T09:13:41.000Z\n STRIPE_API_KEY all environments 2026-10-08T09:12:00.000Z\n\n Names are capital letters, digits and `_`, not starting with a digit,\n and must not be PATH, HOME, USER, SHELL, NODE_OPTIONS,\n NODE_EXTRA_CA_CERTS, `LD_*` or `COFACTOR_*`. A value is 8 to 8192\n bytes, with no NUL. The environment, when given, must be one the\n application's synced definition has.\n\nOVERRIDE AND INHERITANCE\n\n A secret with no `--environment` applies to every environment. One with\n `--environment` is an override that wins for that environment. Resolution\n follows the same chain as `env:` and seeds (see `environments`): the\n application-wide value, then each ancestor's override, then the claimed\n environment's own, last one wins. An override on a parent applies to\n every environment that extends it; a child's own override beats it.\n\n A secret also overrides an `env:` entry of the same name.\n\nREACHING THE APP\n\n Secrets are injected into:\n - services that declare `reload_on_prepare: true`;\n - `lifecycle.prepare` commands;\n - seeds and actions;\n - `authentication.run`.\n They are NOT injected into `lifecycle.build` steps or `setup`:\n build-time secrets are unsupported (a build bakes its output into a\n layer that is shared). And a service WITHOUT `reload_on_prepare: true`\n never sees a secret, not even once: set it on every service that reads\n one.\n\n # cofactor.sandbox.yaml: the key itself is not here, only the service\n # that reads it.\n services:\n api:\n run: node server.js\n health: { http: 3001, path: /healthz }\n reload_on_prepare: true # reads STRIPE_API_KEY; without this it never sees it\n\n environments:\n staging:\n extends: default\n description: Same as default, with the staging Stripe account.\n\n The app reads `process.env.STRIPE_API_KEY`; nothing in the definition\n names the secret.\n\n A secret can also be served through the `aws` emulator, for an app that\n fetches its real third-party key from AWS Secrets Manager at run time\n instead of reading an env var: `app/prod/ai-key: \"${secrets.AI_API_KEY}\"`\n under `emulation.aws.secrets_manager.secrets`. Sandbox-internal values (a\n local database password) are `{secret: true}` params, not store secrets.\n See the `emulation` topic.\n\nROTATION\n\n Set the new value with `secret set`, or remove it with `secret rm`.\n Neither operation rebuilds an existing ready snapshot.\n - Fast copies keep the snapshot's baked secret values, including a\n rotated or removed value, until that snapshot is rebuilt. A new\n commit or image push builds a snapshot with the current values.\n - Claims on the legacy prepare path pick up changes immediately:\n prepare reads the current store, omits removed secrets, and restarts\n `reload_on_prepare` services when values change. Prepare commands,\n actions and `authentication.run` then see those current values.\n - A sandbox that is already claimed keeps the values it was prepared\n with until it is destroyed. A pull request preview keeps the values\n of its first prepare (later pushes move it in place without\n preparing again), and is scrubbed of the application's current\n values; a value rotated away since that first prepare is not\n scrubbed.\n - Seeds ran once, when the ready snapshot was built, with the value\n that existed then. Anything a seed created in a third-party system\n with a since-rotated key stays as created. A new commit or push\n builds a fresh snapshot with the current values.\n\n So set secrets BEFORE the first claim: the first snapshot's seeds run\n with whatever is set at that moment.\n\n The ready snapshot is private to your organization, but the processes\n running when it was captured hold the values set when the snapshot was\n built, so the snapshot contains them: the snapshot's `prepare.json`\n holds the values. Layers built before prepare (platform, toolchain,\n repo base) never do.\n\nWHAT IS SCRUBBED\n\n Every injected value is replaced with `***` in action output and tails,\n seed outputs and failed-build messages. Object keys and numeric values\n equal to a secret are scrubbed too. The match is exact only:\n - a value your app prints transformed (base64, URL-encoded,\n JSON-escaped, split across lines) is not scrubbed;\n - a multi-line value (a private key) is scrubbed only when printed\n whole;\n - an action's `output` keeps the action's own `sensitive` inputs: only\n injected secrets are scrubbed from it;\n - values your app renders into a page are not scrubbed from\n screenshots, traces or page text.\n Don't print a secret, and don't render one into a page.\n","Emulation makes third-party services deterministic and inspectable: instead\nof a sandbox's outbound calls to an email provider, Google, Jira or Slack\nleaving the VM, they're intercepted by an in-sandbox emulator that behaves\nlike the real thing well enough for tests, and lets a test look at what\nwas sent.\n\nPLUGINS\n\n Declaring a plugin id under `emulation:` turns on its interception and\n adds it to what `envs` reports.\n emulation:\n email: {}\n\n Available plugin ids:\n aws Serves AWS service APIs to the app's own, unmodified AWS\n SDK: no endpoint override, no code change. Only the\n services you give a section are intercepted, in each of\n `regions` (default [us-east-1]). Today: Secrets Manager,\n KMS and SSM Parameter Store (see AWS below). SES stays\n with `email`.\n email Intercepts the HTTP send APIs of several providers\n (SES, SendGrid, Resend, Postmark, Mailgun) so outbound\n mail is captured instead of actually sent. SES is\n intercepted in us-east-1 unless you list others:\n `email: { sesRegions: [eu-west-1] }`; add other hosts\n with `extraDomains`. Mail through a host that isn't\n intercepted leaves the sandbox for real.\n `email: { mailosaur: {} }` also serves the Mailosaur\n API from what was captured (see MAILOSAUR below).\n SMTP (nodemailer, Django's SMTP backend, Action\n Mailer…) is not intercepted. Leave SMTP unconfigured\n in the sandbox so the app sends nothing, or ship an\n SMTP catcher such as Mailpit in the image, run it as\n one of your `services`, and point the app's SMTP host\n at 127.0.0.1.\n google-oauth Intercepts Google's OAuth endpoints, for testing login\n flows without a real Google account. Every declared\n persona is a Google account (its email is the Google\n email), so the definition must declare at least one\n persona.\n oidc A standard OpenID Connect provider for apps that sign in\n through Auth0, Okta, Cognito, Keycloak, Clerk or any\n other OIDC provider whose issuer the app lets you\n configure. It takes no options: `oidc: {}`. Its\n address is the copy's public `emulation` URL, which\n is the issuer; point the app's OIDC issuer setting at\n `${params.issuer_url}` with `issuer_url: { port:\n emulation }`. Every declared persona is an account,\n so the definition must declare at least one persona.\n See `cofactor sandbox guide v2 authentication`.\n jira Intercepts the Jira Cloud REST API of your site:\n `jira: { tenant: acme }` serves acme.atlassian.net, plus\n api.atlassian.com (the OAuth gateway) and\n auth.atlassian.com. `tenant` is required (validate\n doesn't check it). The app registers its own webhook\n through the emulated API; operations `jira.create-issue`,\n `jira.add-label` (delivers the tag webhook to what the app\n registered), `jira.get-issue` and `jira.get-comments`\n drive and read it. Details: docs/api/services/\n sandbox-emulation.md.\n slack Intercepts Slack's API for posted-message flows.\n esri Serves the slice of the ArcGIS REST API a connector uses\n for hosted feature layers (token, query, applyEdits);\n layers are seeded with its `seed-layer` operation.\n maximo-stub Serves a slice of the IBM Maximo MBO REST API over its\n built-in sample records: list and read them, and create\n and update work orders, assets, locations and service\n requests.\n\n `cofactor sandbox envs` lists each declared plugin's operations.\n\nAWS\n\n Seed the secrets an app reads from Secrets Manager at startup. A string is\n the SecretString as is; an object is served as its JSON. Values that are\n sandbox-internal (the in-sandbox database's password, a connection\n string) are plain values or `{secret: true}` params, referenced as\n `${params.NAME}` (only `aws` resolves references; one in another plugin's\n config fails validate):\n params:\n db_password: { secret: true }\n env:\n DB_PASSWORD: ${params.db_password} # what the local database is set up with\n AWS_ACCESS_KEY_ID: emulated # any credentials work; the SDK\n AWS_SECRET_ACCESS_KEY: emulated # needs some to sign a request\n emulation:\n aws:\n regions: [us-east-1] # optional; the default\n secrets_manager:\n secrets:\n app/prod/db: { host: 127.0.0.1, username: app, password: \"${params.db_password}\", dbname: app }\n app/prod/feature-flags: \"on\"\n\n If the app fetches a real third-party key (an AI provider's or payment\n provider's test key) from Secrets Manager at run time, keep that key in the\n secret store (see the `secrets` topic) and reference it as\n `${secrets.NAME}`; the definition only names it:\n app/prod/ai-key: \"${secrets.AI_API_KEY}\"\n Local values do not belong in the secret store: it is for real third-party\n credentials only.\n\n References are resolved inside the sandbox when prepare runs, with the\n environment's values (a secret's environment override wins, as for env).\n Fast copies keep the snapshot's config and secret values until it is rebuilt\n by a new commit or image push; legacy prepare claims see changes on the next\n claim. A `${secrets.NAME}` that is not set for the environment fails prepare,\n naming the secret, so the snapshot build or legacy prepare claim fails.\n A `${params.NAME}` must name a declared param (validate checks it).\n Resolved values reach only the emulator: they are\n not written into the definition, not printed, not recorded with the\n emulator's requests, and not returned by `aws.secrets`.\n\n Served: GetSecretValue (by name, full ARN or ARN without its suffix;\n VersionStage AWSCURRENT), BatchGetSecretValue, DescribeSecret and\n ListSecrets (Filters on `name` and `all`, MaxResults/NextToken). An\n unknown name is AWS's ResourceNotFoundException. ARNs look like\n arn:aws:secretsmanager:us-east-1:000000000000:secret:app/prod/db-1a2b3c\n and, like version ids, are the same in every copy. Secrets are read-only:\n CreateSecret, PutSecretValue and other writes answer 501.\n\n KMS. Ciphertext made by real KMS (say, a data key hardcoded in the app)\n cannot be decrypted in a sandbox, so seed what it decrypts to, both base64:\n kms:\n decrypt:\n - ciphertext: AQICAHh...base64...\n plaintext: \"${params.aes_key}\" # or a base64 literal\n key_id: alias/app # optional: the KeyId Decrypt answers with\n `kms: {}` alone serves Encrypt, Decrypt and GenerateDataKey under one\n emulated key (any KeyId is accepted), so an app that encrypts at run time\n round-trips, including what the snapshot's seeds encrypted: the key is\n generated inside the sandbox when the snapshot is built and is never shown.\n Unknown ciphertext is AWS's InvalidCiphertextException; a different\n EncryptionContext fails the same way (seeded ciphertext ignores the\n context). A referenced plaintext must resolve to base64, or the claim fails\n naming the field. Other KMS calls answer 501.\n\n SSM PARAMETER STORE. Seed parameters by name; `type` is String (the\n default), StringList or SecureString, and values take references too:\n ssm:\n parameters:\n /app/prod/feature-x: { value: \"on\" }\n /app/prod/db-password: { value: \"${params.db_password}\", type: SecureString }\n Served: GetParameter (by name or ARN), GetParameters (unknown names come\n back in InvalidParameters), GetParametersByPath (one level, or every depth\n with Recursive; MaxResults/NextToken) and DescribeParameters\n (ParameterFilters on Name, Type and Path). A SecureString's value is\n returned only WithDecryption; without it the value is an opaque stand-in.\n Every parameter is version 1; version and label selectors (`name:1`) are\n not supported. An unknown name is AWS's ParameterNotFound. Writes\n (PutParameter, ...) and other filters answer 501. Operation\n `aws.parameters` lists names, types, versions, ARNs and read counts,\n never a value.\n\n The SDK still needs credentials (any value) and a region: set them in\n `env:` unless the app or a declared `object_storage: { provider: s3 }`\n already does. Operation `aws.secrets` lists each secret's name, ARNs,\n version id and how often the app read it, never a value.\n\nMAILOSAUR\n\n A test suite that reads the app's mail through Mailosaur (its API or an\n official SDK) runs unchanged in the sandbox. Declare the inbox on the\n email plugin:\n emulation:\n email:\n mailosaur: {}\n `mailosaur.com` is then answered inside the sandbox from the mail the app\n sent through any intercepted provider (SES, SendGrid, Resend, Postmark,\n Mailgun). A message addressed to `<anything>@<server id>.mailosaur.net`\n is in that server's inbox; mail to other addresses is in none. Any server\n id works, there is nothing to create. The suite needs no base URL change\n and no code change: keep the SDK's default host, the real server id and\n the real addresses. A Playwright suite Cofactor runs on the sandbox gets\n NODE_EXTRA_CA_CERTS, as actions do, so its Node trusts the emulator (unless\n the suite sets its own NODE_EXTRA_CA_CERTS, which then wins and must\n include /etc/emulation/ca.crt). The\n SDK still needs a key; any value works, so give the suite one in its\n `cofactor.tests.yaml`:\n env:\n MAILOSAUR_API_KEY: emulated\n\n Served (the read side, as the SDKs call it): list messages, search them\n (sentFrom and sentTo match a whole address, subject and body match any\n part, ignoring case; match ALL or ANY; receivedAfter, page, itemsPerPage),\n get one message, delete one, delete all of a server's, list servers and\n get one. The SDKs' wait-for (`messages.get`, search with a timeout) works\n as against Mailosaur, polling until the mail arrives. A message carries\n from, to, cc, bcc, subject, received, the html and text bodies with their\n links (html `<a>` links, text URLs), html images, verification codes\n (each standalone run of 4 to 8 digits, or 4 to 8 upper-case letters and\n digits with both, in order of appearance, never inside a URL; a year in\n a footer counts too), attachment names and types, and From/To/Subject/\n Date headers. Deleting hides a message from the Mailosaur inbox only:\n `email.sent-emails` still returns it, and `email-reset` clears both.\n The emulator keeps the last 500 emails the app sent, to any address, so\n in a long run the oldest leave the inbox.\n\n Not emulated (501, recorded): creating servers or messages, forward,\n reply, attachment and raw-message downloads, spam and deliverability\n analysis, previews, devices (TOTP) and usage; SMS; mail the app sends\n over SMTP (see email above).\n\nHOW INTERCEPTION WORKS\n\n Most plugins own real domains. Inside the sandbox, those domains (e.g.\n `email.us-east-1.amazonaws.com` for the email plugin's SES routes, or\n `accounts.google.com` for `google-oauth`) are resolved via `/etc/hosts` to\n the emulator instead of the real internet. The emulator serves them over\n real TLS, using a CA generated when the version's ready snapshot is built\n (every copy shares it). It survives an emulator restart, so services that\n read `NODE_EXTRA_CA_CERTS` once at startup don't break when the emulator\n restarts. Services with `reload_on_prepare: true`, prepare commands and\n actions get `NODE_EXTRA_CA_CERTS=/etc/emulation/ca.crt` so they trust\n that CA. The CA is also added to the system trust store when the image\n has `update-ca-certificates` (Debian, Ubuntu, Alpine with the\n ca-certificates package), which .NET, Go, curl and OpenSSL-based clients\n read; on an image without it, a non-Node runtime must trust\n /etc/emulation/ca.crt itself. Some clients carry their own bundle and\n ignore the system store; point them at the system bundle in `env:`:\n boto3/botocore with `AWS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt`,\n Python `requests` with `REQUESTS_CA_BUNDLE` (same path). Java reads its\n own keystore, updated by the same command only when the image has\n ca-certificates-java. A service WITHOUT `reload_on_prepare` never gets it (nor any\n emulation env): its calls to an emulated domain fail TLS. Any service that\n talks to an emulated provider must be `reload_on_prepare`.\n\n `oidc` owns no domain and needs neither `/etc/hosts` nor the CA. It is\n reached at its public URL (the copy's `emulation` port), which has a\n publicly trusted certificate, so any client reaches it the way it reaches a\n real provider. The other plugins keep using `/etc/hosts` and the CA.\n\n NO PASSTHROUGH. An unmatched path on an emulated domain doesn't\n fall through to the real provider — it returns 501 and the attempt is\n recorded, so a route the app calls that no plugin has implemented yet\n fails loudly and visibly instead of silently reaching a real third\n party.\n\n The emulator depends on `openssl` for certificate generation. For a\n GitHub-delivered app, the platform installs it before your `setup`\n runs, so a definition doesn't need to install it itself; a pushed image\n that declares `emulation` must include it (`apt-get install -y\n openssl`), or the emulator never turns healthy and prepare fails.\n\nOPERATIONS\n\n Each plugin declares typed operations — the same shape as an action\n (id, description, typed input, typed output) — that appear right alongside\n your definition's own actions, named `<plugin>.<op>`. They run exactly\n like an action: `cofactor sandbox run <id> <plugin.op>`, or as a seed\n step (`action: email.sent-emails`). Cofactor calls the emulator's control\n plane directly rather than shelling into the sandbox for these.\n\n Today: `aws.secrets` and `aws.parameters` — no input, return the seeded\n secrets' / parameters' names, ARNs, versions and read counts (never\n values). `email.sent-emails` — no input, returns every email captured since\n boot or the last reset. In the example app, it verifies that\n `invite-user` actually sent the invitation:\n cofactor sandbox run <id> invite-user \\\n --input workspace_id=<acme.workspace_id> \\\n --input email=new@example.test --input role=member --input as=owner\n cofactor sandbox run <id> email.sent-emails\n\n\nAUTHENTICATION AND PROVIDER LOGIN\n\n For normal persona-based flows, have `authentication.run` create the\n application's session; see `cofactor sandbox guide v2 authentication`.\n For a flow testing Google login, start without that pre-authenticated state\n and use the `google-oauth` emulator through the app's actual login button.\n This avoids treating a successful pre-authenticated landing as proof of the\n provider redirect and callback.\n\n For Google callback configuration, app-code compatibility, the account\n chooser and `authentication.emulated` (Cofactor signs a persona in through\n Google with no script), read `cofactor sandbox guide v2 authentication`.\n Each persona is a Google account; a definition declaring `google-oauth`\n with no personas fails validation: \"each persona is a Google account;\n declare at least one\". Google browser authorization and the backend\n token/user-info handshake both need to work; verify the full signed-out\n journey. For apps that use a standard OIDC provider (Auth0, Okta, Cognito,\n Keycloak, Clerk...), declare `oidc` instead; see the STANDARD OIDC\n section of `cofactor sandbox guide v2 authentication`. Hosted identity intermediaries are not covered by declaring\n google-oauth alone, and neither are One Tap, FedCM or popup sign-ins or\n a person using the copy in their own browser.\n\n Reference:\n https://docs.runcofactor.com/reference/sandbox-emulation#google-oauth-and-third-party-login\n","An app that stores files (uploads, attachments, exports) can have an\nemulated S3 or Google Cloud Storage server in every sandbox, instead of\nreaching a real bucket. You declare the provider and the buckets; the\nsandbox runs the server, creates the buckets, and gives your app the\nstandard client settings for that provider.\n\nDECLARING IT\n\n object_storage:\n provider: s3 # or gcs\n buckets: [attachments, exports]\n\n - One provider per app. Bucket names are 3–63 lowercase letters,\n digits, `-` and `.`, starting and ending with a letter or digit, with\n no `..` and not shaped like an IP address. A GCS bucket can't be\n named `storage`, `upload`, `download` or `batch`.\n - The buckets exist before your first prepare command runs.\n - Declaring it adds an implicit public port named `object-storage` (you\n don't declare it yourself, and you can't name a port of your own\n `object-storage`). It counts toward the 8-port limit.\n - The server listens on port 9000 (S3), or 4443 and 4444 (GCS);\n a port of your own can't use those numbers, and a service of your own\n can't be named `object-storage` or `object-storage-front`.\n\n For a pushed image, `cofactor sandbox push` adds the declared provider's\n server to the image; you don't install anything.\n\nYOUR DATA IN EACH SANDBOX\n\n Each sandbox gets its own copy of the buckets. Files your seeds put there\n (through an action, below) are captured in the ready snapshot with the\n rest of the sandbox, so every claim starts with them. A test that\n uploads a file changes only its own sandbox, and the data is gone when\n the sandbox is destroyed.\n\nTHE ENV YOUR APP GETS\n\n Every service, prepare command and action runs with the provider's\n standard client variables:\n\n s3 AWS_ENDPOINT_URL_S3 http://127.0.0.1:9000\n AWS_ACCESS_KEY_ID cofactor-sandbox\n AWS_SECRET_ACCESS_KEY cofactor-sandbox-secret\n AWS_REGION us-east-1\n\n gcs STORAGE_EMULATOR_HOST http://127.0.0.1:4443\n GOOGLE_CLOUD_PROJECT sandbox\n\n The credentials are fixed and the same in every sandbox. If your app\n reads different names (say `ATTACHMENTS_ENDPOINT`), set them in your\n definition's `env:`, which also overrides any of the above (an\n environment's `env:` too; your image's own ENV does not):\n env:\n ATTACHMENTS_ENDPOINT: http://127.0.0.1:9000\n ATTACHMENTS_BUCKET: attachments\n\nS3: CLIENT SETTINGS\n\n AWS SDK v3 (JavaScript) and boto3 1.28 or later read\n `AWS_ENDPOINT_URL_S3` themselves. Your app must use PATH-STYLE\n addressing (`http://host/bucket/key`, not `http://bucket.host/key`):\n\n // AWS SDK v3\n new S3Client({ forcePathStyle: true })\n\n # boto3\n boto3.client(\"s3\", config=Config(s3={\"addressing_style\": \"path\"}))\n\n Older SDKs that don't read `AWS_ENDPOINT_URL_S3`: pass it as the\n client's endpoint yourself.\n\nGCS: CLIENT SETTINGS\n\n Point your client at `STORAGE_EMULATOR_HOST`. Google's Node\n (`@google-cloud/storage`) and Python (`google-cloud-storage`) clients\n read it themselves and skip authentication when it is set, so `new\n Storage()` and `storage.Client()` work with no credentials. On an older\n Python client that doesn't, pass both yourself:\n storage.Client(\n credentials=AnonymousCredentials(),\n client_options={\"api_endpoint\": os.environ[\"STORAGE_EMULATOR_HOST\"]},\n )\n For another language, set your client's endpoint to\n `STORAGE_EMULATOR_HOST` and give it no (anonymous) credentials.\n\nLINKS A BROWSER OPENS: THE PUBLIC URL\n\n `STORAGE_EMULATOR_HOST` and `AWS_ENDPOINT_URL_S3` are only reachable\n inside the sandbox. A presigned (S3) or signed (GCS) URL your app hands\n to a browser must use the `object-storage` port's public URL instead.\n Its origin is a `{port}` param, which you pass to your app:\n params:\n storage_url: { port: object-storage }\n env:\n STORAGE_PUBLIC_URL: ${params.storage_url}\n\n The service that reads it needs `reload_on_prepare: true`, like any\n service that reads a param (see the `upload` topic).\n\n Anyone with that URL reaches the storage: S3 with the fixed credentials\n above, GCS with no authentication at all. It holds the sandbox's test\n data, like every other port of the sandbox, so never put real data in\n it.\n\n S3: sign the URL against the public origin with a second client used\n only for presigning (signing makes no network call), and keep every\n real read and write on the default one:\n const presigner = new S3Client({\n endpoint: process.env.STORAGE_PUBLIC_URL,\n forcePathStyle: true,\n });\n const url = await getSignedUrl(presigner,\n new GetObjectCommand({ Bucket: 'attachments', Key: key }));\n The name is yours: an app that already has such a setting (e.g.\n `S3_PRESIGN_PUBLIC_ENDPOINT: ${params.storage_url}`) just sets it. The\n public URL preserves the request's Host, so the signature holds, and\n browser uploads through a presigned PUT are allowed from any origin.\n\n GCS: generate signed URLs with the public origin as their host (Node:\n the `host` option of `file.getSignedUrl`; Python: the\n `api_access_endpoint` argument of `blob.generate_signed_url`). Signing\n still needs a credential that can sign, such as a service account key\n file; the emulator doesn't check signatures, so give the sandbox a\n throwaway key, never a real one. Signed downloads and signed uploads\n work. A browser must upload in ONE signed PUT: a resumable upload\n started from a browser is answered with an address inside the sandbox\n (127.0.0.1) that the browser can't reach, so resumable uploads work\n only from inside the sandbox (your app's server, an action).\n\nSEEDING FILES WITH AN ACTION\n\n An action can put files in a bucket, so a seed can start every claim\n with them. If your app records each file in its database (an attachment\n row pointing at the object), go through the app's own upload API\n instead, so the row exists too — the same rule as any other seed (see\n `seeds-and-actions`). For a file the app only reads (a logo, an import\n template), write it straight to the bucket. The examples need only\n curl (7.75 or later, for `--aws-sigv4`):\n\n #!/usr/bin/env bash\n # sandbox/actions/add-template.sh (S3)\n set -euo pipefail\n curl -fsS --aws-sigv4 \"aws:amz:$AWS_REGION:s3\" \\\n --user \"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY\" \\\n -H 'content-type: text/csv' -T sandbox/files/import-template.csv \\\n \"$AWS_ENDPOINT_URL_S3/exports/templates/import-template.csv\" >&2\n echo '{\"key\": \"templates/import-template.csv\"}'\n\n #!/usr/bin/env bash\n # sandbox/actions/add-template.sh (GCS)\n set -euo pipefail\n query='uploadType=media&name=templates/import-template.csv'\n curl -fsS -X POST -H 'content-type: text/csv' \\\n --data-binary @sandbox/files/import-template.csv \\\n \"$STORAGE_EMULATOR_HOST/upload/storage/v1/b/exports/o?$query\" >&2\n echo '{\"key\": \"templates/import-template.csv\"}'\n\nLIMITS\n\n - One provider per app.\n - GCS: one upload request carries at most 128 MB, a signed PUT from a\n browser included.\n - GCS: a browser uploads only through a single signed PUT (above).\n - The data lives only in the sandbox: nothing is copied from or to a\n real bucket, and it goes when the sandbox is destroyed. It is\n reachable on the public `object-storage` URL: test data only.\n","A sandbox can declare several ports, and each one gets its own public URL:\n ports:\n web: { port: 3000, access: public }\n api: { port: 3001, access: public }\n\nWHY SEVERAL PORTS\n\n Bundling web and API into one sandbox means a single claimed environment\n serves both UI flows (driving the browser against `web`'s URL) and API\n sequences (calling `api`'s URL directly) against the *same* underlying\n data — the workspace, projects and tasks a UI test just created through\n the browser are the same rows an API test can immediately query.\n There's no separate \"API sandbox\" to keep in sync with the \"UI sandbox.\"\n\n`access`\n\n `public` gets a real, routable URL a test or a human can hit from\n outside the sandbox. `private` stays internal — reachable by other\n services in the sandbox, not from outside it.\n\n`{port}` PARAMS\n\n A `params` entry of the form `{port: <name>}` resolves to that port's own\n public URL, so the app (and its build/prepare steps) can be handed its\n own public origin rather than hardcoding it:\n params:\n web_url: { port: web }\n api_url: { port: api }\n env:\n PUBLIC_URL: ${params.web_url}\n API_URL: ${params.api_url}\n\n Each `{port}` param gives the app its own public URL for exactly the port\n it names — `web_url` always resolves to `web`'s URL, `api_url` to `api`'s,\n and so on for however many ports a definition declares.\n\n `{ port: emulation }` is also allowed when the definition declares an\n emulator under `emulation:`. It resolves to the emulator's public origin\n (the implicit `emulation` port), which is the issuer for the `oidc`\n plugin:\n params:\n issuer_url: { port: emulation }\n Without a declared emulator, `emulation` is an undeclared port and\n validation fails.\n\nLIMITS\n\n At most 8 ports total per sandbox — a platform limit. That count\n includes the implicit `emulation` port that gets added automatically the\n moment any plugin is declared under `emulation:` (see the `emulation`\n topic) — it isn't optional and isn't something you declare yourself, but\n it does count against the 8. `emulation` is also a reserved port name: you\n can't declare your own port called `emulation`. The same goes for the\n implicit `object-storage` port that `object_storage` adds (see the\n `object-storage` topic), and the implicit public `sync` port every\n sandbox has, which `cofactor sandbox sync` writes your working tree\n through (see the `local` topic): a definition can declare at most 7 ports\n of its own (fewer with `emulation` or `object_storage`), and `sync` and\n port 7099 are reserved. `cofactor sandbox\n validate` checks the reservations and the 8-port cap. The `sync` port is\n never shown as one of the sandbox's URLs.\n\n Ports 80 and 443 are reserved for the emulator whenever emulation is on —\n a definition can't put its own `web` or `api` port there in that case.\n\nOAUTH CALLBACKS\n\n Map the callback through the application's existing configuration, e.g.\n GOOGLE_REDIRECT_URI: ${params.api_url}/auth/callback. Use the port that\n serves the actual callback, not necessarily the web port. Services that\n read this environment variable at startup need reload_on_prepare: true.\n Read `cofactor sandbox guide v2 authentication` before using live: true or\n configuring Google emulation.\n\n https://docs.runcofactor.com/reference/sandbox-emulation#callback-configuration-and-app-changes\n","Two different checks, at two different costs.\n\n`cofactor sandbox validate [file]`\n\n Offline — no sandbox, no claim. Parses the YAML, runs the full\n schema, and cross-checks everything static analysis can catch: unknown\n actions or operations referenced by a seed, `${id.key}` references to a\n later step or an undeclared output key, an `extends` cycle, a duplicate\n step id across a chain, an unknown emulation plugin, port 80/443 declared\n while emulation is on, and more. Every issue is reported with a dotted\n path to exactly where it is, so a broken definition doesn't need a claim\n to find the problem:\n environments.default.seeds[0].input.workspace_id: references step \"acme\", which has not run yet (or does not exist)\n environments.free-plan.extends: unknown environment \"defualt\"\n ports.emulation: \"emulation\" is a reserved port name\n\n Run this before pushing any change to `cofactor.sandbox.yaml` — it's\n instant and catches most authoring mistakes before they cost a real\n claim. It is the only local check; only a claim proves the definition.\n\n`cofactor sandbox verify-env --application A [--environment E]`\n\n Actually claims each requested environment of the application's current\n version (or every environment in the definition, if none is named) in turn:\n claim, wait for ready, report every seed step's output and timing, sign\n every persona in as a test's Login step would (when the definition\n declares `authentication`), then destroy. This is the only way to prove an\n environment's seed chain and persona logins actually run end to end —\n `validate` can tell you a reference is well-formed, but only a real claim\n proves the referenced action produces the output at runtime, and that\n `authentication.run` works in the image (a command the image lacks, a\n wrong URL). Each persona prints `signed in`, or FAILED with the script's\n exit code and the end of its stderr; any failure fails the environment.\n Without a pinned version, a GitHub application uses the default\n environment's head (or the default branch's tip when that head is stale).\n A default branch without a definition is not ready for unpinned checks.\n\n[cli >= 0.56.0]\n`cofactor sandbox verify-env --application A --at <sha> [--environment E]`\n\n Reads and verifies that pushed commit's definition before any merge.\n Use either a branch SHA or the full source SHA printed by\n `claim --from-local`. The latter creates its source object without\n changing the user's HEAD or index. After the last edit, validate and\n make a fresh claim from local files or the final branch SHA, then run\n this check. A previously synced copy alone is not evidence that the\n source snapshot builds from scratch.\n\n Check the UI at the claimed copy's web URL and run a workflow with\n `cofactor workflow run <flow> --sandbox <id>` before merge. For one\n existing copy, `cofactor sandbox auth-check <id> --persona P` checks a\n native persona without a workflow. Merge only when the user wants\n default-branch adoption; unpinned refresh and verify-env check that\n adoption afterwards. See `cli`, SETTING UP A NEW GITHUB APPLICATION.\n[/cli]\n\nWHEN A SEED FAILS\n\n Seeds run once per version (commit or push) and environment, while its\n ready snapshot is built. Any step's failure fails that snapshot — the\n builder is destroyed, so there's no partially-seeded sandbox left around\n to poke at — and the claims waiting on it fail with that error. The next\n claim builds the snapshot again, so a seed that fails every time keeps\n failing until a fixed source version is supplied. An infrastructure failure,\n by contrast, is retried by the build, within the claim's time limit\n (`cofactor sandbox claim --timeout`, 2 hours by default).\n\n To debug a failing seed:\n 1. Read the error — the claim (or `verify-env`) reports which step\n failed, its action, and the script's stderr (everything a script\n prints besides its final `{...}` result line goes to stderr, so\n this is where the real error message is).\n 2. Claim a sandbox **without** the failing environment (or on an\n environment that stops short of the failing step) so you have a\n live sandbox to work on.\n 3. Run the failing action by hand against that live sandbox with\n `cofactor sandbox run <id> <action> --input k=v ...`, using the\n same inputs the seed step would have resolved (substitute in the\n actual values for any `${id.key}` references by hand). This lets\n you iterate on the action's script without repeatedly re-claiming.\n 4. Once the action succeeds standalone, fix the seed definition or the\n action script and confirm the full chain on a fresh source version.\n For a pushed image, push the fixed image and rerun verify-env.\n[cli >= 0.56.0]\n For GitHub, validate, claim --from-local again, and verify-env --at\n its new source SHA, or use a fixed pushed branch SHA. Neither path\n requires a merge or a user commit during local iteration.\n[/cli]\n","All commands are under `cofactor sandbox`, and every sandbox belongs to an\napplication (`--application <name|id>`). `--env` is the CLI's *global*\nflag for which Cofactor stage to talk to (`dev` / `prod`) — it has nothing\nto do with a sandbox's environment, which is always `--environment`.\n\ncofactor sandbox validate [file] [--upload]\n Offline schema + cross-check of a `cofactor.sandbox.yaml`. Defaults to\n the definition in the current directory if `file` is omitted. `--upload`\n checks a definition for pushed images (no `setup`);\n without it, one built from a GitHub checkout. See the `verify` topic for\n the issue format.\n\ncofactor sandbox connect --application A --repo owner/name [--dir D]\n Builds the application's sandboxes from a GitHub repository: the\n `cofactor.sandbox.yaml` under `--dir` (default: the repo root) at each\n commit. Its environments become the application's sandbox environments\n and its personas the application's personas. A new application name\n creates the application, with its sandbox `default` environment as its\n default environment; there is no base URL to give. It is created only\n after GitHub is connected (`cofactor github connect`), the repository is\n reachable and its definition is valid, so a refusal creates nothing.\n Without a pinned commit, connect reads the definition at the default\n branch's head; that version must hold a valid cofactor.sandbox.yaml.\n The selected definition supplies environments and personas. Later\n definitions add more when connected or refreshed; none is ever removed.\n\n[cli >= 0.56.0]\ncofactor sandbox connect --application A --repo owner/name [--dir D] --at <sha>\n Reads the definition at that full pushed commit SHA, on any branch. No\n merge is needed, even if the default branch has no definition yet. The\n response describes the selected version; added environments and personas\n are retained if the branch is abandoned. An unknown commit says to push\n it first; a missing definition names the path and commit problem.\n[/cli]\n\ncofactor sandbox push --application A <image> [--definition F] [--tag T]... [--wait]\n For an application that gives Cofactor no repository access: pushes a\n built image (with its `cofactor.sandbox.yaml`) as the application's next\n version. A new application name creates the application, as `connect`\n does. See the `upload` topic.\n\ncofactor sandbox envs --application A [--json]\n The application's delivery (GitHub or pushed images), its current\n version, each environment's head (see `refresh`), recent pushes, sandbox environments, and what its definition\n offers: every environment (descriptions, seeds), ports, personas,\n actions and emulator operations (with their schemas). This is what an\n agent reads to decide which environment to claim and how to call each\n action once it has a sandbox.\n\ncofactor sandbox refresh --application A [--environment E] [--wait]\n Moves an environment's head to now. The head is what flow runs, the flow\n designer, scheduled runs and unpinned claims use: for GitHub, the ready\n snapshot of the default branch; for pushes, the newest push. It is never\n built per commit and never follows the default branch on its own, so it\n is normally behind it. It moves in two ways only: the background refresh\n (every 6 hours, for environments in use) and this command, which builds\n the default branch's tip as of this moment and advances the head when its\n snapshot is ready. A pushed application's head is already its newest push\n (\"already at head <push id>\"). Prints the old head, the new head and its\n build status; `--wait` blocks until the snapshot is ready or failed. A\n refresh while that snapshot is already building joins the build. Whoever\n can claim can refresh. `envs` shows each head and whether the default\n branch has moved past it, for information only.\n\ncofactor sandbox claim --application A [--environment E] [--at V] [--wait]\n Claims a sandbox: a fresh copy of the ready snapshot for the version and\n environment (built first — build, `prepare`, seeds — if it doesn't exist\n yet), given its own URLs and logins by `prepare`. A version is a commit\n SHA for a GitHub-delivered application, or a push id or tag for a pushed\n one. A commit may be on any branch you pushed (it need not be merged),\n and is built with the definition in that commit. Without `--at`: the\n environment's head (see `refresh`); for GitHub, the default branch\n snapshot, else the default branch's tip if the head is stale; for\n pushes, the newest push. `--environment` defaults to `default`. `--wait`\n blocks until the sandbox is ready (or failed) instead of returning\n immediately with a claim id you'd otherwise have to poll with `show`.\n\n[cli >= 0.56.0]\ncofactor sandbox claim --application A --from-local [--repo owner/name] [--dir D] [--environment E] [--wait]\n Sets up and claims the local files without a user commit or merge. The\n CLI snapshots the whole Git working tree (tracked files, tracked\n deletions, and nonignored untracked files), using a temporary index. It\n leaves your index, HEAD and branches alone. It pushes a deterministic\n commit object to refs/cofactor/wip/<tree-hash> with your own Git\n credentials, connects the application at that SHA, and claims it.\n --repo defaults to the origin push repository; --dir is relative to the\n repository root and defaults to the current directory. New application\n names work as with connect. --from-local and --at are mutually exclusive.\n The full source SHA is printed (the version field with --json); keep it\n for verify-env or a pinned suite. Unchanged files reuse the same SHA.\n\n Inclusion follows Git: an unignored untracked file is pushed too. Set\n ignore rules before claiming, and keep credentials out of tracked files.\n Push protection or a Git credential failure stops the claim and reports\n the push error. The CLI deletes its wip refs on destroy, or reaps them\n after seven days on a later local claim for that application. Another\n live claim keeps its ref. Cleanup uses your Git credentials; remote refs\n changed by someone else are left intact. Re-run --from-local to publish\n an expired ref again before rebuilding a lost snapshot. Deleting a ref\n does not erase GitHub's object storage immediately.\n[/cli]\n\ncofactor sandbox claims [--application A] [--json]\n Lists the sandboxes you claimed that are still claiming or ready: id,\n application, environment, status, when claimed and when the lease ends.\n Use it to find an id you lost, then `destroy` it.\n\ncofactor sandbox show <id>\n Current status, per-port URLs, persona logins (decrypted — this is where\n they're meant to be read from, never a PR comment), the snapshot's seed\n steps with the time each took, seed outputs, and the available actions\n and operations.\n\ncofactor sandbox run <id> <action|plugin.op> [--input k=v ...] [--json-input '{...}']\n Runs a definition action, an emulator operation (named `<plugin>.<op>`,\n e.g. `email.sent-emails`) or, when the definition declares `databases`,\n the built-in `database.fast-forward` (`--input database=<name>`; see the\n `data-fast-forward` topic) against a claimed sandbox, waits for it, and\n prints its output. `--input` is repeatable and coerced to the action/\n operation's declared type (string/number/integer/boolean/persona);\n `--json-input` passes the whole input as JSON, overriding `--input`.\n Invalid input prints the action or operation's input schema instead of\n running it, so you can see what's expected.\n\ncofactor sandbox exec <id> [--timeout S] [--json] -- <command...>\n Runs one shell command in a claimed sandbox, waits for it, and prints its\n output. One-shot: there is no interactive shell. It runs as the sandbox\n user in the definition's directory, with the environment actions get\n (the application's secrets included). Everything after `--` is the\n command, its words joined with single spaces (ssh semantics), so quote\n pipes and redirects yourself: `-- 'ls | wc -l'`.\n - Output is the last 4000 characters of stdout and of stderr, scrubbed of\n the sandbox's secrets and persona logins (best effort: an encoded value\n isn't caught). The CLI exits with the command's exit code (124 on a\n timeout). A failure that isn't the command's (the helper, or a sandbox\n that can't exec) prints `Error: ...` and exits 1. `--json` prints the\n whole run instead.\n - `--timeout S` is 1 to 1800 seconds (default 300). On a timeout the\n command's whole process group is killed. Background processes (`cmd &`)\n keep running after a normal exit; redirect their output\n (`cmd > log 2>&1 &`), because the exec's pipes close when it returns.\n If the CLI stops waiting (its deadline is the timeout plus 60 s), the\n command may still be running in the sandbox.\n - Permissions: a signed-in CLI session (`cofactor login`) isn't\n scope-checked, so every org member can exec, just as they can run\n actions. An API key needs the `sandbox:exec` scope explicitly:\n `sandbox:*` does not include it, full access (`*`) does. Create a key\n with `sandbox:exec` selected under Sandboxes in Settings -> API keys (or\n `POST /settings/api-keys` with `\"scopes\": [..., \"sandbox:exec\"]`).\n Existing `sandbox:*` keys don't gain it, and a key's scopes can't be\n edited, so mint a new key.\n - Commands and their scrubbed output are visible in the sandbox's run\n history to anyone with `sandbox:read`. Don't type credentials into a\n command; set them as secrets (see the `secrets` topic).\n - Every exec is audited in the organization's events: who ran it (and\n with which API key), the sandbox, the scrubbed command, and how it\n ended (exit code, or a failure). Never its output. An exec that cannot\n be audited does not run.\n - Platform: works on sandboxes built on a platform that has exec. An older\n sandbox answers \"This sandbox was built on an older platform; exec works\n once it is rebuilt on a newer one.\" Exec ships with the next sandbox\n platform update (it needs no bump of its own).\n\ncofactor sandbox sync <id> [--build] [--json]\ncofactor sandbox sync <id> <local-dir> --to <path> [--json]\n Makes a sandbox you claimed match your working tree, uncommitted and\n untracked changes included, straight over the sandbox's own sync port.\n It lists every file it writes (untracked ones marked) and deletes, then\n applies, records the synced tree id (`show` prints it) and extends the\n sandbox's lease (up to 8 h after the claim). Nothing restarts on its own:\n `exec <id> -- cofactor-sandbox restart <service>`. `--build` reruns the\n definition's changed build steps and restarts `after_build` services\n (GitHub and pushed applications alike). `--to` mirrors local build output into <path>. Needs the same\n `sandbox:exec` scope as exec. See the `local` topic.\n\ncofactor workflow run <flow> --sandbox <id>\n Runs a flow on a sandbox you claimed instead of a fresh copy, and never\n destroys it. The run targets the sandbox's own application and environment.\n An `--environment` of another application or sandbox environment is refused.\n The run records the version and synced tree (`cofactor workflow debug overview <run>`). An API key also needs\n `sandbox:read`.\n\ncofactor sandbox destroy <id>\n Tears the sandbox down. Always do this when a claimed sandbox is no\n longer needed — it isn't reclaimed automatically mid-session.\n\ncofactor sandbox secret set <NAME> --application A [--environment E] [--json]\n Stores a third-party secret (an API key) for the application, for every\n environment, or for just `--environment E` (an override). The value is\n read from stdin only and the CLI refuses a terminal:\n `printf %s \"$KEY\" | cofactor sandbox secret set NAME --application A`.\n One trailing newline is stripped. A name is an environment variable name\n (capital letters, digits and `_`, not starting with a digit; not PATH,\n HOME, USER, SHELL, NODE_OPTIONS, NODE_EXTRA_CA_CERTS, LD_* or COFACTOR_*);\n a value is 8 to 8192 bytes, with no NUL. Fast copies of an existing\n ready snapshot keep its old value until a new commit or image push\n rebuilds the snapshot. Legacy-prepare claims see the change on the next\n claim. Prints `Secret NAME set for all environments of A.` (or\n `... for environment \"E\" of A.`). See the `secrets` topic.\n\ncofactor sandbox secret list --application A [--json]\n Names and scopes (all environments, or one) with the time last set.\n Never values. With none set it prints `No secrets set for A.`\n\ncofactor sandbox secret rm <NAME> --application A [--environment E] [--json]\n Removes the secret at that scope: without `--environment`, the\n application-wide one; with it, that environment's override. Prints\n `Secret NAME removed for all environments of A.` (or\n `... for environment \"E\" of A.`). A secret that is not set fails with\n \"No secret with that name and scope.\". Fast copies of an existing ready\n snapshot keep the removed value until a new commit or image push rebuilds\n the snapshot. Legacy-prepare claims see the removal on the next claim.\n See the `secrets` topic.\n\ncofactor sandbox verify-env --application A [--environment E]\n Claim → wait → report seed outputs and timings → sign every persona in\n (when the definition declares `authentication`) → destroy, for each\n environment of the current version (or the one named). The end-to-end\n proof that an environment's seed chain and persona logins actually work,\n not just that they're well-formed. Without a pinned version it verifies\n the application's default version. See the `verify` topic.\n\n[cli >= 0.56.0]\ncofactor sandbox verify-env --application A --at <sha> [--environment E]\n Verifies that pushed commit's own definition, environments and personas\n before any merge. Use the source SHA from claim --from-local for the\n same check without making a user commit.\n\ncofactor sandbox auth-check <id> --persona <name> [--json]\n Signs one persona in on an existing claim as a Login step would, prints\n pass/fail, and exits nonzero if sign-in fails. It returns no session state.\n[/cli]\n\ncofactor sandbox preview --application A --pr N\n A pull request's preview of a GitHub-delivered application: status, URLs,\n the logins GitHub deliberately never sees, and the URL of its Cofactor page\n (the same details plus build output on failure, for signed-in org members).\n The PR's GitHub deployment links that page as its \"View details\".\n To run a test on it (a private copy at its applied head, never the shared preview):\n cofactor workflow run <workflowId> --preview N --application A\n and to list tests in a pull request as proof: `cofactor proofs guide`.\n\ncofactor sandbox guide [topic]\n Prints this guide (`guide v2 [topic]` is the same). Omit `topic` to\n print every section; name one (e.g. `upload`) to print just that topic.\n `guide v1 [section]` prints the guide for the earlier sandboxes (its\n `verify` section is `guide v1 verify`; plain `guide verify` is this\n guide's).\n\nTHE CORE LOOP\n\n validate → (connect | push) → envs → claim --wait → run (actions/\n operations, as many as the test needs) → destroy. Always validate a\n changed definition before pushing it — see the `verify` topic for why.\n A GitHub-delivered application is never tried in local Docker (see\n `overview`).\n\nSETTING UP A NEW GITHUB APPLICATION\n\n Never build or run the GitHub app in local Docker or a local container.\n Validate locally; build and prove it with a Cofactor claim. Use CLI\n 0.56.0 or later for the following no-merge setup commands; upgrade an\n older CLI instead of merging an unproved definition to unlock setup.\n\n[cli >= 0.56.0]\n 1. Write cofactor.sandbox.yaml and run `cofactor sandbox validate` until\n clean. Review Git's ignored and untracked files before sending them.\n Keep third-party credentials in the application's secret store, never\n in the definition or local files sent to Git.\n 2. Choose the source:\n - Already pushed on a branch: use its full commit SHA, with the\n definition and scripts in that commit:\n cofactor sandbox connect --application A --repo owner/name --at <branch-sha>\n cofactor sandbox claim --application A --at <branch-sha> --wait\n Add --dir D to connect for a monorepo definition directory.\n - Local files, including a new uncommitted definition:\n cofactor sandbox claim --application A --from-local --wait\n This also connects the application. No user commit, branch push or\n merge is needed. Use --dir D from the repository root for a monorepo;\n --repo owner/name overrides origin's inferred GitHub name.\n For an existing application, set its required test secrets before\n the first claim (see `secrets`). If a new application is created and\n its first claim reports a missing secret, set that secret and retry\n with the same application name. Connect retains added environment\n and persona names; abandoning the branch does not prune them.\n 3. Iterate on that copy:\n cofactor sandbox sync <id> --build\n This adopts changed lifecycle.build steps, services and env, and\n restarts the affected services (see `local`). Existing action script\n files and authentication.run scripts also apply after a plain sync.\n Data and applied migrations stay. Changes to setup, resources,\n ports, prepare, seeds, personas, actions or other nonsyncable\n definition fields need a fresh snapshot: validate, then claim again\n with --from-local, or use a newly pushed branch SHA. A failed claim\n has no running copy to sync; fix the files and claim again by the\n same path. Use --from-local for retries without a user commit or merge.\n 4. Prove a fresh source snapshot, rather than just a synced copy. After\n the last edit, validate and claim --from-local again (or claim the\n final pushed SHA). Record its full source SHA, then:\n cofactor sandbox verify-env --application A --at <source-sha>\n This checks each environment and signs every declared native persona\n in. Open the claimed copy's web URL too and confirm the app renders\n and each role lands where it should. For a focused sign-in check:\n cofactor sandbox auth-check <id> --persona <name>\n A workflow can run before merge:\n cofactor workflow run <flow> --sandbox <id>\n A suite can pin the same SHA with `cofactor test-suite run --at`.\n Destroy your claims when finished. Merge only when the user wants\n the proven definition and code adopted on the default branch. At\n that point commit and push the files for review, and prove that final\n SHA if it differs; after merge, refresh --wait and unpinned verify-env\n check default-branch adoption. They are not prerequisites to setup.\n[/cli]\n","PUSHED IMAGES: SANDBOXES WITHOUT REPOSITORY ACCESS\n\nAn application whose team gives Cofactor no access to its repository\ndelivers its sandboxes as built images: build your app's image as usual,\nthen push it with its `cofactor.sandbox.yaml` whenever you have a new\nbuild (nightly, per release, per merge — your cadence, your CI or your\nlaptop). Each push is a version of the application. A tag names the newest\nready push that carries it: pushing `--tag nightly` again moves the tag to\nthe new push, and older pushes stay claimable by their id.\n\nTag your main builds `main` (or `nightly`). These two tags are reserved: once\nthe application has a push carrying either, its head (what the flow designer,\nscheduled runs and claims without `--at` use) is the newest push tagged `main`\nor `nightly`, and only those runs move the visual-review baseline. Pull\nrequest and branch pushes are compared against them and change nothing. Until\nthen the newest push is the head. Claim any other push with\n`--at <push id or tag>`.\n\nUse tags that move (`nightly`, `release`, a branch name), not one per push\nsuch as a date. Cofactor keeps the layers of each application's newest push,\nits head, and its 10 newest tagged pushes. Older pushes stay claimable, but when the\nsandbox platform is updated only kept pushes move to it on their own; an\nolder one says to push again.\n\n docker buildx build --platform linux/amd64 -t tracker:sandbox --load .\n cofactor sandbox validate --upload cofactor.sandbox.yaml\n cofactor sandbox push --application \"Project Tracker\" tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n cofactor sandbox envs --application \"Project Tracker\"\n cofactor sandbox claim --application \"Project Tracker\" --environment default --wait\n\n`push` adds Cofactor's layers to your image (its sandbox helper as the\nentrypoint, plus the object storage server when the definition declares\n`object_storage`) and a small layer with your definition, reads the image\nout of your local Docker (`docker save`), and uploads only the layers\nCofactor doesn't already have for your organization (see FAST RE-PUSHES).\nCofactor boots it once until every service is healthy (`--wait` waits for\nthat). Then each environment in the definition\nbecomes a sandbox environment of the application, and each persona one of\nits personas. The first claim of an environment builds its ready snapshot\n(prepare + seeds, about a minute); later claims copy it in seconds.\n\nWHAT THE IMAGE MUST BE\n\n - linux/amd64. On an Apple Silicon Mac, build with\n `--platform linux/amd64`; `push` refuses any other platform.\n - glibc-based with bash (Debian, Ubuntu, `python:*-slim`, `node:*`\n non-alpine…). Alpine/musl and distroless images do not work: the\n helper is a glibc binary and every command runs through `bash -lc`.\n If only your final stage is Alpine, keep the build stages and change\n that one `FROM` to its Debian equivalent (`node:24-alpine` →\n `node:24-bookworm-slim`, `python:3.12-alpine` → `python:3.12-slim`),\n with `apt-get install` in place of `apk add`. Native dependencies\n compiled on Alpine don't run on glibc: install them in a glibc stage\n rather than copying `node_modules` or wheels out of an Alpine one.\n - A WORKDIR other than `/`. Services, prepare commands and actions run\n from it, and the helper gives it to the sandbox user (below).\n - Built already. Nothing builds at push or claim: everything the app\n needs (dependencies, compiled assets, a database server if it uses one)\n is in the image. Optional `lifecycle.build` steps run only on\n `sync --build` (the `local` topic), which needs `git` installed in the\n image: without it the build fails with `Executable not found in $PATH: \"git\"`.\n\nFAST RE-PUSHES\n\n A push uploads only the layers that changed since an earlier push (in\n the last 30 days); `push` says how much it sent and how much Cofactor\n already had. These keep a re-push small:\n\n - Order the Dockerfile so what changes most comes last: system packages,\n then the dependency manifest and lockfile and the install step, then\n your source. A code change then rebuilds, and uploads, only the last\n layers:\n FROM python:3.12-slim\n RUN apt-get update && apt-get install -y --no-install-recommends curl jq\n WORKDIR /app\n COPY requirements.txt .\n RUN pip install -r requirements.txt\n COPY . .\n - In CI, keep the build cache between runs. A runner that starts with an\n empty cache rebuilds every step, and every rebuilt layer is new to\n Cofactor even when nothing in it changed. With GitHub Actions, build\n with Docker's official actions, which pass the Actions cache to\n buildx for you:\n - uses: docker/setup-buildx-action@v4\n - uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: app:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n (or `type=registry` with a cache image of your own). Docker's own\n actions are pinned by major version here, like `actions/checkout`;\n pin them to a commit if your policy requires it. A raw\n `docker buildx build ... --cache-from type=gha` in a `run:` step\n can't see the Actions cache token and skips the cache without a\n warning. If you must use one, add the community action\n `crazy-max/ghaction-github-runtime` before it, pinned to a full commit\n (`@04d248b84655b509d8c44dc1d6f990c879747487`, v4.0.0): it exposes the\n Actions runtime token to every later step of the job.\n - For compiled stacks (.NET, Java, Rust), build outside Docker and keep\n the Dockerfile thin. `RUN --mount=type=cache` (NuGet, Maven, cargo,\n incremental `obj/` or `target/` directories) is not exported by\n `--cache-to type=gha`, so inside a Dockerfile those caches are empty\n on every CI run and the whole compile reruns. Compile on the runner\n with `actions/cache` for the compiler's incremental state, and have\n the Dockerfile only `COPY` the published output onto a base image\n holding what rarely changes (database server, runtimes, seed data;\n rebuild it only when its inputs change). Three details decide whether\n the re-push is actually small:\n - Incremental compilers (MSBuild, make, cargo) compare file mtimes.\n A fresh `git clone` stamps every file \"now\" and invalidates the\n cached state. Before compiling, set tracked files to an old date\n (`git ls-files -z | xargs -0 touch -h -d '2000-01-01'`), then\n touch the files changed since the commit you last built. Keep its\n SHA in the cache entry, check out with `fetch-depth: 0` so it is\n present, and skip this on a cache miss (that build is full anyway):\n `git diff -z --name-only --diff-filter=d <last-sha> HEAD | xargs -0 -r touch -h`.\n - Layers are digested by content including mtimes, and the parent\n directories a COPY creates can get the build time, so identical\n files can yield a new layer digest on every build. Stage the\n output under its final path (`staged/opt/app/...`), pin every\n mtime in it including the directories\n (`find staged -exec touch -h -d '2000-01-01' {} +`), and use\n `COPY --link staged/ /` (BuildKit).\n - Split third-party files from your own into separate COPYs so a\n code change rewrites only the small one.\n\n CI authenticates with an API key (`COFACTOR_API_TOKEN`). Give it only\n what the job does: `sandbox:read`, `sandbox:publish` and `property:read`\n to push (plus `property:create` if CI's push may be the first, which\n creates the application); add `sandbox:create` and `sandbox:delete` to claim and destroy,\n `sandbox:run` to run actions, and `sandbox:exec` to run shell commands\n (not included in `sandbox:*`). Create it from the CLI (the secret prints\n once): `cofactor api-key create --name \"CI image push\" --scope sandbox:read\n --scope sandbox:publish --scope property:read`.\n\n Push main builds with `--tag main` (or `nightly`). For pull requests, push\n each with its own tag and claim it by that tag:\n cofactor sandbox push --application \"Project Tracker\" app:sandbox --tag pr-123 --wait\n cofactor sandbox claim --application \"Project Tracker\" --environment default --at pr-123 --wait\n A pull request's push shares every layer below the source with main's.\n\nNIGHTLY PUSH FROM GITHUB ACTIONS\n\n A complete `.github/workflows/cofactor-nightly.yml` for the example\n application \"Acme Tracker\": it builds the image every night (and on\n demand), pushes it tagged `nightly`, then runs your suite on that push.\n Replace the application and suite names with yours.\n\n Before the first run, create the CI key (see above) and store it as the\n repository secret COFACTOR_API_TOKEN. For the last step add `--scope\n test-run:run --scope test-run:read`; without that step the push scopes\n are enough.\n\n name: cofactor-nightly\n on:\n schedule:\n - cron: \"0 3 * * *\"\n workflow_dispatch:\n concurrency:\n group: cofactor-nightly\n cancel-in-progress: false\n permissions:\n contents: read\n jobs:\n nightly:\n runs-on: ubuntu-latest\n timeout-minutes: 180\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm install -g @runcofactor/cli\n - uses: docker/setup-buildx-action@v4\n - name: Build the image\n uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: acme-tracker:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n - name: Check the definition\n run: cofactor sandbox validate --upload cofactor.sandbox.yaml\n - name: Push the image\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: |\n cofactor sandbox push --application \"Acme Tracker\" acme-tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n - name: Run the suite on the new push\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: cofactor test-suite run --suite \"Acme Tracker nightly\"\n\n The CLI reads `COFACTOR_API_TOKEN` from the environment (no login step);\n only the steps that call Cofactor get it, and `validate` needs none.\n The build step's cache settings are the ones under FAST RE-PUSHES.\n `--wait` follows the import for up to 50 minutes and fails the job if\n Cofactor cannot boot the image. A push still importing after 50\n minutes is not a failure: the job moves on and the suite runs on the\n previous head. The suite runs on the application's head: the newest push\n tagged `main` or `nightly`. Start the workflow by hand with \"Run\n workflow\" (the `workflow_dispatch` trigger) to test a change to this\n file. `timeout-minutes` covers a build, the longest import wait and the\n suite; raise it if your suite runs longer.\n\n The last step is optional: a suite can also run on a Cofactor schedule.\n\nMAKING YOUR IMAGE SYNCABLE\n\n `cofactor sandbox sync` writes your working tree into a claimed sandbox\n (see `guide v2 local`). For a pushed image it syncs the directory holding\n cofactor.sandbox.yaml into the image's WORKDIR, so the image must keep\n that directory's files at WORKDIR in the same layout. The rule: a\n syncable image has your definition file at its WORKDIR, as `COPY . .`\n gives you.\n\n WORKDIR /app\n COPY . .\n RUN npm ci && npm run build\n\n A multi-stage build that copies only some directories works too, as\n long as each lands at the same relative path under WORKDIR and the\n definition file is copied in:\n WORKDIR /app\n COPY --from=build /src/api /app/api\n COPY cofactor.sandbox.yaml /app/cofactor.sandbox.yaml\n\n Files the image doesn't ship (sources it compiles away, docs) are fine:\n `sync` writes them as new files and nothing runs them.\n\n Compiled output isn't rebuilt in the sandbox (nothing builds at push or claim; `sync --build` reruns a declared `lifecycle.build`):\n build it locally, then sync the result to where the image serves it:\n npm run build\n cofactor sandbox sync <id> dist --to public\n\n `push` checks the rule on the local image and says one of:\n Sync: syncable.\n Sync: syncable (the image is older than your working tree, which is fine).\n Sync: not syncable: cofactor.sandbox.yaml isn't at /app/cofactor.sandbox.yaml (...)\n Sync: not checked (...) (Docker, or git, couldn't answer)\n It never stops a push. It reads the definition at your WORKDIR, not the\n copy `push` adds for Cofactor itself.\n\nHOW YOUR IMAGE RUNS IN A SANDBOX\n\n - Its ENTRYPOINT and CMD are NOT used. Start each process yourself with a\n `services.<name>.run` command (often your entrypoint script).\n - Its ENV and WORKDIR ARE applied, as `docker run` would, its PATH\n included: commands run through a `bash -lc` login shell, and the image's\n own PATH is put back in front of whatever the login profile sets.\n - Leave the image's USER as root (or unset). The sandbox starts as root\n and drops to uid 1000 itself; an image that ended on another `USER`\n has failed to boot with a permission error on `/etc/cofactor`.\n - Everything — services, prepare commands, actions — runs as an\n unprivileged user with uid 1000 (the sandbox adds that account if your\n image has none). The WORKDIR tree is made theirs; anything the app\n writes must be under the WORKDIR or `/tmp`, not elsewhere in a root-\n owned path.\n - One machine, no Docker inside: a database is a process you run as a\n service (Postgres installed in the image, or SQLite), not a container.\n For S3 or GCS, declare `object_storage` and the sandbox runs the\n server for you (see the `object-storage` topic).\n - Each public port gets its own https URL, unique per sandbox. Its\n origin is a param — `params: { web_url: { port: web } }` — that you\n pass to the app through `env` (e.g. `PUBLIC_URL: ${params.web_url}`).\n\nTHE DEFINITION FOR PUSHED IMAGES\n\n The same `cofactor.sandbox.yaml` as for a repository (see the\n `definition` topic), minus installing the toolchain: no `setup`. `lifecycle.build`\n and `after_build` are optional and run only on `sync --build`. A minimal one for the example app,\n built as one image that serves its UI and API on port 8080:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n ports:\n web: { port: 8080, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n services:\n app:\n run: exec ./start.sh\n health: { http: 8080, path: /health }\n reload_on_prepare: true\n personas:\n owner: { email: owner@example.test }\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n actions:\n create-workspace:\n description: A new workspace, created by the given persona, who becomes its owner.\n run: sandbox/actions/create-workspace.sh\n input:\n name: { type: string, required: true }\n as: { type: persona, required: true }\n output:\n workspace_id: { type: string }\n environments:\n default:\n description: Workspace \"Acme\", owned by the owner persona.\n seeds:\n - id: acme\n action: create-workspace\n input: { name: Acme, as: owner }\n\n From here, add what your tests need, in the order \"WHAT TO BUILD FIRST\"\n in the `seeds-and-actions` topic gives: a persona per role, an\n environment per plan or flag combination, and actions for the core\n objects.\n\n Keep the file anywhere in your repo: `validate --upload <file>` and\n `push --definition <file>` read it from where you point them, and the\n push puts it in the image. Paths inside it (`run:` of actions, prepare\n commands) are relative to the WORKDIR, so ship your sandbox scripts in\n the image (e.g. `COPY sandbox/ sandbox/`).\n\n Services start when the image is first booted, BEFORE any sandbox exists,\n with params at placeholder values. A service that needs the sandbox's own\n values (its URL, a `{secret: true}` param) or reads a secret sets\n `reload_on_prepare: true`: it is started (and restarted) by each\n sandbox's prepare instead. In doubt, set it on your app's service.\n\n `health` passes when the http path answers below 400 (or the command\n exits 0) within 5 minutes of the service starting.\n\nPERSONAS: MAKING THE LOGINS WORK\n\n Cofactor generates a fresh password for every persona when a snapshot is\n built; every sandbox copied from it has the same ones. Your\n `lifecycle.prepare` commands (which also run on each copy) receive them as `COFACTOR_PERSONAS`, a\n JSON map `{ \"<persona>\": { \"email\": \"...\", \"password\": \"...\" } }`, and\n must make each login work in your app: create or update that user with\n that password (through your app's API, its CLI, or its database). A\n persona whose login doesn't work breaks every test that signs in as it.\n A persona's `email` is yours to choose: pick an account your app already\n has (its seed data) and set its password, or have prepare create it.\n Cofactor's test runs then sign in with these logins automatically, as\n the application's persona of the same name.\n\n Prepare runs while your app is up: the `reload_on_prepare` services are\n running and healthy (restarted first when their environment changed),\n the others have run since the image was booted. So write the logins\n somewhere the running app reads on every sign-in (its database, or its\n own API). If your app loads its accounts once at startup (a file or\n config read into memory), change it to read them per request, or the new\n passwords won't take. For an app whose logins are fixtures, a common fix\n is a small JSON file the prepare script writes from `COFACTOR_PERSONAS`\n and the app checks on each sign-in before its fixtures.\n\nACTIONS AND SEEDS\n\n An action is a script in the image with typed input and output (the\n `seeds-and-actions` topic covers designing them). Inputs arrive as\n `INPUT_<NAME>` env vars (a `persona` input carries the persona's name,\n e.g. `owner`), alongside the definition's `env:`, every param as\n `COFACTOR_PARAM_<NAME>`, and `COFACTOR_PERSONAS`. The script prints its\n result as a JSON object on the LAST stdout line that starts with `{`\n (send everything else to stderr). It runs inside the sandbox, so it\n reaches your app on localhost. An environment's `seeds` call actions in\n order, once per version, before the snapshot every claim copies;\n `${<step id>.<output>}` passes one step's output to a later step. Run\n one against a live sandbox with `cofactor sandbox run <id> <action>\n --input name=... --input as=owner`.\n\n An action that signs in as a persona and creates data through the app's\n own API (here a cookie-session JSON API on port 8080; the port, paths and\n JSON fields are the example app's, use your app's). It needs jq and\n curl, which slim base images lack: add them to your Dockerfile\n (`apt-get install -y --no-install-recommends jq curl`).\n\n #!/usr/bin/env bash\n # sandbox/actions/create-workspace.sh\n set -euo pipefail\n login=$(jq -c --arg p \"$INPUT_AS\" '.[$p] | {email, password}' <<<\"$COFACTOR_PERSONAS\")\n jar=$(mktemp)\n curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n -d \"$login\" http://127.0.0.1:8080/api/auth/login >&2\n id=$(curl -fsS -b \"$jar\" -H 'content-type: application/json' \\\n -d \"$(jq -nc --arg n \"$INPUT_NAME\" '{name: $n}')\" \\\n http://127.0.0.1:8080/api/workspaces | jq -er '.id')\n jq -nc --arg id \"$id\" '{workspace_id: $id}'\n\n\nTROUBLESHOOTING\n\n - `push` says the image is not linux/amd64, or its WORKDIR can't be\n used: rebuild as it says.\n - The push fails with \"Cofactor could not start this image\": a service\n exited or never turned healthy. The message ends with that service's\n log. To reproduce, open a shell in the image —\n docker run --rm -it --entrypoint bash <image>\n — and run that service's `run` command from the WORKDIR. In the\n sandbox it runs as uid 1000 with the WORKDIR made theirs; plain\n `docker run --user 1000` leaves the WORKDIR root-owned, so a write\n failure there is not the sandbox's.\n - A claim fails in prepare or a seed: the error carries the command's\n output. Fix the script, rebuild the image and push again.\n - To try prepare and actions before pushing, run the app as usual and\n call your scripts in it with the variables they read (a sandbox also\n sets the definition's `env:` and runs them as uid 1000):\n docker run -d --name try -p 8080:8080 <image>\n docker exec -e COFACTOR_PERSONAS='{\"owner\":{\"email\":\"owner@example.test\",\"password\":\"pw-1\"}}' try bash sandbox/personas.sh\n docker exec -e COFACTOR_PERSONAS='…' -e INPUT_NAME=Acme -e INPUT_AS=owner try bash sandbox/actions/create-workspace.sh\n (`docker exec` starts in the image's WORKDIR.)\n - `cofactor sandbox logs` and `diagnostics` are for the older sandboxes;\n for these, `cofactor sandbox show <id>` has the status and any error.\n - \"push the image again\": Cofactor's sandbox helper changed since that\n push; a fresh `cofactor sandbox push` picks up the current one.\n - A layer larger than 5 GB is refused; split or slim it.\n - Every push uploads the whole image: the build cache is missing (see\n FAST RE-PUSHES), or the Dockerfile copies the source before\n installing dependencies.\n","YOUR UNCOMMITTED CHANGES IN A SANDBOX (SYNC)\n\nWrite a test next to your code and run it against your real app, before\nyou commit: claim a sandbox, sync your working tree into it, rebuild what\nthe change needs, and open it or run a flow on it. No commit, no image\nbuild, no pull request.\n\n cofactor sandbox claim --application \"Project Tracker\" --wait\n # edit files\n cofactor sandbox sync <id>\n cofactor sandbox exec <id> -- cofactor-sandbox restart app\n cofactor workflow run <flow> --sandbox <id>\n # edit, and sync again\n\n[cli >= 0.56.0]\nStart even the first sandbox from uncommitted files:\n cofactor sandbox validate\n cofactor sandbox claim --application \"Project Tracker\" --from-local --wait\n # edit and iterate on the same copy\n cofactor sandbox sync <id> --build\n\nThe CLI connects from a deterministic wip commit object using your Git\ncredentials; your HEAD and index stay untouched. For changes sync cannot\nadopt (setup, prepare, seeds, personas or actions), validate and claim\n--from-local again. After the final edit, make a fresh claim and verify\nits printed source SHA with `verify-env --at <sha>`. No merge or user\ncommit is required. See `cli`, SETTING UP A NEW GITHUB APPLICATION.\n[/cli]\n\nYou decide what to rebuild and restart: a sync only writes and deletes\nfiles. Each sync extends the sandbox's lease by up to 2 hours, up to 8 hours\nafter the claim. Syncing needs the `sandbox:exec` scope for API keys.\n\nWHAT A SYNC DOES\n\n `sync` makes the sandbox's files match your working tree, the way git\n sees it: tracked files with your uncommitted edits, plus untracked files\n that aren't ignored. Ignored files (node_modules/, .env if you ignore it,\n build output) are not synced. Your git index is never touched.\n\n - GitHub application: your repository's top level syncs into the\n sandbox's checkout. Run `sync` anywhere inside the repository.\n - Pushed image: run `sync` from the directory holding\n cofactor.sandbox.yaml; it syncs into the image's WORKDIR (see\n \"Making your image syncable\" in `guide v2 upload`).\n\n Before it writes anything, `sync` prints the files it will write (each\n untracked one marked), the files it will delete, and what it keeps. Check\n for a stray untracked file you didn't mean to send, such as an unignored\n .env.\n\n It ends with the synced tree id, which `cofactor sandbox show` and every\n flow run on the sandbox record. A later commit of exactly these files has\n the same tree (`git rev-parse <commit>^{tree}`, or `<commit>:<dir>` for a\n pushed image's directory).\n\nDELETIONS\n\n A file only the sandbox has is deleted only when all three hold:\n - it isn't in your working tree,\n - your ignore rules don't match it, and\n - some commit in your local history once had a file at that path.\n So a file you deleted, or a directory you removed, is deleted in the\n sandbox, while everything your image or build put there (node_modules/,\n .venv/, compiled assets, a script your Dockerfile copied elsewhere) is\n kept. `sync` lists both.\n\n Syncing again is always safe: it compares against the sandbox's real\n files, so a sync that stopped part-way converges on the next one.\n\nREBUILD AND RESTART\n\n Services keep running the old code until you restart them:\n cofactor sandbox exec <id> -- cofactor-sandbox restart app\n `restart` names one or more services, waits until they're healthy, and\n prints each one's status. An unknown name lists the sandbox's services.\n\n Install dependencies, run migrations or a build with `exec` first: it runs\n in the same directory and environment your services use (the definition's\n directory in the checkout, or a pushed image's WORKDIR):\n cofactor sandbox exec <id> -- npm ci\n cofactor sandbox exec <id> -- npx prisma migrate deploy\n\n Logs: each service writes /var/lib/cofactor/logs/<service>.log.\n cofactor sandbox exec <id> -- tail -n 100 /var/lib/cofactor/logs/app.log\n\n Let the definition do it (GitHub and pushed applications alike):\n cofactor sandbox sync <id> --build\n reruns each `lifecycle.build` step whose `inputs` changed (and every step\n after it), then restarts the `after_build` services. Only this flag runs\n them: a sync without it, a push and a claim never do. A pushed image's\n steps run in its WORKDIR and come from the definition baked into the image,\n so changing them takes a new `push`. The first `--build` on a claimed\n sandbox has no record of earlier runs, so it runs every step.\n\nCOMPILED OUTPUT\n\n When the sandbox runs compiled output (a bundled frontend, compiled\n TypeScript), build it locally and sync the result to where the sandbox\n serves it:\n bun run build\n cofactor sandbox sync <id> frontend/dist --to static\n `--to` is relative to the sync root (the checkout, or the image's\n WORKDIR). This mirrors the local directory as is: ignore rules don't\n apply, so a git-ignored dist/ is sent; every file in it is written, and\n every other file under --to is deleted.\n `show` and runs then record it as build output.\n\nDATA IS FORWARD-ONLY\n\n A sync changes files, never data. A migration you ran stays applied when\n you sync older files back, and seeded rows stay. To start over, destroy\n the sandbox and claim a new one.\n\nRUN A FLOW ON IT\n\n cofactor workflow run <flow> --sandbox <id>\n\n The run uses this sandbox instead of claiming a fresh copy, and never\n destroys it. The run targets the sandbox's own application and\n environment (no --environment needed; naming another one is refused), the\n sandbox must be ready, and it must be one you claimed, not a test run's. The run records the version and the synced tree it ran\n against, as of when it started; `cofactor workflow debug overview <run>`\n shows them. Syncing during a run changes the files under it, so sync\n between runs.\n\nLIMITS\n\n - Sync only sandboxes claimed with `cofactor sandbox claim`; a test run's\n own copy can't be synced.\n - Symlinks and submodules are not synced (`sync` lists them).\n - A single file larger than 63 MiB is refused; a sync of many files is\n sent in several requests.\n - The `sync` port counts toward the 8-port limit (see `guide v2 ports`).\n","import type { GuideSection } from '../types.js';\nimport { renderSupportMatrix } from './support.js';\n\n// Read by the CLI's drift test (guides.test.ts: every row reaches `guide feasibility`)\n// and, from slice 2 of the spec, by `cofactor sandbox check`.\nexport { SANDBOX_V2_SUPPORT_MATRIX } from './support.js';\n\nimport overview from './overview.txt';\nimport feasibility from './feasibility.txt';\nimport definition from './definition.txt';\nimport environments from './environments.txt';\nimport seedsAndActions from './seeds-and-actions.txt';\nimport dataFastForward from './data-fast-forward.txt';\nimport personas from './personas.txt';\nimport authentication from './authentication.txt';\nimport env from './env.txt';\nimport secrets from './secrets.txt';\nimport emulation from './emulation.txt';\nimport objectStorage from './object-storage.txt';\nimport ports from './ports.txt';\nimport verify from './verify.txt';\nimport cli from './cli.txt';\nimport upload from './upload.txt';\nimport local from './local.txt';\n\nexport const SANDBOX_V2_GUIDE_SECTIONS: GuideSection[] = [\n { title: 'Sandboxes v2 Overview', slug: 'overview', content: overview },\n // feasibility.txt ends at its SUPPORT heading; the matrix is appended there.\n { title: 'Feasibility: Check What the App Needs First', slug: 'feasibility', content: feasibility + renderSupportMatrix() },\n { title: 'Definition (cofactor.sandbox.yaml)', slug: 'definition', content: definition },\n { title: 'Environments (Flavors)', slug: 'environments', content: environments },\n { title: 'Seeds vs. Actions', slug: 'seeds-and-actions', content: seedsAndActions },\n { title: 'Dumps and Stale Dates (database.fast-forward)', slug: 'data-fast-forward', content: dataFastForward },\n { title: 'Personas', slug: 'personas', content: personas },\n { title: 'Authentication', slug: 'authentication', content: authentication },\n { title: 'Environment Variables (env)', slug: 'env', content: env },\n { title: 'Secrets (Third-Party Keys)', slug: 'secrets', content: secrets },\n { title: 'Emulation', slug: 'emulation', content: emulation },\n { title: 'Object Storage (S3 or GCS)', slug: 'object-storage', content: objectStorage },\n { title: 'Ports', slug: 'ports', content: ports },\n { title: 'Pushed Images (No Repository Access)', slug: 'upload', content: upload },\n { title: 'Your Uncommitted Changes in a Sandbox (sync)', slug: 'local', content: local },\n { title: 'Verification', slug: 'verify', content: verify },\n { title: 'CLI Reference', slug: 'cli', content: cli },\n];\n","/**\n * CLI-version-aware guide sections.\n *\n * A guide `.txt` may wrap text that needs a newer CLI in whole-line markers:\n *\n * [cli >= 0.54.0]\n * cofactor proofs attach ... --perf-run <prun_...>\n * [/cli]\n *\n * Only `>=` is supported and blocks do not nest. Markers are not code-fence\n * aware. The API filters the served copy with the caller's `cliVersion`; the\n * CLI filters its bundled copy with its own version. Both call this function.\n */\n\n// Digit groups are capped because `cliVersion` is caller-supplied on a public route: observed with a\n// 400-digit major, Number() returns Infinity and the gate shows every block (or hides all via NaN).\nconst OPEN = /^\\s*\\[cli >= (\\d{1,6}\\.\\d{1,6}\\.\\d{1,6})\\]\\s*$/;\nconst CLOSE = /^\\s*\\[\\/cli\\]\\s*$/;\n/** Any line that starts like a marker but is not a valid one is an authoring error. */\nconst MARKER_LIKE = /^\\s*\\[\\/?cli\\b/;\nconst SEMVER = /^(\\d{1,6})\\.(\\d{1,6})\\.(\\d{1,6})(?:[-+][0-9A-Za-z.+-]{0,40})?$/;\n\ntype Semver = readonly [number, number, number];\n\n/** `X.Y.Z` (leading zeros allowed); a short `-prerelease` / `+build` suffix on the CLI version is ignored. */\nfunction parseSemver(value: string): Semver | null {\n const match = SEMVER.exec(value);\n return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;\n}\n\n/** True when `value` is a CLI version `filterGuideForCli` accepts. The API route validates with this before filtering. */\nexport function isCliVersion(value: string): boolean {\n return parseSemver(value) !== null;\n}\n\nfunction atLeast(a: Semver, b: Semver): boolean {\n for (let i = 0; i < 3; i++) {\n if (a[i] !== b[i]) return (a[i] as number) > (b[i] as number);\n }\n return true;\n}\n\n/**\n * Keep `[cli >= X]` blocks when `cliVersion` meets X (marker lines dropped);\n * otherwise replace the block with a one-line upgrade notice.\n * Throws on an invalid `cliVersion` or an unclosed, nested, stray or malformed marker.\n */\nexport function filterGuideForCli(content: string, cliVersion: string): string {\n const cli = parseSemver(cliVersion);\n if (!cli) {\n throw new Error('Invalid cliVersion');\n }\n // Unmarked guides (all of them today) skip the line scan.\n if (!content.includes('[cli') && !content.includes('[/cli')) return content;\n const out: string[] = [];\n let openAt: number | null = null;\n let keep = true;\n\n const lines = content.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNo = index + 1;\n const open = OPEN.exec(line);\n if (open) {\n if (openAt !== null) {\n throw new Error(`Nested cli marker at line ${lineNo} (open block from line ${openAt})`);\n }\n openAt = lineNo;\n const minimum = open[1] as string;\n keep = atLeast(cli, parseSemver(minimum) as Semver);\n if (!keep) {\n out.push(`(Needs cofactor ${minimum} or later. Run \\`cofactor upgrade\\` to see this.)`);\n }\n } else if (CLOSE.test(line)) {\n if (openAt === null) {\n throw new Error(`[/cli] at line ${lineNo} without a matching [cli >= X]`);\n }\n openAt = null;\n keep = true;\n } else if (MARKER_LIKE.test(line)) {\n throw new Error(`Malformed cli marker at line ${lineNo}: \"${line.trim()}\"`);\n } else if (keep) {\n out.push(line);\n }\n }\n\n if (openAt !== null) {\n throw new Error(`Unclosed [cli >= X] marker opened at line ${openAt}`);\n }\n return out.join('\\n');\n}\n","An ADE is your remote coding environment. An agent can inspect and apply laptop\nsetup without a terminal. Only browser approvals need the person.\n\n1. Plan without changing the laptop or sending credentials:\n cofactor dev setup --plan --json --env <dev|prod> --org <org> [--template <name>]\n Read every decision, its findings, recommended default, consequences and exact\n flags. The plan lists the selected published template's repositories. It never\n prints token values or file contents. No sign-in command runs during planning.\n\n2. Ask the person which logins and file groups they want to send. Explain:\n --github=token reuses their current gh token; it creates no new GitHub key.\n Both clone and push use HTTPS with gh auth git-credential in the ADE.\n --github=none sends no GitHub token. Private repos need browser sign-in inside\n the ADE before clone/push can work; none does not mean repos need no login.\n --claude=token uses a dedicated CLAUDE_CODE_OAUTH_TOKEN from the environment or\n the local ADE cache, never laptop OAuth refresh credentials.\n --claude=in-box leaves Claude sign-in to the attached ADE browser flow.\n --codex-key=yes sends an explicit OPENAI_API_KEY and uses API billing.\n --codex-key=no leaves Codex to in-box browser sign-in when selected.\n --git-identity=yes sends the global author name/email, not authentication.\n --git-identity=no leaves them for the user to configure before committing.\n --include=none sends no optional files. Or combine any of claude-settings,\n claude-skills, claude-memory, codex-config,shell with commas. Review these\n locally: settings/dotfiles can contain secrets and laptop-specific commands.\n Claude settings.env, OAuth auth.json, symlinks and hardlinks are excluded.\n\n3. Only the plan's needsYou browser steps require the person. Each supplies the\n exact command prefixed with ! for the person to run in their local terminal.\n GitHub: !gh auth login --hostname github.com --git-protocol https --web\n Claude: !claude setup-token\n The latter creates a dedicated token. Keep it out of chat/logs and export it\n as CLAUDE_CODE_OAUTH_TOKEN in the environment running setup; an existing ADE\n token cache is also accepted. Browser steps are conditional on choosing token;\n choosing none/in-box defers sign-in to the attached ADE. Run the plan again\n after the person finishes; the agent should never request a token in chat.\n\n4. Apply the person's complete explicit choices without prompts, for example:\n cofactor dev setup --yes --github=token --claude=token --codex-key=no --git-identity=yes --include=none --env dev --org <org>\n All five decision flags are required even when skipping. Missing decisions\n error with --plan guidance; --yes never launches browser login or a prompt.\n Choices and transfer consent are saved privately on the laptop. Credentials\n are read again at each claim, never stored in the choices file. If a selected\n credential disappears, setup errors with guidance instead of silently changing\n the person's choices.\n\n5. Claim with the same org/environment/template:\n cofactor dev --env dev --org <org> [--template <name>]\n New homes receive the selected items privately through SSH, without asking\n again. Existing prepared homes reuse their saved settings and setup marker.\n Re-running setup changes laptop choices for new homes; it does not overwrite\n an already prepared home. Interactive `cofactor dev setup` remains available\n for humans and uses the same decisions and consequences.\n\nLaptop SSH private keys never leave the laptop. There is no SSH file group and\nno deploy key is needed. The separate ADE attach key stays on the laptop; only\nits public half is authorized for access to the session. It is unrelated to\nGitHub cloning/pushing. Personal AWS files are never imported; explicitly select\napproved organization AWS connections at claim if you need AWS access.\n","import type { GuideSection } from '../types.js';\nimport setup from './setup.txt';\n\nexport const DEV_GUIDE_SECTIONS: GuideSection[] = [\n { title: 'Set up an ADE with an agent', slug: 'setup', content: setup },\n];\n","The product map describes THE PRODUCT — what it does today, in the\npresent tense, as a person who has never seen a test run would need it\nexplained. It is not a log of what a tester did, what a run tried, or\nwhat a cycle found. Anyone in the company should be able to read a\ncapability and understand the feature; nobody should have to know which\nagent wrote it or when.\n\nTWO VOICES, TWO HOMES\n\n Product voice — title, description, body, rules.\n Describes the feature as it is. Present tense. No actors other than\n the product's own users and roles. No \"run\", \"worker\", \"cycle\",\n \"session\", \"tested\", \"attempted\", \"corrected\", \"documented\".\n\n Tester voice — setup notes, coverage-blocked reason, open questions.\n First-hand notes for the next person who has to reach and exercise\n this capability: what has to exist first, why regression coverage\n cannot be added, what is still unclear. Direct, specific, and about\n testing.\n\n Persona notes are product voice: they state the limit as the product\n presents it to that type of user, never what a run saw.\n\n Never mix them inside one field. A run observation in the body is a\n defect; a product description in the coverage-blocked reason is noise.\n\nTWO LAYERS, NEVER MERGED\n\n The map is about the application under test. Every field would still\n be true if the tester, the credential, the run, and the tool that wrote\n it had never existed. Nothing on the map names a credential, a run, a\n cycle, a sandbox, a worker, or \"we\".\n\n What a tester has seen and could reach — which capabilities were\n exercised, which were unreached and why, where the leads are — is a\n separate exploration layer, shown beside the map, never written into\n it. A sentence about reach belongs there; a sentence about the product\n belongs on the map.\n\nTHE LOG TEST\n\n Before writing a sentence into the body, ask: would it still be true,\n and useful, to someone who never saw the run that produced it? If it\n reports something that HAPPENED (\"was rejected on the first try\",\n \"an initial ordering was corrected before saving\", \"was not further\n documented\", \"during this run\") it is a log line. Either translate it\n into the product fact it reveals, or move it to the tester's fields.\n\n Log line (wrong, body):\n \"The tested checkout required a shipping address before payment;\n an initial address-less attempt was corrected before submitting.\"\n\n Product fact (right, added as a rule):\n \"Payment requires a shipping address; checkout blocks the payment\n step until one is entered.\"\n\n Log line (wrong, body):\n \"Bulk-delete behaviour was not further documented.\"\n\n Tester note (right, open questions):\n \"Does bulk delete respect the per-row permission, or delete\n everything selected?\"\n\nREWRITE, NEVER APPEND\n\n A field is a current statement, not a journal. When new evidence\n changes a section, rewrite that section. Never append a dated entry,\n a \"last run\" note, or an \"update:\" paragraph to any field — the\n revision history already records who changed what and when.\n","The product map states what a product does. It has exactly two levels —\nareas group capabilities, and capabilities are what a user can accomplish.\nThere is no third level: a capability's detail lives in its own fields\n(description and body), not in a separate entity.\n\nAREAS ARE THIN\n\n An area exists to group, not to document. It carries a title, a\n one-line description, a sort order, and a code stem — nothing else.\n Resist the urge to pile detail into an area's description; that detail\n belongs on its capabilities.\n\n Area titles are noun phrases naming a product surface a stakeholder\n would recognize on sight:\n\n Good:\n - \"Checkout & Payments\"\n - \"Account Management\"\n - \"Reporting & Exports\"\n\n Bad:\n - \"Checkout\" (verb-shaped, reads as an action not a surface)\n - \"Backend Team\" (names a team, not a product surface)\n - \"Misc\" or \"Other\" (a junk drawer defeats the point of grouping)\n\n If a capability has no obvious home, leave it in the Unassigned bucket\n rather than inventing a catch-all area for it. An unassigned capability\n is visible and honest; a junk-drawer area quietly becomes permanent.\n\nCAPABILITIES ARE THE RICH ENTITY\n\n Everything that documents behavior — what it does, how it varies, what\n \"working\" looks like — lives on the capability, not the area. See the\n Naming and Capability Bodies sections for how to write those fields\n well.\n\n[cli >= 0.54.0]\nAGENT AREAS\n\n An area has a kind: feature (the default) or agent. An agent area\n is for what an AI agent can do and how that is proven. Write its\n capabilities and rules exactly as you would a feature's, in the\n product's voice; only the proof differs. Evals prove them (see\n Coverage), not flows. Create one with `cofactor product-map area create\n \"<title>\" --kind agent`, or change an existing area with\n `area update <ARE-…> --kind agent`. The kind adds an Agent badge in the\n app and changes nothing else about the area.\n[/cli]\n\nREAD BEFORE YOU WRITE\n\n Read the whole map when it fits; otherwise search for every title you are about to use and read the area you are writing into. A map built one write at a time without ever seeing the relevant slice drifts into duplicates and inconsistent naming within a single conversation.\n\nLIFECYCLE STAGES\n\n Every area, capability and rule has a status: in_dev, alpha, beta, ga,\n or deprecated. New items default to in_dev unless a stage is supplied.\n Never infer a later stage from observed working behavior. Never set a\n child's stage above its parent: rule <= capability <= area, ordered\n deprecated < in_dev < alpha < beta < ga. Unassigned capabilities have\n no area constraint. Every stage except deprecated is live and visible.\n Promote parents first; lower or deprecate children first. A conflicting\n change is refused and names the parent or children; nothing cascades.\n CLI and MCP call this stage; API request/response fields are status.\n","Capability titles are user goals: verb-first, singular, in the user's own\nwords. A capability names one thing a user can accomplish end-to-end —\nnot a feature area, not a UI element, not an internal mechanism.\n\n Good:\n - \"Apply a discount code at checkout\"\n - \"Invite a teammate to the organization\"\n - \"Export a report as CSV\"\n\n Bad:\n - \"Discount codes\" (a noun, not a goal — too broad to test)\n - \"Shopper applies a discount code\" (third-person narration, not a\n verb-first instruction)\n - \"Checkout\" (this is the area, not a capability inside it)\n\nTHE ONE-FLOW TEST\n\n A capability should be provable end-to-end by one flow. Ask: \"could one\n flow prove this from start to finish?\" If the honest answer is \"well,\n it depends which part,\" the capability is too broad — split it into the\n distinct goals hiding inside it. \"Manage billing\" hides at least\n \"Update a payment method,\" \"Download an invoice,\" and \"Cancel a\n subscription\"; write those as three capabilities, not one.\n\n Conversely, don't split a single user goal into artificial fragments\n just because it touches multiple screens. \"Apply a discount code at\n checkout\" is one capability even if it involves a cart page and a\n payment page — it is still one thing a user sets out to do.\n\nTHE DESCRIPTION LINE\n\n The one-line description says what the capability is and who it's for,\n in a sentence a non-technical stakeholder could read and immediately\n understand. It is not a restatement of the title and not a summary of\n the implementation — save behavior detail for the body.\n\nREUSE BEFORE YOU MINT\n\n Before proposing anything, read the complete current map. Reuse an\n existing area or capability by its exact identity whenever the new\n need matches one, even if the wording you'd have chosen differs\n slightly. Minting a near-duplicate with different phrasing (\"Apply a\n promo code\" beside an existing \"Apply a discount code at checkout\") is\n the single fastest way a map stops being legible — coverage math\n silently splits across two entities that mean the same thing.\n\n When you are unsure whether something is a genuine duplicate or a\n distinct goal, that uncertainty itself is worth surfacing rather than\n guessing — see Asking Well.\n","A capability is a verb-first title, a one-line description, an entry URL pattern, an importance, a body, and a set of rules. Every one of them is in the product voice (see Voice).\n\nTITLE\n Verb-first, naming what the user can do. See Naming.\n\nDESCRIPTION\n One line: who it is for + what it is.\n\nENTRY URL PATTERN\n Where the capability starts: a path pattern with :param placeholders (e.g. /projects/:id/settings). Never a full origin, never a one-off id. Null when no single entry exists.\n\nIMPORTANCE\n p0 business-critical, p1 important, p2 normal (default), p3 minor.\n\nBODY\n\n A short product document a stakeholder could read cold. Three headings, in this order:\n\n ## Overview — one to three sentences: what the user accomplishes and why it matters\n ## Steps — the happy path as an ordered list, from the entry point to done, in the user's terms\n ## Outcome — what is observably true when it worked: what the user sees, what now exists\n\n Example (a generic invoicing product):\n\n ## Overview\n A user sends an invoice to a customer by email from the invoice's page.\n\n ## Steps\n 1. Open an invoice with status Draft.\n 2. Click Send and confirm the recipient address.\n 3. Click Send invoice.\n\n ## Outcome\n The invoice status changes to Sent, the send appears in the invoice's activity list, and the customer receives the email with a PDF attached.\n\n The body never repeats what a first-class field already says (entry URL, setup notes, blocked reason, open questions, persona verdicts, rules). An update rewrites the affected section rather than appending a dated log.\n\nRULES\n\n A capability's constraints and meaningfully different paths live as their own records, added to the capability — never a heading inside the body. Each rule is one bullet's worth: one observable constraint or one meaningfully different path — a required input, a state the capability must be in, a limit, a role difference. Name the persona when a rule only applies to one.\n\n Continuing the invoicing example above, the capability carries these rules:\n\n - Only Draft invoices can be sent; a Paid or Void invoice has no Send action.\n - The recipient defaults to the customer's billing email and can be changed for this send only.\n - Viewers can open the dialog but the Send button is disabled for them.\n\n Write a rule only when it was observed or confirmed. When something is unclear, it is an open question, not a rule stated as a guess. Never invent detail.\n\nHISTORY\n\n Edits to a capability are kept as revisions: who made each one, when, and the body as it stood. A body that was blanked or rewritten wrongly is restored from its last good revision, not redrafted from memory. A restore is a person's edit, so the body it puts back is theirs, and any rules the old body carried in its text come back as rule records rather than as a heading in the body.\n","Every area and capability gets a permanent natural code — an area gets a\nshort stem (e.g. \"ARE-CMM\"), and a capability gets a stem plus a running\nnumber (e.g. \"CAP-CMM-001\"). Codes exist so a capability can be cited\nstably in a ticket, a report, or a conversation without repeating its\nfull title.\n\nSTEMS ARE MECHANICAL, NOT CONVENTIONAL\n\n The stem is derived from the title by a fixed rule, and that rule does\n not know your domain's conventions. It will produce results a person\n would not have chosen — for example \"Reports\" derives the stem \"RPR\",\n not the more intuitive \"RPT\". This is expected, not a bug to route\n around: don't rename a title purely to chase a \"nicer\" code.\n\nCODES ARE PERMANENT\n\n Once assigned, a code is never reused and never freed — not even when\n the area or capability it names is deprecated. A deprecated\n capability's code stays retired, not available for the next thing\n that happens to get a similar title.\n\nWHAT CAN RE-CODE (AND WHY IT'S RARE)\n\n Only two events cause a re-code:\n - An area's title changes enough to re-derive its stem.\n - A capability moves to a different area (its stem depends on its\n area).\n\n When a re-code happens, the old code is retired into an alias table so\n that anything citing it — a ticket, a saved link — still resolves to\n the current entity. This means a re-code is safe for existing\n references, but it is still a real event: don't treat area renames or\n capability moves as free actions taken lightly. Rename an area only\n when its scope has genuinely changed, and don't promise a stakeholder\n that a specific code will survive a move.\n","A capability carries an importance: p0 (business-critical), p1\n(important), p2 (normal — the default), or p3 (minor). Set it thinking\nabout what breaks for the business if this capability silently stops\nworking, not how technically complex it is to test.\n\nLINKING IS A CLAIM, NOT A GUARANTEE\n\n Linking a flow to a capability is a claim that the flow covers it — it\n is not, by itself, proof that the capability works. The actual\n coverage verdict (covered, failing on a known issue, stale,\n uncovered, or blocked) is derived separately: from that flow's\n recent run health over a rolling window, and from the issues\n linked to the capability. A link with\n no healthy recent run behind it does not mean the capability is\n tested; it means someone asserted it should be.\n\n Never tell a user a capability is tested just because a link exists.\n If asked about coverage, speak to the derived verdict, not the\n presence of a link.\n\n[cli >= 0.54.0]\nEVALS PROVE AGENT CAPABILITIES\n\n An eval is a repeatable test of an agent, run against a fixed set of\n cases. It proves a capability (typically in an agent area) the way a flow\n does for a feature. Link one with `cofactor product-map capability\n link-eval <CAP-…> --eval <name>` (an eval's `proves.capabilities`\n does the same at `eval push`), and a rule with `rule link-eval <prule_…>\n --eval <name>`. The eval must belong to the capability's application;\n links to an eval in another application or organization are refused.\n\n Coverage counts only passing FULL eval runs: every case, at the eval's\n own repeats and pass bar, on the eval's default model. A partial run\n (`--case`) or a run of another model proves nothing, and neither moves\n the verdict. A capability is covered when any linked flow or eval has\n passed recently. An eval that never passed, or whose latest full run\n failed, is red: with an open issue explaining it (see Known Bugs) the\n capability reads failing on a known issue, exactly as for a flow;\n without one it is stale (it passed before) or uncovered (it never\n did, and no eligible flow is linked). An eval whose last pass is old\n is stale: rerun it. A rule is proven while a linked eval's latest\n full run passed.\n[/cli]\n\nKNOWN BUGS ARE LINKED ISSUES\n\n A bug that breaks a capability is an issue linked to it — to the\n capability, or to the one rule it breaks (`cofactor issues link <id>\n --capability CAP-… [--rule prule_…]`). The link is what coverage reads:\n\n - Failing on a known issue: every flow and eval linked to the capability\n is red (never passed, failing, or degraded; for an eval, never passed\n or its latest full run failed) and an open issue is linked. A flow\n set to blocked on an open issue counts as explained too, and the\n issue it cites counts as linked. The red is explained, so the flow\n stays honest and red; it is not uncovered and not untriaged. It is\n counted on its own, never as covered. A green flow, or an eval whose\n latest full run passed recently, stays covered whatever is linked.\n - Blocked by an issue: no flow or eval is linked yet and an open issue is\n linked — the bug is why coverage can't be built. This is derived; do\n not also write a blocked reason for it.\n - Rerun due: a linked flow is still red and a linked issue closed after\n it last ran (or it has never run). Rerun the flow. A pass makes the capability covered; a failure means the\n issue is not fixed — reopen it or file a new one.\n\n A red flow with no linked open issue is a failure nobody has triaged\n yet. Triage it before linking anything: link an issue only when the\n evidence shows the product is wrong, not the test.\n\nWHERE UNMAPPED COVERAGE LIVES\n\n A count of flows that don't map to any capability belongs to the\n application as a whole — it is not a property of any one area. Don't\n attribute an application-wide unmapped-coverage count to a specific\n area; it says something about the map's completeness in general, not\n about that area's completeness in particular.\n\n\nRULE EVIDENCE\n\n [cli >= 0.54.0]\n A rule is proven by a workflow node or assertion (below) or by an eval\n (see Evals Prove Agent Capabilities); it is proven when any link is.\n [/cli]\n\n Link a rule to the node that proves it, or to one authored assertion on that\n node. Use rule ids from product-map show --json and node/assertion ids from\n workflow get. The rule and workflow must be in the same application and org;\n deprecated rules cannot gain links. Both commands are idempotent:\n\n cofactor product-map rule link <prule_…> --workflow <wf_…> --node <nodeId> [--assertion <assertionId>] --org <org> --env <env>\n cofactor product-map rule unlink <prule_…> --workflow <wf_…> --node <nodeId> [--assertion <assertionId>] --org <org> --env <env>\n\n show (including --json) returns each rule's links and coverage:\n - gap: no links.\n - unproven: links exist, but none passed in its workflow's latest finished run.\n - proven: at least one link passed in its workflow's latest finished run.\n\n Judge the linked node/assertion, not the whole run: a later failing node\n does not un-prove an earlier passed node or assertion in that finished run.\n An unfinished run does not replace finished evidence. Archived workflows\n remain listed but never prove a rule. An assertion must actually have\n passed, not merely been configured or skipped. A standalone assertion's id\n is its node id. Rule evidence is separate from capability coverage health.\n\n Fork promotion carries fork-linked evidence to mapped surviving origin\n nodes/assertions while retaining the fork rows until the fork is deleted.\n Sync and revision/snapshot restore preserve links for surviving node and\n assertion ids and drop missing evidence. Re-link a newly authored replacement\n when it has no mapped origin identity.\n\n[cli >= 0.57.0]\nSCREEN CHECKPOINTS PROVE RULES\n\n A linked visual checkpoint proves a rule when its latest qualifying suite capture matches the accepted screen with the same masks; step success alone is not proof. An active screen regression makes the rule Failed.\n[/cli]\n","There is no structured choice mechanism here — clarification happens in\nplain conversation. That makes it easy to skip when it's inconvenient;\ndon't. These are the situations where asking first is the difference\nbetween a legible map and a guessed one.\n\nALWAYS ASK OR PROPOSE-AND-CONFIRM FIRST\n\n - A candidate title actually names two different jobs (\"Update profile\n and change password\") — ask which one is meant, or propose splitting\n it into two capabilities and confirm before creating both.\n - A capability could plausibly live under two different areas — pick\n the one that seems right, propose it, and ask rather than silently\n filing it.\n - A candidate title is close enough to an existing one (small wording\n difference, same apparent meaning) that it might be the same thing\n wearing different words — ask whether to update the existing entity\n instead of minting a new one.\n - Deprecating an area — permitted only after all children are deprecated —\n always requires confirmation first; this cannot be undone quietly.\n - Replacing a capability's existing flow links (rather than adding to\n them) always requires confirmation first; it removes someone else's\n coverage claim.\n - There is no application in scope to work against — list the\n available applications and ask which one, rather than guessing.\n\nOTHERWISE, PROCEED AND REPORT\n\n Don't stall on wording the user can trivially correct afterward. A\n reasonable title, a reasonable one-line description, a thin honest\n body — these are all fine to write and then summarize back to the\n user, who can adjust in a follow-up. Reserve asking for the cases\n above, where a wrong guess is expensive to undo (duplicated entities,\n deprecations, lost coverage claims) rather than merely\n imperfect wording.\n","These fields are in the tester's voice (see Voice): notes for the next person or agent who has to reach and exercise the capability.\n\nSETUP (setup_md) — markdown, two parts:\n\n **Needs**\n - <a record or state that must exist before the capability can be exercised, one bullet each>\n\n **Steps**\n 1. <where and how a tester creates it — the screen, the action, the safe test data to use>\n 2. <next step>\n\n Setup describes how to reach a testable state, not what a particular run created. Never end it with a \"last set up\" or \"created during this run\" line — the revision history carries when it was written.\n\nBLOCKED REASON (blocked_reason) — one paragraph: why regression coverage cannot\n be added for this capability, and what would unblock it (a manual-only flow,\n an external integration, a gate that cannot be flipped in a test\n environment, a need for a real environment). Set by a person, never by an\n exploring agent. It is for the environment, not for bugs: a product bug that\n stops coverage is an issue linked to the capability, and coverage derives\n \"blocked by issue\" from that link (see Coverage). When such an issue\n explains a free-text blocker, clear the blocker and link the issue.\n\n An explorer that cannot reach a capability this cycle\n records that as an exploration outcome (unreached), not here — pursuing\n provenance and clearing the block on a later successful attempt is that\n outcome's doctrine, not this field's.\n\nOPEN QUESTIONS (open_questions_md) — a markdown bullet list, each bullet one question:\n\n - <behaviour that was seen but not understood>\n - <two observations that contradict each other, naming both>\n - <a boundary or variant nobody has been able to check yet>\n\n An open question is something a person with product knowledge could answer, or a later run could settle — it is not a block (that is the blocked reason) and not a gap padded into the body. When a question is answered, remove the bullet and write the answer where it belongs (add a rule to the capability, or a Needs line in setup). Rewrite the list; never append dated entries. Anything that leaks into the body as \"not yet documented\" or \"unclear whether…\" belongs here instead.\n\n A question on this list is a lead for whoever picks it up, not a quiz to read out. Some of them the application itself answers — which fields a form has, which options a control offers, what it does when you use it — and those are answered by going and looking, then adding a rule to the capability for what was seen. Reserve asking a person for what the application cannot show: intent, policy, and behaviour behind a system nobody can reach.\n","Personas are whatever the property defines; there is no hierarchy, never infer \"admin can\" from \"member can\".\n\nVerdicts per (capability, persona): can / cannot / limited. Limited always carries a note (\"own records only\"). Cannot note says how denial appeared (control absent, disabled, server error).\n\nA verdict describes access, not correctness. Contradictions between runs are reported, not silently resolved.\n\nA PERSONA VERDICT IS A FACT ABOUT THE PRODUCT, NOT ABOUT A RUN\n\n The persona is how a tester logs in; the verdict is what that TYPE OF\n USER can do in the product. Write the note as the product presents the\n limit — present tense, third person, no \"I\", no \"was available\", no\n \"this run\", no reference to the data that happened to exist when it was\n checked. What a run tried, found missing, or could not reach is an\n exploration outcome or a run-summary line; it never becomes a note.\n\n The test: would the note still be true if no test had ever run?\n\n Run narrative (wrong, note):\n \"No proposal review/apply control was available because the\n property had no pending proposal.\"\n\n Product fact (right): a rule on the capability says\n \"Proposal review and apply appear only while a proposal is pending.\"\n and the verdict is `limited` with the note\n \"Can only act on a pending proposal.\"\n\n Run narrative (wrong, note):\n \"Could not verify delete because no records existed as this user.\"\n\n Nothing to write: the verdict is `not_attempted`; what was missing\n goes in the run summary, and the setup notes say how to create a\n record first.\n","import type { GuideSection } from '../types.js';\nimport voice from './voice.txt';\nimport structure from './structure.txt';\nimport naming from './naming.txt';\nimport bodies from './bodies.txt';\nimport codes from './codes.txt';\nimport coverage from './coverage.txt';\nimport askingWell from './asking-well.txt';\nimport testability from './testability.txt';\nimport personas from './personas.txt';\n\nexport const PRODUCT_MAP_GUIDE_SECTIONS: GuideSection[] = [\n { title: 'Voice', slug: 'voice', content: voice },\n { title: 'Structure', slug: 'structure', content: structure },\n { title: 'Naming', slug: 'naming', content: naming },\n { title: 'Capability bodies', slug: 'bodies', content: bodies },\n { title: 'Codes', slug: 'codes', content: codes },\n { title: 'Coverage', slug: 'coverage', content: coverage },\n { title: 'Asking well', slug: 'asking-well', content: askingWell },\n { title: 'Testability', slug: 'testability', content: testability },\n { title: 'Personas', slug: 'personas', content: personas },\n];\n","Workflows are visual test sequences that model user journeys through your application. Each workflow is a directed graph of nodes — login, navigate, action, assertion, wait, condition, seed, and more — connected to form a testable path.\n\nWhen to use workflows:\n\n- Critical paths: signup, checkout, billing, onboarding\n- High-risk areas: features that regress often or have complex logic\n- Multi-step journeys: flows spanning multiple pages or user roles\n\nFor visual regression coverage, add one Visual checkpoint for each meaningful\nstate. Seed the state in Sandboxes v2 before capturing it. See\n`cofactor workflow guide visual-checkpoints` for native and Playwright examples.\n\nFLOW TYPES\n\n standard The default. Models a single user journey end-to-end.\n Login → Navigate → Actions → Assertions → Cleanup.\n\n setup Deprecated. Formerly a prerequisite workflow referenced by a\n setup node; use a seed workflow for shared data and suite\n layers for ordering instead.\n\n seed A data-seeding workflow that generates test data and exports\n variables for downstream workflows. Seed outputs are declared\n in the workflow definition and available as $var.{dataKey}\n in any workflow that references the seed.\n\n teardown A cleanup workflow that runs after a seed workflow completes.\n Automatically invoked to remove data the seed created.\n\nEXECUTION MODEL\n\n 1. Login node provisions an authenticated browser session\n 2. Nodes execute in sequence (or branch at condition nodes)\n 3. Each action node interacts with the live application\n 4. Assertions verify expected outcomes\n 5. Additional login nodes switch to a different user — each starts a\n new segment with a fresh session (great for multi-role testing)\n 6. Wait nodes pause execution (minutes, hours, days) — also creating\n segments. After a wait, a login re-establishes the session.\n 7. On completion, the run is marked success or failure\n\nVERSION HISTORY\n\n Workflows maintain version history. Publishing creates a versioned\n snapshot. Previous versions can be previewed or restored to draft.\n Seed nodes reference a specific published version of the seed workflow.\n","Every workflow is built from nodes. Each node has a type, a title, a description,\nand type-specific configuration.\n\nDEACTIVATING A NODE (any type except condition)\n Set `deactivated: true` in the node's config to skip it during runs — the\n step records status `skipped` and execution continues with the next node.\n Useful for converging a flow node-by-node from the front, or parking a\n chronically failing step without deleting it. Toggle via the CLI\n (`workflow node deactivate|activate <workflowId> <nodeId>`) or the node's\n three-dot menu in the Flow Designer. Two rules: a run is blocked while a\n later step still references a `$var` that only a deactivated step produces,\n and condition nodes cannot be deactivated (skipping the branch decision\n would run both branches — deactivate the branch nodes instead).\n\nSTARTING NODE — pick the first node in this order\n The first node of a flow follows a strict hierarchy. Getting this wrong is the\n single most common authoring mistake (most often: opening with a navigate node).\n\n 1. SEED first — if the flow depends on pre-existing data, a `seed` node comes\n first. It provisions data in its own isolated context and exports\n $var.{dataKey} outputs the rest of the flow consumes.\n 2. Otherwise LOGIN first — for any authenticated flow, the first node is a\n `login` node that declares the ACTOR (who the journey runs as). This is the\n \"acting as\" step. Do NOT open an authenticated flow with a navigate node.\n 3. Navigate comes AFTER login — a navigate jumps to the test's entry point, but\n it follows the login; it is never the opening node of an authenticated flow.\n\n The only flows that may start with a navigate are UNauthenticated ones — signup,\n password reset, a public/marketing page, or a test of the login experience\n itself. For those, set the login node's `performLogin: false` (or omit the login\n node) and author the steps as the journey.\n\n LOGIN SUBSUMES LOGGING IN. A login node already provisions an authenticated\n session — it IS the act of logging in. Never emit a login node and then a\n \"navigate to /login\" → \"enter email/password\" → \"click Sign in\" sequence. Start\n with the login node and go straight to the first real action. The one exception\n is a flow whose explicit purpose is to test login (start logged out and make the\n login steps the journey).\n\nLOGIN\n Sets the user context. Select a credential to act as that user.\n The login node provisions an authenticated browser session — subsequent\n nodes start with the browser already logged in.\n\n Config:\n credentialId UUID of the credential to log in as. REQUIRED for the\n node to bind to a real credential. Without it the\n Flow Designer shows a red \"Select a credential\"\n warning even if credentialName is set.\n credentialName Human-readable name, used as the node title. Optional\n but recommended alongside credentialId.\n performLogin When true, the browser is authenticated automatically.\n\n Finding the credentialId:\n cofactor credential list [--env prod|dev]\n → prints \"ID: <uuid>\" under each credential name.\n Pick the ID whose property matches the environment the flow runs in.\n\n Ordering: login nodes should typically follow any seed nodes, not\n precede them. Seeds export variables (e.g. $var.workflow_id) that\n later nodes consume, so a seed-first / login-second order avoids\n wasted browser segments and lets login/navigate URLs reference\n seed outputs. See the SEED node type and the Node Ordering section\n of best-practices.\n\n - Workflows support multiple login nodes for multi-role testing.\n Each login node starts a new segment with a fresh browser session.\n Example: Login as Customer → place order → Login as Sales Rep → verify order.\n Great for two-sided apps, permission checks (member vs admin), and\n cross-role journeys.\n - Login nodes also appear after Wait nodes to re-establish the session.\n - A navigate node after login is optional but useful for jumping\n directly to the test's entry point.\n\nNAVIGATE\n Directs the browser to a specific path.\n\n Prefer relative paths (e.g. \"/flows\", \"/flows/$var.workflow_id\")\n over hard-coded absolute URLs. Relative paths let the same workflow\n run unchanged across environments (prod, dev, local, preview) — the\n base URL comes from the property/environment the run targets. A\n workflow that hard-codes \"https://app.dev.runcofactor.com/...\" will\n silently drive traffic against dev even when run against prod.\n\n Use an absolute URL only when the destination is genuinely off the\n application being tested (external identity provider, third-party\n checkout, docs site, etc.).\n\n - A navigate node after login is a great way to jump straight to the\n page where the test begins.\n - Select from discovered pages or enter a custom URL.\n\nACTION\n Performs a user interaction on the current page. Describe what the user\n should do (\"Create a new ticket\") and optionally provide input data via\n formFields.\n\n Every action node must have a \"title\" beside configJson. The title shows\n in the canvas, run timeline, failure reports, and issue tickets — an\n untitled action is nearly impossible to reason about after the fact.\n Keep titles short and intent-oriented: name the goal (and, where it helps,\n the purpose), e.g. \"Create a ticket\", \"Publish the flow\", or \"Click Start to\n open the run-config modal\". A title that names only the mechanics with no\n intent (\"Click the button\") is the thing to avoid.\n\n Keep actions focused on a single user goal — usually broader than one click,\n but a lone action that genuinely stands alone in the flow is a fine node:\n Good: \"Create a ticket with required fields\"\n Good: \"Click Start to open the run-config modal\" (single action — but it\n stands on its own and names its purpose)\n Bad: \"Click the button\" (mechanics only, no intent)\n Bad: \"Create a ticket, assign it, comment, upload, and verify\" (too stuffed)\n\n Aim for ~3–8 playbook steps per node; if a single outcome needs more, split it\n into sequential action nodes. See the Action Nodes section for the full\n altitude guidance (coherent step, assertion boundary, no hidden conditionals).\n\n Rule of thumb: if an action crosses multiple screens or changes multiple\n entities, split it. If it's a single user goal on a single screen, keep it.\n\n Actions can include:\n - title (beside configJson, required): short intent-oriented title, e.g.\n \"Create a ticket\". The detail of what to do goes in actionDescription:\n { \"nodeType\": \"action\", \"title\": \"Create a ticket\",\n \"configJson\": { \"actionDescription\": \"Fill in the required fields and save\" } }\n - formFields: input data the action needs (name, email, etc.)\n - inlineAssertions: checks verified after the action completes (preferred\n over standalone assertion nodes — see Assertions section)\n - declaredOutputs: values to extract from the page for downstream use\n - customInstructions: additional guidance for the agent executing the step\n\nEDITING A NODE\n `cofactor workflow node patch <workflowId> <nodeId> --config '{...}'` merges the\n JSON into the node's configJson one level deep, so a nested object is replaced\n whole: {\"params\":{\"statement\":\"...\"}} changes a sandbox_action's SQL and\n leaves its actionSlug and outputKeys alone, but drops any other params key,\n so list every param to keep. Copy the key path from `workflow get`.\n `--set-input key=value` writes configJson.inputValues, which only action, seed\n and call_flow nodes read, and only for inputs they declare. On any other node\n type the value is never used: use --config.\n\nASSERTION\n Verifies expected outcomes. Define a condition to check.\n What happens on failure depends on the severity.\n\n Prefer inline assertions on action nodes over standalone assertion nodes.\n Inline assertions keep the test on track and produce cleaner failure\n reports. If verifying something requires significant interaction first,\n that's better modeled as an action node with inline assertions.\n\n Use standalone assertion nodes only when the check doesn't belong to\n any action — e.g., verifying a page state after navigation, or\n confirming data absence after a sequence of steps.\n\n Severity levels:\n fatal The test must end now — the run halts at this step.\n Use for preconditions later steps depend on.\n fail The test may continue — the run finishes marked failed,\n but later steps still run and produce signal.\n warn The test should continue — logged as a warning, run passes.\n\n Agentic assertions: for visual/layout checks that can't be mechanically\n validated, enable agent evaluation. These require the AI agent during\n replay and cannot be cached. Use for:\n - Timestamps or dates\n - Real-time data feeds\n - Content that changes between runs\n\nCONDITION\n Branches the workflow based on a runtime condition. Define a condition\n to evaluate, then connect separate paths for true and false outcomes.\n The condition is deterministic when it compares a $var or the acting-as\n identity (evaluated without the agent); otherwise the agent evaluates it\n against the live page.\n\n Reach for a condition node only when the branch is genuine. Conditional\n logic baked INTO an action node — \"if it isn't set to New York, set it\" /\n \"if patient X is missing, create X\" — is usually a sign the data wasn't\n staged, and it caches poorly. Prefer, in order: seed the precondition so\n the branch disappears; run an idempotent sequence unconditionally when the\n same steps reach the goal from any starting state; else a formal condition\n node. If a conditional must live inside an action node, set\n `alwaysUseAgent: true` on that node so the branch is reasoned about live and\n never mis-cached. See the Action Nodes section.\n\nWAIT\n Pauses execution for a specified duration (minutes, hours, or days).\n Creates a segmented flow — execution pauses, then resumes automatically.\n Add a Login node after the Wait to re-establish the browser session.\n\nSETUP (DEPRECATED)\n Referenced another published workflow as a prerequisite. Do not author\n new setup nodes. For shared data use a SEED node (below); for workflows\n that must run in order use suite layers (cofactor test-suite --layer).\n Existing specs that carry a setup node still parse.\n\nSEED\n Injects outputs from a seed workflow as variables. Seed workflows\n generate test data and export named values. Downstream workflows\n access these as $var.{dataKey}.\n\n Ordering: seed nodes should almost always come FIRST — before the\n login node. Seeds run in their own isolated context (usually via\n API sequence) and export variables into the workflow. Placing a\n login before the seed wastes a browser session, and any\n login/navigate node that references $var.{seed_output} must come\n after the seed that produces it.\n\n Typical pattern:\n Seed → Login → Navigate → ... action/assertion nodes\n\n Config:\n seedWorkflowId UUID of the seed workflow (required)\n seedWorkflowName Human-readable name (used as the node title)\n\n Seed workflows provision data that several tests share; data unique to\n one test is created inline. See the Seed Workflows section.\n\nAPI SEQUENCE\n Executes multiple API calls in sequence without a browser.\n Use for backend-only operations, webhooks, or API testing.\n\n API sequences are also used inside seed workflows — start with a\n browser-based seed to discover the right API calls, then convert\n to an API sequence for speed and reliability.\n\nSANDBOX ACTION\n Executes a sandbox action — a server-side script defined on the\n environment template — directly on the run environment's sandbox\n instance. No browser involved. The action's structured output is\n registered as workflow variables: each output key becomes\n $var.{key} for downstream steps.\n\n Config:\n actionSlug Slug of the action (required; discover via\n list_sandbox_actions)\n params Input values keyed by the action's inputSchema\n property names; values may use $var.{name} / macros\n outputKeys Output variable keys (defaults to the action's\n outputSchema keys)\n\n Output registration is allowlisted by declared intent: the action's\n outputSchema keys, else the node's outputKeys. If neither declares\n anything, NO result keys register — declare outputs explicitly for\n schema-less actions.\n\n Environment constraint: the action runs on the environment's\n backing sandbox instance. On a non-sandbox environment (static\n staging/prod) the node SKIPS with a warning and registers no\n variables — so workflows containing one are effectively\n sandbox-only.\n\n Seeds: a seed workflow whose body is a single sandbox action node\n is the fastest way to provision data — it runs entirely\n server-side (no browser session), and its outputs satisfy the\n seed's declared outputs like any captured value.\n\nMCP\n Drives an MCP-server application — an application (property) with type\n \"mcp\" — by letting an agent accomplish a plain-language goal through the\n server's tools. No browser involved. Use it to test an MCP server the org\n has registered as an application: exercising its tool surface, creating or\n mutating data through MCP tools, and capturing returned values as\n workflow variables.\n\n When to use: the step's target is the MCP server itself. To verify the\n EFFECT of the MCP work in the product UI, keep the MCP interaction in the\n mcp node and put the browser verification in SUBSEQUENT action/assertion\n nodes — an mcp node never drives the browser.\n\n Authoring fields (create_node / update_node):\n mcpApplicationName Name of the MCP application (required; discover via\n list_mcp_applications). Resolved to the stored\n application id at authoring time.\n mcpInstructions Plain-language goal (falls back to description).\n State ONE verifiable outcome and name the values to\n capture — \"Create a project named Acme via the\n create_project tool and capture its id as\n project_id\", not \"try out the project tools\".\n mcpPersona Persona (credential template name) supplying\n authentication, resolved against the application.\n Preferred over pinning a raw credential.\n mcpOutputs Variable names the node must produce; each becomes\n $var.{name} for downstream steps.\n mcpTools Optional allowlist restricting which server tools\n the agent may call; omit to allow all.\n\n Like action nodes, mcp nodes replay from cache first with agent fallback\n (set alwaysUseAgent in config to skip replay). Requires the mcp.testing\n feature flag for the organization.\n\nCALL FLOW\n Runs another flow from inside this one, passing arguments in and getting\n that flow's declared outputs back as variables. The child executes as its\n own nested run with its own environment, credentials and browser session,\n so a browserless parent can call a browser child and still watch it.\n\n When to use: a check you want to reuse. Unlike seed nodes —\n which are hoisted out of the chain and run before everything else — a\n call_flow node runs exactly where you put it, which is what makes it\n usable after an action node.\n\n The flow picker in the designer lists only approved-and-promoted (or\n eligible) flows, so a draft is not offered — but that is a picker filter,\n not a runtime rule. Authoring a call_flow node directly against a draft's\n id, as `workflow create` can, is not blocked today; the handler only\n checks that the caller may read the flow.\n\n Arguments map to the child's declared value inputs by name, and an\n argument the child no longer declares fails the step rather than being\n silently dropped. Only outputs the child DECLARES cross the boundary.\n\n Config:\n flowId Flow to run (required). A flow cannot call itself.\n flowName Cached child name, used as the node title.\n snapshotVersion Pinned published version, or null for the live definition.\n inputValues Map of the child's declared input names to values.\n $var.{name} is interpolated here; macros pass through\n unresolved so the child's own macro context expands\n them and per-run uniqueness is preserved.\n environmentId Explicit environment override, or null to let the\n parent's environment apply when the child is eligible\n for it, else the child's own resolution.\n executionMode recommended | api | browser\n onFailure block (default) — a failed child fails this step;\n warn — a child that ran to completion and failed only\n warns. A child this step could not START — bad flowId,\n depth cap hit, an argument the child does not declare —\n fails either way: misconfiguration is never softened.\n\n Declared outputs arrive as both a bare $var.{key} and a namespaced\n $var.{Child_Flow_Name}.{key}, where the child's name is slugified —\n \"Verify checkout totals\" becomes $var.Verify_checkout_totals.{key}. Use\n the namespaced form when two called flows export the same key.\n\n Nesting is capped at 5 levels. Each level is a full run with its own\n browser session, so cost compounds.\n\nNOTIFICATION\n Sends notifications (Slack, email, etc.) at a point in the workflow.\n\nVISUAL CHECKPOINT\n Captures a meaningful screen state (whole page, or an element named by a\n plain-English target) and compares it with the rolling baseline. Fields:\n name, optional target, optional expected state, severity (record or fail).\n See `cofactor workflow guide visual-checkpoints`.\n\nEND\n Explicitly terminates the workflow. Use when a condition branch\n should stop execution rather than continue.","This is the exact syntax of a `.cofactor.md` spec file — what the parser actually matches, not\na loose description. A spec that reads as good prose but drifts from this grammar does NOT\nerror at parse time in most cases: the parser drops the line/bullet it can't match and reports\nonly a warn-severity issue. A spec can validate as \"ok\" while silently missing content the\nauthor wrote. When in doubt, match the grammar below character-for-character rather than\nwriting what merely reads naturally.\n\nFRONTMATTER\n\n A `---`-fenced YAML block, first thing in the file:\n\n ---\n name: Org onboarding — invite a member\n workflowId: <uuid> # present only once compiled; NEVER remove or edit it by hand\n status: draft # draft | approved\n flowType: standard # standard | seed | teardown (setup is deprecated)\n actor: admin # persona name (or \"persona <role>\"), \"credential <name>\"\n # for one fixed account, or \"seed-output <dataKey>\" for a\n # seed's credential output — see ACTOR FORMS below\n authenticated: true\n tags: [smoke, p0]\n requirementRef: LIN-4421\n ---\n\n `workflowId` binds the file to a live workflow: present → `spec_compile` diffs/updates that\n workflow; absent → `spec_compile` creates a new one. If it's present, treat it as immutable —\n removing it turns your next compile into a duplicate-creating CREATE instead of an UPDATE.\n\nSECTION HEADINGS (H2, in this order)\n\n ## Spirit — one blockquote paragraph: the user-visible promise this flow proves.\n Becomes the workflow's description, so it carries no capability codes,\n preconditions, issue ids, status, step narration or raw IDs.\n ## Preflight — plain prose or bullets: non-seedable preconditions the environment must\n already satisfy, including pre-existing data the test reads but does not\n own, and external systems (flags, payments, third parties) it relies on.\n Becomes the workflow's preflight field. Name things, never IDs.\n ## Cleanup — plain prose or bullets: how the run's side effects get reset. Becomes\n the workflow's cleanup field.\n ## Inputs — bullets: `- <name>: <defaultValue>` or `- credential <name>: <value>`.\n ## Declared Outputs — seed flows only: `- <dataKey>` bullets with optional indented\n `- label:` / `- property:` / `- description:` sub-bullets. A\n credentialOutput block is NOT authorable here — it is preserved from\n the live seed (rendered as an HTML comment on pull) and set via the\n Flow Designer or `workflow seed create --from-file` / `workflow patch\n --seed-outputs-file` (see `workflow guide seed-users`).\n ## Scenarios — H3 sub-blocks per scenario: `### <Name> *(default)*` or `*(skip: <reason>)*`,\n with `- values: {...}` / `- credentials: {...}` YAML underneath.\n ## Journey — the linear sequence of nodes described below. Required — a spec with no\n Journey nodes fails to parse.\n\nJOURNEY LEAD-INS\n\n Every Journey line is either a bold lead-in (`**...**`) or a blockquote/bullet attached to\n the preceding lead-in. Blank lines between a lead-in, its description, and its bullets are\n fine — the parser skips them.\n\n **Navigate to** /path/here\n **Action — Fill out the signup form**\n **Assert** the confirmation toast is visible\n **Wait** 30s (also accepts 5m, 2h)\n **End** (add \"fail\" anywhere in the line for a failing End)\n **Seed** seed-workflow-name\n **Setup** setup-workflow-name (deprecated — still parses; author **Seed** instead)\n **Switch role to** admin (or **Switch role to admin** — both forms work)\n **Sandbox action — provision-org**\n **Api sequence — Create via API**\n **Visual checkpoint — Invoice list after seeding**\n **If** $var.status is \"blocked\"\n **Then:**\n <nested nodes, indented 2 spaces>\n **Else:**\n <nested nodes, indented 2 spaces>\n\n CRITICAL: for `Action`, `Sandbox action`, and `Api sequence`, the title goes AFTER an em-dash\n (`—`) or hyphen INSIDE the bold span: `**Action — Title**`. Writing `**Action** Title` (title\n outside the bold) used to silently lose the title; it is now tolerated as a fallback, but the\n em-dash-inside-bold form is canonical — use it. Do not invent a form for a lead-in not listed\n above (e.g. there is no `**Click**` or `**Type**` lead-in) — express it as an Action instead.\n\nACTOR FORMS (frontmatter `actor:` and `**Switch role to** <ref>` take the same forms)\n\n actor: Admin persona named \"Admin\" (default form)\n actor: persona Admin same, explicit\n actor: credential admin-bob one specific stored credential by name\n actor: seed-output new_user the credential output `new_user` of a **Seed** node\n earlier in the Journey (credentialSource: from_seed)\n\n `template <name>` is a legacy alias for `persona <name>`. `seed-output` binds to the\n nearest **Seed** node that precedes the login, so the Seed must come first in the\n Journey (for `actor:`, as the leading node); compile fails if no Seed precedes it.\n\nDESCRIPTION (optional, any lead-in)\n\n A blockquote line immediately after the lead-in (or after a blank line) becomes the node's\n description — free prose is fine and expected here:\n\n **Action — Create the org**\n > Fill the org form with orgName and plan, then submit.\n - field name: $orgName\n\nACTION BULLET GRAMMAR (exact — only these forms parse; everything else is DROPPED)\n\n - field <name> [(type)]: <value> e.g. - field plan (select): $plan\n - capture: <name> e.g. - capture: createdOrgId\n - assert (warn|fail|fatal[, agentic]): <condition>\n e.g. - assert (fail): URL ends with `/dashboard`\n e.g. - assert (warn, agentic): chart looks right\n - alwaysUseAgent: true\n - captureAudio: true\n - captureLabel: <label>\n - captureExtraMs: <number>\n\n A standalone `**Assert**` node takes its own kv option bullet the same way:\n\n - severity: warn|fail|fatal (assert nodes only; NOT `- severity: warn — text`,\n which is an unparseable kv bullet, not an assert)\n\n `Seed`/`Setup` bullets: `- version: latest`, `- mode: ...`, `- onFailure: block|warn`,\n `- input.<name>: <value>`. `Sandbox action` bullets: `- label: ...`, `- outputs: a, b`,\n `- param.<name>: <value>`. `Visual checkpoint` bullets: `- target: <plain-English element>`,\n `- expected: <visible expected state>`, and `- severity: record|fail`.\n\nCOMPLETE MINIMAL EXAMPLE\n\n ---\n name: Login smoke\n actor: admin\n authenticated: true\n requirementRef: LIN-101\n ---\n\n ## Spirit\n > A returning admin can sign in and reach the dashboard.\n\n ## Inputs\n - adminEmail: $EMAIL()\n\n ## Journey\n\n **Navigate to** `/login`.\n\n **Action — Sign in**\n > Enter email and password, submit.\n - field email: $adminEmail\n - field password: $CREDENTIAL\n - assert (fail): URL ends with `/dashboard`\n\n **End**.\n\nCOMMON MISTAKES (each of these compiles into an empty or half-empty node — no parse error)\n\n - A free-text bullet (\"- Should reach the dashboard eventually\") is not `field`/`capture`/\n `assert (...)`/a known kv key — it is DROPPED. Put prose in the `>` description instead of\n inventing a bullet shape.\n - `- severity: warn — should show a toast` is a kv bullet with key `severity`; it is not a\n recognized action option, so it is DROPPED. Use `- assert (warn): should show a toast`.\n - `**Action** Do the thing` (title outside the bold) is tolerated but non-canonical — prefer\n `**Action — Do the thing**`.\n - Any Journey line that doesn't start with a recognized bold lead-in is DROPPED with only a\n warn-severity \"Unexpected line in Journey\" issue — it will not error your compile.\n - After authoring or editing a spec, always run `spec_validate` and inspect its\n `droppedContent` field. Non-empty `droppedContent` means real content is about to be lost —\n treat it as a blocking error, not a warning, even though its underlying severity is `warn`.\n","WHAT A STRONG ACTION NODE LOOKS LIKE\n\n Action nodes are the workhorse of a flow — and they are also the unit the\n cache stores and replays. Sizing them well is what makes a flow both fast\n (cacheable sooner) and durable (resistant to benign UI churn). Five things\n separate a strong action node from a weak one.\n\n 1. IT READS AS ONE COHERENT STEP\n\n A good node has a title you could read aloud as a single instruction to a\n teammate: \"Create a ticket\", \"Fill out the subscription form\", \"Click Start\n to open the run-config modal\". The title names a goal — and, where it helps,\n the purpose of the step — not raw mechanics.\n\n A node is usually broader than a single click, but not always: a lone action\n that genuinely stands on its own in the flow is a fine node (\"Click Start to\n open the modal\"). The extremes to avoid are a purely mechanical title with no\n intent (\"Click the button\") and a node stuffed with several unrelated\n outcomes (\"Create a ticket, assign it, comment, and upload\"). When in doubt,\n err toward clearer, more discrete steps over one overloaded node.\n\n 2. IT HAS ROUGHLY 3–8 PLAYBOOK STEPS\n\n Each playbook step is one atomic intent executed in one tool call, bounded\n by stable page states. Fuse actions coupled through ephemeral UI state — an\n opener and a target that exists only while its menu is open — into that\n call. Split unrelated intents and every stable intermediate. A healthy node\n lands around 3–8 steps:\n\n click → fill_form → click (good — open, fill, submit)\n click → click → fill_form → click (good)\n 27 clicks (bad — that's several outcomes)\n\n \"Meaningful\" is the operative word. Incidental events collapse into the\n nearest step (a focus-click before a fill is part of the fill), and a whole\n form-fill is ONE fill_form step, not one step per field. If a single outcome\n would need more than ~8 steps, that's the signal to split it into sequential\n action nodes (\"Fill customer details\" → \"Fill billing details\" → \"Review &\n submit\") rather than grow one megastep.\n\n This ~3–8 is the STEP count inside one action node — not a cap on how many\n NODES a flow has. A flow is as long as its coherent journey needs (see Best\n Practices on flow scope).\n\n Interaction recipes are property-scoped, reusable interaction mechanics.\n A recipe invocation appears as one `call` step for its whole transaction,\n not as separate steps for the browser actions inside it.\n\n 3. IT HAS A CLEAR ASSERTION BOUNDARY\n\n A strong node ends at a checkpoint you can assert on: a modal disappeared,\n the URL changed, a row appeared, a toast fired. Put that check inline on the\n node (see the Assertions section) so the node's success is observable and the\n run gets a real progress marker at each step. State the behavior promised\n by the requirement, not DOM attributes, SVG details, or an arbitrary\n measurement threshold discovered during exploration. Keep observation\n mechanics in executable code and chosen interaction inputs in the action.\n\n Inline assertions verify the END STATE after the action. They can be relative\n or temporal (\"the row count dropped by 2\"), but only if the baseline was\n captured upstream — write `current row count = $var.captured_row_count - 2`,\n never \"the count dropped by 2\". The reason: an action may replay from cache\n while its assertion is judged live by the agent, which does NOT have the\n action's execution context. A self-contained assertion (absolute values, or\n values bound to captured variables) survives that split; one that silently\n assumes \"before\" state does not.\n\n 4. IT IS NOT CONDITIONAL\n\n Avoid baking branching into an action node — \"if the location isn't New York,\n set it to New York\", \"if patient X isn't in the table, create patient X\". A\n conditional in a node usually means the data wasn't staged properly, and it\n caches poorly (the cached path and the live path diverge).\n\n Prefer, in order:\n - Seed the precondition so the branch never exists (\"patient X always\n present\" → seed patient X). See Seed Workflows.\n - If the action is idempotent — the same sequence reaches the desired state\n regardless of where it started (re-selecting \"New York\" works whether or\n not it was already set) — just run the sequence unconditionally. No branch\n needed.\n - If the branch is genuine, use a formal CONDITION node (deterministic when\n the check is a $var / identity comparison; agentic otherwise), which keeps\n the branch explicit and cacheable per path.\n\n If a conditional truly must live inside a node, set `alwaysUseAgent: true` on\n it so the branch is reasoned about live every run and never mis-cached.\n\n 5. ITS PROSE IS SHORT\n\n The description is what a tester would say out loud — two to four plain\n sentences: what to click, what should happen, what state to leave behind.\n Playbook targets name controls by their visible text (\"'Create copy' menu\n item\", \"QA Policy row checkbox\"). Exact strings belong in the\n assertions only when they are required by the spec, not merely locator\n details. The readable condition must remain about the product behavior.\n\n Good: \"Search for QA Carrier, tick the QA Policy A and\n QA Policy B row checkboxes, and click the 'Create policies'\n toolbar icon. Review the preview, then click 'Cancel'. Leave both\n rows selected.\"\n Bad: a paragraph that restates hover/retry mechanics, quotes request\n URLs and response bodies, or narrates a bug with its ticket id\n and repro steps.\n\n Do not restate the same mechanics in every node (\"hover the row so its\n controls appear — they render only while hovered — then click …\"); say it\n once in a short phrase or leave it to the agent. Known failures go in a\n warn-severity assertion that cites the ticket id in one clause, with the\n ticket attached to the flow (status event / comment), never as an essay\n inside the node. If a description needs more than four sentences, the node\n is doing more than one thing — split it (point 1).\n\n WHY THIS MATTERS\n\n The node is the atomic cache unit. The smaller and more reliable each node is,\n the sooner the flow caches and the faster it replays. But because the node is\n also the unit of specificity, the more a node pins to incidental detail (exact\n button text, a precise field order), the more a benign change breaks it.\n Nailing the altitude — coherent goal, ~3–8 meaningful steps, a clean assertion\n boundary, no hidden branches, short prose — is what buys both speed and\n reliability at once.\n","A playbook is a sequence of human-readable steps that describe the actions\ntaken during a workflow node's execution. Playbooks bridge the gap between\nhigh-level node descriptions (\"Create a ticket\") and the low-level browser\ninteractions that fulfill them.\n\nWHAT A PLAYBOOK CONTAINS\n\n Each playbook is an ordered list of PlaybookSteps:\n\n order Step sequence number\n action What to do: click, fill, fill_form, select, navigate,\n wait, scroll, hover, upload, verify, handle_native_dialog,\n toggle, press_key, focus, drag, resize, capture,\n extract_table, python_execute, verify_download,\n verify_download_content, screenshot, agent_loop, call\n target The element or area being acted on\n params Key-value parameters for the action\n note Human-readable description of the step\n\n Each step is one atomic intent executed in one tool call, bounded by stable\n page states. Fuse actions coupled through ephemeral UI state — opening a\n transient menu and choosing its item, for example — into that call. Split\n unrelated intents and every stable intermediate into separate steps.\n\n Example playbook for \"Create a ticket\":\n 1. click \"New Ticket\" button\n 2. fill_form Title: \"QA Test — Billing Bug\", Priority: \"High\"\n 3. click \"Submit\" button\n 4. verify Success toast appears\n\nHOW PLAYBOOKS ENABLE CACHE REPLAY\n\n When a workflow runs for the first time, the AI agent explores the\n application, performs actions, and generates a playbook for each node.\n The raw browser actions (clicks, keystrokes, waits) are cached alongside\n the playbook steps.\n\n On subsequent runs, the system uses playbook-step-driven replay:\n\n 1. Load the playbook steps from the node config\n 2. For each step, look up the cached browser actions\n 3. Replay the cached actions directly (no AI agent needed)\n 4. If a step's cache misses or fails, the AI agent heals that step\n\n This means most runs execute instantly via cache, with the agent only\n invoked for steps that have changed or broken.\n\nCOMPILED ENTRIES\n\n The cache maps playbook steps to cached action indices:\n\n cached Playbook step has matching cached actions — replayed directly\n gap No cache for this step — agent performs it fresh\n\n A fully cached node replays in seconds. A node with gaps falls back to\n agent execution only for the missing steps.\n\nVERIFY STEPS & ASSERTIONS\n\n A `verify` (or `verify_download`/`verify_download_content`) step names the\n inline assertion it proves via `assertionId` — the assertion's UUID from\n the node's inlineAssertions, or `'completion'` for the step that closes\n out the node's job-to-be-done. The step sits at the position in the flow\n where the check actually happened, not appended at the end.\n\n A verify step whose assertionId matches a recorded assertion compiles into\n the cache at that position, alongside the other steps. An assertion the\n agent didn't place in the flow gets a synthesized trailing verify step\n instead.\n\n Agentic assertions (agent-evaluated, not mechanically checkable) never get\n a verify step — they can't compile and are judged live during replay.\n\n See assertions.txt for how inline assertions are authored, severities, and\n when to use agentic evaluation.\n\nINTERACTION RECIPES\n\n Interaction recipes are property-scoped, reusable interaction mechanics.\n Calling one executes its transaction atomically and appears in the playbook\n as one `call` step, with a human-readable target and the recipe arguments.\n\nPLAYBOOK HEALTH\n\n Each cached node tracks playbook health:\n\n verified Cache is working correctly across recent runs\n degraded Some steps have failures — cache still used but monitored\n stale Cache is no longer reliable — full agent re-execution needed\n\n Step-level failure counts track which specific steps break most often,\n guiding targeted cache healing.\n\nWHY PLAYBOOKS MATTER FOR AUTHORING\n\n When you build a workflow, the quality of your node descriptions directly\n affects playbook quality. Clear, intent-focused descriptions produce\n better playbooks, which produce more reliable cache replay.\n\n Good: \"Create a ticket with title 'QA Test' and priority 'High'\"\n → Clean playbook: fill_form with clear fields, click submit\n\n Bad: \"Do the thing on the page\"\n → Vague playbook: agent guesses at actions, cache breaks easily\n\n The playbook is the semantic contract between your intent and the\n execution engine. Write nodes that describe what a human would do,\n and the playbook will faithfully capture and replay it.\n","EXPLORE BEFORE AUTHORING\n\n Read the intended journey and relevant product-map capability. Use a\n cofactor session with the intended credential and application environment\n to explore the real UI before choosing targets. Confirm the signed-in user\n and role during this preflight, not as an extra runtime requirement unless\n authentication or identity is what the test covers.\n\n Discover named data candidates yourself, then confirm with the user which\n existing fixtures are reliable for repeated tests. Ask about genuinely\n ambiguous behavior or data stability, not labels or URLs you can discover.\n If shared data is unsuitable, agree on test-owned setup.\n\nSCOPE: ONE COHERENT USER JOURNEY\n\n A workflow should test one job-to-be-done, not multiple unrelated tasks.\n Many steps are fine if they're all part of the same flow (an action and\n everything it is meant to cause). Stop when the goal is accomplished.\n\n Good — multi-role journey:\n Login as Customer → Place an order →\n Login as Sales Rep → Verify the order appears in the queue\n\n Good — single-entity journey:\n Login → Navigate to projects → Create project → Edit name →\n Assert name updated → Cleanup: delete project\n\n Good — read-only flow:\n Login → Navigate to dashboard → Assert widgets visible\n\n Bad — multiple unrelated journeys:\n Dashboard → Create item → Manage users → Generate report\n (These are separate jobs — create separate workflows)\n\n Rule: \"Is this step dependent on or naturally connected to the previous\n steps?\" If no, you're starting a new journey — stop.\n\n A coherent flow can be long or short — there is no target node count. If a\n flow grows very long, ask whether it is really several INDEPENDENT journeys:\n splitting them into separate tests lets them run in parallel and fail\n independently. Keep a single coherent journey together — don't split mid-flow.\n\nTEST THE SEAMS: FOLLOW THE ACTION TO ITS EFFECT\n\n CRUD rarely breaks on its own. The seams between features break: the place\n where one action is supposed to cause something somewhere else. Creating a\n ticket is not the point. The point is what the ticket does: notify the\n assignee, land in Support's queue, bump a count on the dashboard, sync to\n an integration.\n\n Before writing a flow, ask: \"When this action succeeds, what is supposed to\n happen next, and where would a user see it?\" When there's an answer, the\n stronger journey follows the action to that effect:\n\n Weaker — stops at the write:\n Login → Create ticket assigned to Support → Assert ticket in my list\n\n Stronger — crosses the seam:\n Login as Customer → Create ticket assigned to Support →\n Login as Support → Confirm the assignment email in Support's inbox →\n Verify the ticket is in the Support queue\n\n Common seams to look for:\n - Another screen or view (a list, dashboard, report, activity feed)\n - Another role or user (a queue, an approval, a shared record)\n - A message (email, in-app notification; for email, see EXTERNAL\n DEPENDENCIES in cofactor workflow guide data-lifecycle)\n - A downstream state change (status, count, permission)\n\n Create → edit → delete is mostly data management: the test makes what it\n needs and cleans it up (see MAKE EACH TEST SELF-SUFFICIENT). It is a fine\n flow for a page whose writes have no effect elsewhere. But when the action\n is meant to trigger something, the effect is what's worth proving.\n\n This is a preference, not a requirement. Some seams can't be observed from\n the test (a third-party system with no readable surface, a side effect with\n no UI). Assert what you can see and move on. When authoring from a\n recording, apply this only within what was captured — don't add steps\n the recording never performed.\n\nWRITE STEPS AS HUMAN INTENT\n\n A workflow should read like instructions you'd explain to a teammate:\n - \"Create a ticket assigned to Support.\"\n - \"Submit the ticket and confirm it appears in the list.\"\n - \"Change the ticket status to Closed.\"\n\n Capture the \"why\" and the \"what\" — not UI mechanics.\n\nCHOOSE THE RIGHT GRANULARITY\n\n Too granular:\n - \"Click New\"\n - \"Type title\"\n - \"Type description\"\n - \"Click Save\"\n\n Too stuffed:\n - \"Create a ticket, assign it, comment, upload, and verify in three views\"\n\n Good:\n - \"Create a ticket with required fields\"\n - \"Assign the ticket to Support\"\n - \"Verify the ticket appears in the queue\"\n\n Sizing rule: a node is a single coherent goal of roughly 3–8 playbook steps\n (one meaningful action each — a whole form-fill is ONE fill_form step). The\n \"too granular\" list above is about splitting one goal into a node per keystroke\n or writing mechanics-only titles — NOT a ban on single-action nodes: a lone\n action that stands on its own (\"Click Start to open the modal\") is fine. This\n ~3–8 is per action node (its playbook STEPS); it is NOT a cap on how many NODES\n a flow has — flow length follows the journey (see SCOPE above). See the Action\n Nodes section.\n\n If an action crosses multiple screens or changes multiple entities, split it.\n If it's a single user goal on a single screen, keep it as one action.\n\nMAKE EACH TEST SELF-SUFFICIENT\n\n A strong test is idempotent, controls its own data, and is isolated. The\n full rules and their sanctioned workarounds are the \"STRONG TESTS\" block of\n the Test Data Lifecycle guide (cofactor workflow guide data-lifecycle).\n In short:\n\n - Create the data the test operates on, inline, with macros ($EMAIL(),\n $UUID(), etc.) so reruns don't collide\n - Use a seed workflow only for data that several tests share\n - Pre-existing data the test only READS is fine — declare it in\n ## Preflight (the workflow's preflight field, not its description)\n and never mutate it\n - Tests that must run in order go in suite layers, never rely on\n implicit ordering\n\n This keeps runs deterministic and prevents cascading failures.\n\nVERIFY THE SPECIFIED BEHAVIOR\n\n Verify each meaningful product claim, without adding implementation-only\n checks. Assertions express the required outcome; replay code supplies DOM\n evidence and measurements. Chosen scroll distances and viewport sizes are\n action/setup inputs, not acceptance thresholds unless the spec says so.\n Do not assert ARIA values, SVG paths, or a signed-in user merely because\n exploration exposed them.\n\n Before recording a step, verify its expected outcome:\n\n - Form submissions: confirm acceptance (success message, redirect, new item in list)\n - Data creation: navigate to where the record should appear and confirm\n - Data modification: reload and confirm the change persisted\n - Deletions: confirm the item is gone (a toast alone is not enough)\n - Side effects: where an action is meant to cause something elsewhere,\n check it there when you can (see TEST THE SEAMS)\n - Navigation: confirm the expected page loaded\n\n If verification fails, debug the issue — don't record and move on.\n\nKEEP DESCRIPTIONS CLEAR\n\n An action node's \"title\" is short and says what the user does; its\n configJson.actionDescription adds the detail the title can't carry:\n { \"nodeType\": \"action\", \"title\": \"Create a support ticket\",\n \"configJson\": { \"actionDescription\": \"Fill the form with a QA Test subject and High priority, then submit\" } }\n Bad title: \"Step 3 - ticket creation process\"\n Bad title: \"Fill the form with a QA Test subject and High priority, then submit\"\n (that is the description; a copied instruction is not a title)\n A missing title is rejected, never derived from the description.\n\n The workflow description gives the spirit of the whole test in one or two\n plain sentences: what it does and why it matters. Capability codes,\n preconditions, issue ids, status and step narration do not belong there:\n preconditions go in ## Preflight, and the rest lives with the product-map\n link, the linked issue, the run history and the nodes.\n\n People read description, preflight and cleanup, so write preflight and\n cleanup as short markdown bullet lists and never put a raw ID in any of\n them (no UUID or UUID prefix, no wf_/prule_ or other prefixed id, no issue\n or run id). Name the seed workflow, teardown, connection or record instead.\n\nNODE ORDERING\n\n The start-of-workflow order is a strict hierarchy (see \"STARTING NODE\"\n in the Node Types guide):\n\n Seed (if needed) → Login → Navigate → ...\n\n The first node is the ACTOR, not a navigate: an authenticated flow opens\n with a `login` node (or a `seed`, if it needs data first) — never with a\n navigate node. A navigate jumps to the test's entry point AFTER login.\n Only unauthenticated flows (signup, password reset, public pages, or a\n login test) may start with a navigate.\n\n Seed nodes should come first. Seeds run in their own isolated context\n (usually via API sequence) and export variables the rest of the flow\n consumes. Putting a login before the seed wastes a browser segment,\n and any login/navigate that references $var.{seed_output} must come\n after the seed that produced it.\n\n Only skip the seed-first rule if the seed itself genuinely depends on\n a browser session created earlier in the same workflow (rare — in\n that case, prefer a separate browser-based seed workflow and reference\n it here).\n\nLOGIN BEST PRACTICES\n\n - Most workflows start with a login node. Workflows can also start\n with a seed node or API sequence when no browser session is needed yet.\n - Login SUBSUMES logging in. A login node already provisions an\n authenticated session — it IS the act of logging in. Never follow a\n login node with \"navigate to /login\" / \"enter email + password\" /\n \"click Sign in\" steps. Start with the login node and go straight to\n the first real action. (Only exception: a flow whose purpose is to\n test login — start logged out and make the login steps the journey.)\n - Login nodes require a credentialId (UUID), not just a credentialName.\n Without a credentialId the Flow Designer flags the node as invalid.\n Find credential IDs with: cofactor credential list [--env prod|dev]\n - Use multiple login nodes for multi-role testing — each login starts\n a new segment with a fresh session as that user\n - Don't test login itself (unless that's the explicit goal)\n - A navigate node after login is a great way to jump directly to the\n test's entry point\n\nPREFER INLINE ASSERTIONS\n\n Attach assertions to action nodes using inlineAssertions rather than\n adding standalone assertion nodes. Inline assertions keep the test\n focused and produce clearer failure reports. See the Assertions section\n for details.","Macros generate fresh, unique data every time a workflow runs. Variables let\nyou reference shared values across nodes. Both use the $ prefix.\n\nAVAILABLE MACROS\n\n $EMAIL() Unique test email test-a1b2@example-test.com\n $FIRST_NAME() Random first name Jordan\n $LAST_NAME() Random last name Chen\n $PHONE() Random US phone +1-555-382-7491\n $UUID() Random UUID v4 a1b2c3d4-e5f6-...\n $NOW() ISO 8601 timestamp 2026-02-19T14:30:00.000Z\n $NOW(+5m) Shifted ISO timestamp $NOW(-2h), $NOW(30s), $NOW(1d)\n $NOW(+5m, \"fmt\") Shifted time, UTC fmt $NOW(+5m, \"m H * * *\") = 35 14 * * *\n $TODAY() Current date 2026-02-19\n $DATE(N) Date offset by N days $DATE(7) = next week\n $TIMESTAMP() Unix timestamp (ms) 1771520400000\n $RANDOM_INT(min,max) Random integer $RANDOM_INT(1,100) = 42\n $RANDOM_STRING(len) Random alphanumeric $RANDOM_STRING(6) = xK9mQ4\n $SEQUENCE(name) Auto-incrementing $SEQUENCE(order) = 1, 2, 3...\n\n$NOW FORMATS\n\n The optional second argument renders the time in UTC. Tokens: YYYY MM DD\n HH mm ss (zero-padded) and H m (unpadded hour and minute); everything else\n is literal, except that a bare H or m is always a token (there is no escape). Use $NOW(, \"fmt\") to format the current time with no shift.\n A format cannot contain commas or parentheses. Quote it with single or\n double quotes (or not at all); inside a JSON string such as an API\n sequence bodyTemplate use single quotes.\n\n $NOW(, \"YYYY-MM-DD HH:mm\") 2026-02-19 14:30\n $NOW(+5m, \"m H * * *\") daily cron that first fires in ~5 minutes\n\nSYNTAX\n\n Macros follow the format $MACRO_NAME() or $MACRO_NAME(args).\n They can appear standalone or inline within a larger string:\n\n Standalone: $EMAIL()\n Inline: user-$UUID()@corp.com\n Escaped: \\$EMAIL() produces the literal text $EMAIL()\n\n Each run resolves macros independently — every execution gets fresh values.\n\nVARIABLES\n\n Variables reference values set during a workflow run. When an action has\n input fields or extracts data, those become $var.{name} references.\n\n Sources:\n Input A value entered in an action's field $var.company\n Macro A macro expression in a field $var.email\n Extracted Data extracted from the page $var.order_id\n Credential Values from the login credential $var.username\n\n Use $var.{name} in subsequent step formFields and descriptions.\n Don't re-hardcode values — reference the variable from the earlier step.\n\nHOW VARIABLES GET BOUND\n\n Two distinct paths create $var.{name} bindings. Picking the wrong one is a\n common foot-gun.\n\n 1. Typed input (auto-promote)\n\n Add an entry to inputValues on an action node:\n\n inputValues: { company: \"$RANDOM_STRING(8)\" }\n\n The key \"company\" auto-promotes to $var.company for every downstream\n node. Do NOT also list it in declaredOutputs — the input itself IS the\n binding.\n\n 2. DOM capture (declaredOutputs + playbook capture)\n\n For values that only exist after the page renders (URL fragment, badge\n text, generated ID in the breadcrumb), declare them upfront and capture\n them inside the playbook:\n\n declaredOutputs: [\"order_id\"]\n playbookSteps: [\n { order: N, action: \"capture\", target: \"<selector or rule>\",\n outputKey: \"order_id\" }\n ]\n\n The capture step writes into $var.order_id at runtime. Without\n declaredOutputs the downstream resolver won't recognize the binding.\n\n Rule of thumb: if you can type the value into a field before the run, it's\n an input. If the value only exists after something happens in the browser,\n it's a captured output.\n\nSEED VARIABLES\n\n When seed workflows are configured, their outputs are available as\n $var.{dataKey} references. If a seed provides data you need (a project ID,\n a user record), use the seed variable — don't create that data again.\n\nCOMMON PATTERNS\n\n Unique user registration:\n Email: $EMAIL()\n Name: $FIRST_NAME() $LAST_NAME()\n\n Numbered items:\n $SEQUENCE(\"orders\") → 1, 2, 3... (resets each run, independent per name)\n\n Date-relative testing:\n $DATE(30) for 30 days out, $DATE(-7) for last week, $TODAY() for now\n\n Unique identifiers:\n $UUID() for globally unique, $RANDOM_STRING(8) for shorter strings\n Combine with text: order-$RANDOM_STRING(6)\n\nBEST PRACTICES\n\n - Use $EMAIL() instead of hardcoded addresses for uniqueness\n - Use $SEQUENCE(\"name\") when you need predictable ordering within a run\n - Prefer $DATE(days) over fixed dates so tests stay valid over time\n - Always include a value for every formField — don't leave fields empty","Seed workflows provision test data that several tests share. A seed\nworkflow creates data and exports named variables that downstream\nworkflows can reference. Data unique to one test is created inline in\nthat test, not seeded (see the Test Data Lifecycle section).\n\nWHY USE SEEDS\n\n - Reusable — the qualifying condition: one seed serves many test workflows\n - Fast: seeds can use API sequences instead of slow browser interactions\n - Clean: paired teardown workflows handle cleanup automatically, on\n pass and fail\n - Isolated: each run gets its own fresh data via macros\n\nHOW SEEDS WORK\n\n 1. Create a workflow with flowType \"seed\"\n 2. Define seedOutputs — the named values the seed exports\n (e.g., project_id, user_email, order_number)\n 3. The seed creates data and captures those values during execution\n 4. Downstream workflows add a Seed node that references this seed\n 5. The seed's outputs are available as $var.{dataKey} in the test\n\n Example:\n Seed \"Create Test Project\" exports: project_id, project_name\n Test workflow references the seed, then uses:\n Navigate to $var.project_id\n Assert page title contains $var.project_name\n\nCREATING SEEDS\n\n A seed workflow shell needs a name, declared outputs, and usually a paired\n teardown. Outputs are the contract downstream workflows depend on; define\n them before building the seed body so every node captures the right values.\n\n Simple data outputs need:\n - dataKey: stable variable name, used as $var.{dataKey}\n - name: human label\n - description: optional context for builders and reviewers\n\n Credential outputs additionally define credentialOutput — the field\n mapping (\"$generate.email\" for a minted address), inbox behavior, and\n loginUrl. The full recipe for a seeded user, from declaration to the\n consumer's from_seed login and teardown, is the Seeding a Test User\n section (`cofactor workflow guide seed-users`).\n\n After creating the shell, add nodes through the Flow Designer, workflow sync,\n or API, then publish once the seed and paired teardown both pass.\n\n CLI users: see `cofactor workflow guide cli-seeds` for command usage.\n\nBUILDING A SEED: START WITH THE BROWSER\n\n The easiest way to build a seed is to start with browser-based steps:\n\n 1. Create a seed workflow (flowType: seed)\n 2. Add a login node with credentials that have permission to create data\n 3. Add action nodes that create the data through the UI\n - Use macros for all names and identifiers ($EMAIL(), $RANDOM_STRING(6), etc.)\n 4. Add declaredOutputs on action nodes to capture generated values\n (IDs, URLs, confirmation numbers)\n 5. Define seedOutputs on the workflow to export those captured values\n 6. Test and publish the seed\n\n This approach lets you discover exactly what steps and data are needed\n by interacting with the real application.\n\nCONVERTING TO API SEQUENCES\n\n Once a browser-based seed works reliably, convert it to an API sequence\n for speed and reliability. Cofactor can do this automatically.\n\n Auto-conversion from a workflow run:\n 1. Open the seed workflow in the Flow Designer\n 2. Add an api_sequence node\n 3. In the node config, choose \"Generate from workflow run\"\n 4. Select a completed run of the browser-based seed\n 5. The AI agent analyzes the network requests captured during\n that run and generates an equivalent API sequence automatically\n 6. The agent tests each step against the live API to verify it works\n 7. Review the generated sequence and publish\n\n The AI agent has access to your application's API catalog and can:\n - Search for endpoints by keyword\n - Inspect request/response schemas\n - Test individual API calls to verify they work\n - Compose a multi-step sequence with variable passing between steps\n - Validate the full sequence end-to-end before finalizing\n\n You can also generate an API sequence from a prompt — describe what\n data to create and the agent will find the right endpoints and build\n the sequence.\n\n API sequences support two kinds of dynamic values:\n - Step references: {{stepId.response.path.to.value}} to pass data\n between steps (e.g., use the project ID from step 1 in step 2)\n - Macros: $EMAIL(), $UUID(), $RANDOM_STRING(6), etc. for fresh\n unique data each run\n\n Exports map response fields to seed output variables:\n - stepId: which step to extract from\n - responsePath: dot notation path (e.g., \"data.id\")\n - variableName: the $var.{name} downstream workflows will use\n\nEXECUTION MODES\n\n Seeds automatically choose the best execution mode:\n\n browser Runs through the browser like a normal workflow\n api Runs the API sequence directly — no browser needed\n recommended Uses the API sequence if one exists, otherwise browser\n\n Once you've added an API sequence to a seed, it will automatically\n run via API — faster, cheaper, and more reliable than browser execution.\n\n Not all seeds can be converted — some data creation flows require\n browser interaction (file uploads, complex wizards, third-party\n integrations). Keep those as browser-based seeds.\n\nAPI CALL TIMEOUT\n\n When a seed runs in API mode (executionMode \"api\", or \"recommended\"\n resolving to API), each HTTP call uses a 2-minute (120s) timeout by\n default. Seeds/teardowns that hit slow provisioning endpoints can raise\n this per seed node via apiTimeoutSeconds — it applies to both the seed\n and its paired teardown. Ignored for browser-mode seeds.\n\n Set it three equivalent ways:\n - Flow Designer: the \"API call timeout (seconds)\" field on the Seed node\n - .cofactor.md: a \"- apiTimeoutSeconds: 300\" bullet under the Seed node\n - workflow JSON (create / sync): on the seed node's configJson, e.g.\n { \"nodeType\": \"seed\",\n \"configJson\": { \"seedExecutionMode\": \"api\", \"apiTimeoutSeconds\": 300 } }\n\n Leave it unset to keep the 120s default; an invalid value falls back to\n the default rather than erroring.\n\nTEARDOWN WORKFLOWS\n\n Every seed can have a paired teardown workflow that cleans up the\n data it created. Teardowns run automatically after the seed's\n downstream workflows complete — whether they passed or failed.\n\n - Set flowType to \"teardown\" and link it to the seed\n - The teardown receives the same $var.{dataKey} variables the seed exported\n - Use those variables to find and delete the created data\n\n Teardowns are the cleanest way to handle cleanup — they keep cleanup\n logic separate from test logic and run automatically.\n\nSANDBOX-ACTION-BACKED SEEDS\n\n When the property has a verified sandbox template with actions, a seed\n workflow can provision data by running a SANDBOX ACTION node instead of\n driving the UI or composing API calls. The seed's body is a single\n sandbox_action node (plus End); the action's outputs register as\n $var.{key} and satisfy the seed's declared outputs directly.\n\n - Fastest seed form: runs entirely server-side, no browser session\n - Discover actions with list_sandbox_actions; outputs come from the\n action's outputSchema\n - Constraint: consuming workflows become sandbox-only — on a\n non-sandbox environment the action node skips with a warning and\n produces no variables\n - Teardown is usually unnecessary: sandbox environments are destroyed\n after the suite, taking provisioned data with them\n\nSEED BEST PRACTICES\n\n - Use macros in all created data ($EMAIL(), $RANDOM_STRING(6), etc.)\n so each run gets unique, non-conflicting data\n - Keep seeds focused — one seed per logical dataset\n (e.g., \"Create Project with Members\" not \"Create Everything\")\n - Export only what downstream workflows need — don't over-export\n - Start with browser-based seeds, then use auto-conversion to generate\n an API sequence from a completed run\n - For sandbox-backed properties, prefer a sandbox-action-backed seed\n (see above) — server-side, fast, and reliable\n - Pair seeds with teardown workflows for automatic cleanup\n - To share seed data across several workflows in one test suite,\n reference the same seed from each; order dependent workflows with\n suite layers (setup nodes are deprecated)\n","A seeded user is a brand-new account minted for one run, created in the\napplication by a seed workflow, and handed to the tests that consume that\nseed as a login credential plus $var values. This section is the recipe for\nconfiguring one. The general seed model (outputs, execution modes,\nteardowns) is in the Seed Workflows section; this only covers the user case.\n\nWHEN TO SEED A USER (AND WHEN NOT TO)\n\n Use a persona (actor: Admin) when the test only needs SOME logged-in\n account of a role. Personas resolve per environment, need no seed, and are\n the default for every authenticated test.\n\n Seed a user when the test needs a NEW account it owns:\n - signup, invite acceptance, onboarding, first-login flows\n - anything that mutates or deletes the account itself (profile, MFA\n enrolment, password change, leave org, delete account)\n - a fresh inbox: verification codes, invite links, magic links\n - a user with no history, so counts/empty states are deterministic\n\n The seed-only-when-reused rule still applies: one seed serves every test\n that needs \"a fresh user\", so this is exactly the case a seed is for. Do\n not create a per-test copy — reference the same seed from each test.\n\n On sandbox environments the account can come from a sandbox action bound\n to a persona instead (see SANDBOX ALTERNATIVE at the end). This section\n covers the environment-agnostic seed-workflow path first.\n\nHOW IT WORKS (PLATFORM SIDE)\n\n A seed output with a credentialOutput block is a CREDENTIAL output. At the\n start of every run that references the seed, before the seed body runs,\n the platform:\n\n 1. Mints an email address from emailPrefix + the run's execution id:\n <emailPrefix>+<8 hex chars>@<test-user inbox domain>\n e.g. testuser+3fa92c1b@testuser.runcofactor.com\n 2. Resolves fieldMapping — \"$generate.email\" becomes that address,\n \"$generate.phone\" reserves an SMS number, any other value is a\n literal — and creates an ephemeral credential\n (\"[Ephemeral] <dataKey> (<first 8 chars of exec id>)\") on the\n credentialOutput's property, with the mapped fields as its login data.\n 3. Creates an inbox recipient for the address when\n createInboxRecipient is true, so the seed and its consumers can read\n mail sent to it.\n 4. Injects the mapped fields into the seed body as\n $var.<dataKey>.<field> e.g. $var.new_user.username\n $var.credential.<field> (alias; only unambiguous with one\n credential output)\n 5. Binds the credential to any downstream login node that references\n the seed output (credentialSource: from_seed), and deletes the\n credential after the suite finishes — or on seed failure — with a\n 24-hour age sweep as backstop.\n\n Minting the credential does NOT create the account in your application.\n That is the seed body's job (next section). A credential output with an\n empty seed body is a login that will fail.\n\nSTEP 1 — DECLARE THE CREDENTIAL OUTPUT\n\n Create the seed shell with the outputs declared up front; the\n credentialOutput block is only settable through JSON (CLI --from-file /\n --outputs-json, workflow patch --seed-outputs-file, or the Flow Designer\n output editor — .cofactor.md's ## Declared Outputs cannot author it, only\n preserve it on round-trip).\n\n {\n \"name\": \"Create invited user\",\n \"description\": \"Fresh org + admin invite for a seed-minted email\",\n \"outputs\": [\n {\n \"dataKey\": \"new_user\",\n \"name\": \"Invited User\",\n \"credentialOutput\": {\n \"propertyId\": \"<uuid of the application under test>\",\n \"loginUrl\": \"https://app.example.com/login\",\n \"emailPrefix\": \"qa-invite\",\n \"fieldMapping\": {\n \"username\": \"$generate.email\",\n \"password\": \"QaTest-Passw0rd!\"\n },\n \"createInboxRecipient\": true,\n \"loginInstructions\": \"Email/password form at /login.\"\n }\n },\n { \"dataKey\": \"invite_token\", \"name\": \"Invite token\" },\n { \"dataKey\": \"organization_id\", \"name\": \"Organization ID\" }\n ]\n }\n\n cofactor workflow seed create --from-file seed.json\n\n credentialOutput fields:\n propertyId REQUIRED. The application the credential belongs\n to; without it the output is skipped with a\n warning and downstream logins fail.\n fieldMapping REQUIRED. Credential field → value. Use\n \"$generate.email\" for the minted address (usually\n username, and email too if the login form wants\n both). Every other value is a literal stored\n verbatim — macros are NOT expanded here — so the\n password is exactly what you write; pick one your\n app's password policy accepts. Uniqueness comes\n from the email, not the password.\n emailPrefix Local-part prefix; defaults to \"testuser\". Use\n something that names the seed so inbox traffic is\n attributable.\n createInboxRecipient true whenever the flow must read a message sent to\n the user (verification code, invite link, magic\n link). Without it the address exists but nothing\n captures its mail.\n provisionPhoneNumber true (or a \"$generate.phone\" mapping) to reserve an\n SMS-capable number for OTP flows.\n loginUrl Where the login agent goes to sign in as this user.\n loginInstructions Extra guidance for the login agent (form location,\n SSO button to avoid, post-login landing).\n provisioningInstructions Only if the account needs a manual step before\n first login; marks the credential as not yet\n provisioned.\n\n Keep plain data outputs alongside the credential output for every id the\n consumers or the teardown will need (org id, user id, invite token). Declare\n ONLY what the seed body actually exports — a declared-but-unexported output\n shows up as \"missing\" on every run.\n\nSTEP 2 — MAKE THE SEED BODY CREATE THE ACCOUNT\n\n The body turns the minted email into a real account. Prefer an API\n sequence; fall back to browser nodes only when no API exists.\n\n Typical shapes:\n - Invite: admin/superadmin creates (or picks) an org → POST invite for\n $var.new_user.username → export the invite token. The consumer\n test then completes signup through the invite link.\n - Direct: POST /users or /signup with $var.new_user.username and\n $var.new_user.password → export the user id. The account is\n ready to log in.\n - Verify: if the app emails a verification code on signup, the seed can\n read it too (the inbox exists already) — but usually leave the\n verification step to the consumer test when that step IS what\n the test proves.\n\n Reference: the cofactor org's \"Create org & copy invite\" seed — an\n api_sequence of createOrg → switchOrg → createInvite (body\n {\"email\": \"$var.credential.username\", \"role\": \"admin\"}) exporting\n organization_id and invite_key, consumed by \"Create & Delete Org Property\".\n\n Rules for the api_sequence steps:\n - Authenticate as a persona (credentialTemplateId), never a hardcoded\n credentialId — the persona resolves per environment and is minted\n just-in-time on a fresh sandbox.\n - Target $var.ENV.api_url/..., never a baked host, so the same seed runs\n on dev, prod, and every sandbox.\n - Use $var.<dataKey>.username for the email. $var.credential.username\n also works but stops being unambiguous the moment a second credential\n output is added.\n - Export every id the teardown needs.\n\nSTEP 3 — CONSUME THE USER IN A TEST\n\n In the consuming workflow:\n\n 1. Add a Seed node referencing the seed (the seed's outputs become\n $var.<key> and $var.<Seed_Name>.<key>).\n 2. Point the login node at the seed's credential output:\n credentialSource: \"from_seed\"\n seedCredentialRef: { \"seedWorkflowId\": \"<seed uuid>\",\n \"dataKey\": \"new_user\" }\n performLogin: false when the test performs signup/first login\n itself (invite acceptance, onboarding)\n performLogin: true when the account is ready and the test starts\n logged in\n Both fields are required at run time, but you rarely write\n seedWorkflowId yourself: the Flow Designer's \"Acting as → from seed\"\n picker sets both, and the compile path (create-with-nodes, which\n .cofactor.md specs and raw JSON both go through) fills it from the\n nearest preceding Seed node when only dataKey is given. In a spec, put\n the **Seed** node first and write `actor: seed-output new_user` (or\n `**Switch role to** seed-output new_user` mid-journey).\n 3. Use the credential's fields in steps as $var.new_user.username and\n $var.new_user.password (signup forms, \"confirm password\", etc.), and\n the plain outputs as $var.invite_token etc.\n 4. Email steps: describe what to read — \"open the verification email\n sent to $var.new_user.username and follow the accept-invite link\".\n The agent has an inbox tool scoped to the credential's recipient. Do\n NOT hardcode a code value in the node's inputs; it is different every\n run.\n\n Several tests can reference the same seed; each run mints its own user, so\n they run in parallel without colliding. Order tests that must reuse ONE\n user across flows as suite layers instead.\n\nSTEP 4 — TEARDOWN\n\n Pair the seed with a teardown (created by default by `workflow seed\n create`). The teardown receives the same $var values and should delete the\n account through the API — the org if the seed created one, otherwise the\n user — using the exported ids. Same rules: persona auth, $var.ENV.api_url.\n The ephemeral credential and inbox recipient are removed by the platform;\n only your application's rows need the teardown.\n\n A seed that runs daily without a working teardown leaves one orphan user\n (or org) per run. Check once after publishing that the object is gone.\n\nCHECKLIST\n\n [ ] credentialOutput.propertyId set; fieldMapping has \"$generate.email\"\n [ ] createInboxRecipient: true if any step reads mail\n [ ] seed body creates the account and exports every id the teardown needs\n [ ] api_sequence steps use credentialTemplateId + $var.ENV.api_url\n [ ] declared outputs == exported outputs (no \"missing\" on the run)\n [ ] consumer login: from_seed with dataKey, and a Seed node before it\n (seedWorkflowId auto-fills from that Seed node at compile)\n [ ] no hardcoded verification codes in consumer inputs\n [ ] teardown deletes the app-side account/org; verified once by hand\n [ ] seed + teardown published (approved) before consumers are promoted\n\nSANDBOX ALTERNATIVE — ACTION-MINTED PERSONA\n\n On a sandbox template, a sandbox action with a credentialOutputConfig and\n a credentialTemplateId mints the account server-side and binds it to a\n PERSONA (e.g. provision-tenant → \"Admin\"). Tests then just say\n actor: Admin; the persona is provisioned just-in-time on each fresh\n instance and no seed node is needed. A second human member for \"act on\n someone else\" tests comes from a follow-up action (e.g. add-org-member,\n keyed to $var.USER.org_id). Constraint: consuming tests become\n sandbox-only, and no teardown is needed because the instance is destroyed.\n Prefer this on sandbox-backed properties; use the seed-workflow path\n everywhere else.\n","Test data discipline keeps workflows reliable and concurrent-safe.\n\nSTRONG TESTS: DEFAULTS AND THEIR WORKAROUNDS\n\n These are strong defaults, not absolutes. A test may deviate when the\n application makes a rule impossible — but it defaults to the rule, uses the\n workaround named here, and says so in ## Preflight / ## Cleanup so the\n deviation is visible, never silent.\n\n ## Preflight and ## Cleanup are the workflow's own preflight and cleanup\n notes (the spec sections of those names, or the workflow's preflight and\n cleanup fields), never the workflow description. People read them in the\n web UI: write them as short markdown bullet lists and name seeds,\n teardowns, connections and records, never raw IDs.\n\n 1. IDEMPOTENT — running the test again, including after a FAILED run,\n passes without a manual reset. Unique-name macros (\"QA Test —\n $RANDOM_STRING(6)\", $EMAIL()) make this true even when cleanup never\n happened.\n Workaround: if the app forbids duplicates (a singleton setting, one\n config record), revert the change in ## Cleanup and say so.\n\n 2. CONTROLS ITS OWN DATA — the test creates every record it operates on.\n Inline creation is the normal case: if the data is unique to this test,\n create it in the test. Use a seed workflow only when the data is (or\n should be) reused across several tests — a seed with one consumer is\n overhead, not hygiene. (Setup nodes are deprecated; do not use them.)\n Workaround: depending on data that already exists on the environment\n is fine AS LONG AS the test only reads it and never mutates it —\n confirm with the user that the discovered fixture is reliable for\n repeated runs, then declare it in ## Preflight (\"Property 'Acme Demo' exists with ≥1\n issue\"). A test that must mutate pre-existing data creates its own\n copy first, or states in Preflight why it can't.\n\n 3. ISOLATED — the test works only on data it created and never modifies\n shared resources as a side effect (billing, integrations, API keys, org\n config). Match the isolation level to the blast radius:\n record-level unique-id macros in a shared tenant — ordinary CRUD\n tenant/org-level a seeded org — the test mutates org-wide state\n application-level an ephemeral env / sandbox — it mutates global state\n Workaround: tests that genuinely depend on one another run as ordered\n LAYERS in a test suite (cofactor test-suite ... --layer, optionally\n --failure-mode stop_on_layer_failure). Layers run sequentially; members\n within a layer run in parallel. Never rely on implicit ordering between\n standalone flows.\n\n 4. CLEANUP SURVIVES FAILURE — a fatal assertion halts the run, so trailing\n inline cleanup nodes do NOT run. Seed-paired teardown workflows run on\n both pass and fail. Inline cleanup is therefore best-effort and pairs\n with unique naming (\"QA Test\" prefix + macro) so orphans are harmless\n and identifiable. When a record MUST be removed even after a failure\n (quota-bound, billed, visible to real users), that is the reason to move\n it into a seed + teardown pair.\n\n 5. EXTERNAL DEPENDENCIES ARE DECLARED OR CAPTURED — email delivery: give a\n seed credential output \"Create email inbox\" (the Email Inbox feature,\n /settings/inbox) so the flow can read the message. Payments, third-party\n APIs, feature flags: declare them in ## Preflight (cofactor feature-flag\n list --env <env> for exact flag names). Never assume an external\n system's state silently.\n\nSEED ONLY WHAT IS REUSED\n\n A seed workflow runs before your test, creates data, and exports variables\n downstream workflows reference as $var.{dataKey}. Reach for one when the\n same data serves several tests. When it does, you also get:\n - Faster setup — API sequences instead of browser interactions\n - Cleaner separation of setup and test logic\n - Paired teardown that runs on pass AND fail\n\n If only this test needs the data, create it inline (see CREATING TEST\n DATA below). See the Seed Workflows section for how to build seeds.\n\nSANDBOX BASE-SEED DATA ($var.SEED.*)\n\n On a sandbox environment, core baseline data (the application/property, a\n credential, sometimes a starter workflow) is provisioned automatically by a\n base seed and exposed as $var.SEED.{key} — e.g. $var.SEED.property_id.\n\n Prefer these over hardcoded names when you need a core entity that already\n exists on every tenant:\n Good: \"Open the property $var.SEED.property_id\"\n\n Base seeds are core-only. Anything test-specific (an issue, a notification,\n run history) is NOT in the base seed — create it yourself (see below) or via\n a per-test seed/custom action.\n\nTHE DATA OWNERSHIP PRINCIPLE\n\n Own your data:\n - Data you create → clean it up when the workflow finishes\n - Settings you modify → revert them when the workflow finishes\n - Data that was already there → never delete it\n\n It's fine to depend on pre-existing data (existing integrations, org\n settings, reference data) as long as the test only reads it — declare\n it in ## Preflight (rule 2 above). Create everything the test mutates.\n\nCREATING TEST DATA\n\n If the test operates on a record (edit, delete, archive), create that\n record as an early step. Operating on a record that already existed is\n a documented deviation, not the default (rule 2 above).\n\n Never reference \"any\" resource:\n Bad: \"Pick a project from the list\" (which one? may not exist)\n Good: \"Create a project named 'QA Test — $RANDOM_STRING(6)'\"\n Good: \"Use the seed output $var.project_id\"\n\n Use macros in newly created names to ensure uniqueness across parallel runs:\n Bad: \"Test Item\" (generic, will collide with other runs)\n Bad: \"QA Test Project\" (still collides if two runs overlap)\n Good: \"QA Test — $RANDOM_STRING(6)\" (unique every run)\n Good: \"QA Test $FIRST_NAME() $LAST_NAME()\" (unique and readable)\n\n After creating data, verify it exists before continuing.\n\nNAMING CONVENTION\n\n All test data should follow this pattern:\n\n Name/Title: \"QA Test $FIRST_NAME() $LAST_NAME()\"\n Email: \"$EMAIL()\"\n Phone: \"$PHONE()\"\n Project/Item: \"QA Test — $RANDOM_STRING(6)\"\n Unique ID: \"QA$TIMESTAMP()\"\n\n The \"QA Test\" prefix makes test data instantly recognizable. The macro\n suffix ensures every run creates unique data that won't collide with\n other runs or leftover data from previous executions.\n\nCLEANING UP (STRONGLY RECOMMENDED)\n\n Workflows that create data should include cleanup steps at the end.\n Workflows that modify settings should revert them.\n\n This isn't strictly required, but it's strongly recommended:\n - Cleanup prevents data accumulation over time\n - It keeps the test environment clean for other workflows\n - The \"QA Test\" naming convention makes orphaned data identifiable\n even when cleanup is skipped or fails\n\n Inline cleanup is best-effort: a fatal assertion halts the run before the\n cleanup nodes execute. Seed-paired teardowns run on pass and fail. Don't\n over-engineer inline cleanup — \"navigate to the item and delete it\" is\n enough; if a record must be gone even after a failure, provision it from a\n seed and let its teardown remove it (rule 4 above).\n\nWHAT NOT TO TOUCH\n\n - Don't create users, roles, or org-level config unless that's the goal\n - Don't modify shared resources (billing, integrations, API keys) as a side effect\n - Don't delete or modify data not owned by this workflow\n\nAVOIDING COLLISIONS WITH MACROS\n\n Use macros for newly created test names, emails, and identifiers. Keep\n confirmed existing reference names and captured identities intact. When every run generates unique values,\n there's no risk of conflicting with parallel runs or leftover data:\n\n $EMAIL() Unique email every run\n $RANDOM_STRING(6) Short unique suffix for names\n $UUID() Globally unique identifier\n $TIMESTAMP() Unique-per-millisecond numeric ID\n\n Combined with the \"QA Test\" prefix, this means you never need to\n worry about whether data from a previous run still exists. Each run\n creates its own isolated dataset.\n\nREAD-ONLY FLOWS\n\n Workflows that only read data (verify dashboard loads, check reports)\n don't need record creation or deletion. Restore any view state the journey\n changes. The presence of unrelated create/edit/delete controls does not\n expand the test's scope: exercise the specified behavior. A journey that\n tests writes owns those records and verifies their lifecycle. Only create\n what the test specifically requires.","Assertions verify that the application behaves as expected. There are two\nways to add assertions: inline on action nodes, or as standalone assertion\nnodes. Inline assertions are strongly preferred.\n\nINLINE ASSERTIONS (PREFERRED)\n\n Attach assertions directly to action nodes using the inlineAssertions\n field. These are verified immediately after the action completes and\n cached for fast replay.\n\n Inline assertions are the best way to verify outcomes because they:\n - Keep the test on track — verification happens right where the action is\n - Produce clean failure reports — each assertion is tied to the step that caused it\n - Are cacheable — fast replay on subsequent runs\n\n Each non-agentic inline assertion is proved by exactly one `verify` step in\n the playbook, bound to it by the assertion's id (see playbook.txt). That\n binding is what lets the check compile and replay at the point in the flow\n where it was actually verified, instead of trailing at the end of the node.\n\n Example — after submitting a form:\n Action: \"Submit the support ticket\"\n Inline assertions:\n - \"Success toast appears\" (severity: warn)\n - \"Ticket appears in the ticket list\" (severity: fail)\n - \"Form is no longer visible\" (severity: fatal)\n\n Severity levels — what a failure means for the run:\n fatal The test must end now. The run halts at this step.\n Use when continuing is pointless: login succeeded, the page\n loaded, a precondition every later step depends on.\n Example: \"User is logged in and the dashboard is visible\"\n fail The test may continue. The step is marked failed and the run\n finishes marked failed, but later steps still execute and\n produce signal.\n Use for independent checks: data saved, expected row present,\n error absent.\n Example: \"The new record appears in the table\"\n warn The test should continue. The failure is recorded as a warning\n and the run still passes.\n Use for cosmetic or non-critical checks: toast appeared,\n animation completed.\n Example: \"Success toast appears\"\n\n Choosing a severity: ask \"if this is wrong, is there any value in\n running the rest of the test?\" No → fatal. \"Does this being wrong mean\n the feature is broken?\" Yes → fail. Otherwise → warn. When unsure,\n prefer fatal for preconditions and fail for feature claims. Always set\n a severity explicitly — don't leave it unspecified.\n\n If verifying something requires significant interaction (scrolling,\n navigating, expanding sections), model that as an action node with\n inline assertions — not a detailed standalone assertion node.\n\nSTANDALONE ASSERTION NODES (USE SPARINGLY)\n\n Use dedicated assertion nodes only when the check doesn't belong to\n any action:\n\n - Verifying a final page state after navigation (no action to attach to)\n - Checking that data was NOT modified (absence checks after a sequence)\n - Confirming a state visible only after a wait or page transition\n\n A typical workflow has 0-1 standalone assertion nodes. If you find\n yourself writing a complex assertion node that requires multiple steps\n to verify, consider whether it should be an action node with inline\n assertions instead.\n\nAGENTIC ASSERTIONS\n\n For visual or layout checks that can't be mechanically validated,\n enable agent evaluation. These require the AI agent during replay\n and cannot be cached.\n\n Use for:\n - Screenshots that should \"look correct\"\n - Charts or graphs rendering properly\n Changing content, timestamps, and measurable layout can still be checked\n deterministically against current-run inputs and captured baselines.\n Cached assertions replay verification code, never an earlier verdict.\n\n Standard assertions are preferred — only use agentic when the\n check needs subjective judgment.\n\nWRITING GOOD CONDITIONS\n\n Assertions describe codified, user-visible behavior, not the mechanics\n used to observe it. Derive the expected result from the specification,\n product-map requirement, or confirmed user intent. Browser exploration\n discovers how to exercise and measure the behavior; it does not invent\n new acceptance requirements.\n\n Good: \"The named column stays fixed while level columns scroll.\"\n Good: \"After unpinning, the named column scrolls with the grid.\"\n Bad: \"aria-pressed=false and the SVG has an outlined path.\"\n Bad: \"Horizontal scroll increases at least 80 pixels.\" (unless required)\n\n A scroll distance or viewport size chosen for the test belongs in setup\n or the action. Measurements and comparison tolerances belong in replay\n code. Confirm the intended user during browser preflight; do not add a\n signed-in-user assertion unless identity/authentication is the journey.\n\n Assertions must be specific and observable:\n\n Good:\n - \"The status badge shows 'Active'\"\n - \"The table contains a row with name 'Test Record'\"\n - \"Error message is not visible\"\n - \"The page title is 'Dashboard'\"\n\n Bad:\n - \"The page looks correct\"\n - \"The action was successful\"\n - \"Everything works\"\n\n Start with what's observable: \"The page displays...\", \"The table\n contains...\", \"The field shows...\". Include the specific value\n you expect when the requirement specifies it. Do not assert exact\n incidental copy, attributes, or dimensions simply because they were observed.\n\nASSERTION BOUNDARIES & SELF-CONTAINED CHECKS\n\n Each action node should end at a checkpoint you can assert on — a modal\n disappeared, the URL changed, a row appeared, a toast fired. These boundaries\n double as the run's progress markers, so prefer a concrete end-state check\n to a vague one.\n\n Make every assertion SELF-CONTAINED. An action may replay from cache while\n its assertion is judged live by the agent, which does not have the action's\n execution context — so an assertion that silently assumes \"before\" state is\n unreliable. Relative or temporal checks are fine, but only when the baseline\n was captured upstream and referenced by variable:\n\n Good: \"current row count = $var.captured_row_count - 2\"\n Bad: \"the row count dropped by 2\" (no captured baseline — the live\n evaluator can't know the old count)\n\n Capture the baseline with declaredOutputs / a capture on the upstream node,\n then reference it as $var.<name> in the assertion.\n\nASSERTION PLACEMENT\n\n Place assertions at critical verification points:\n\n 1. After data creation: verify the record exists (inline on the create action)\n 2. After data modification: verify the change persisted (inline on the edit action)\n 3. After deletion: verify the item is gone (inline on the delete action)\n 4. At the goal completion point: verify the journey succeeded\n\n Don't assert everything — focus on the claims that matter.\n A focused assertion that proves the test's core claim is better\n than a dozen checks on every UI element.","API Sequences are the API-only counterpart to browser workflows. Instead of\ndriving a browser node-by-node, an API sequence is a single node\n(nodeType: \"api_sequence\") inside an api-only workflow that executes an\nordered list of HTTP calls, chaining outputs from one step into the inputs\nof the next.\n\nWHEN TO USE\n\n - Exercise an API without a UI (integration tests, fixture setup).\n - Seed or tear down state via API before/after a browser run.\n - Fast, deterministic smoke test of a critical flow.\n\nCONFIG SHAPE (ApiSequenceConfig)\n\n {\n \"steps\": [ ... ], // ordered; required\n \"description\": \"...\", // optional\n \"stopOnFailure\": true, // default: true\n \"exports\": [ ... ] // optional; see below\n }\n\nSTEP SHAPE (ApiSequenceStep)\n\n The authoritative type is in packages/types/src/api-sequence.ts. Key fields:\n\n {\n \"stepId\": \"createProject\", // optional; defaults to \"step{index}\"\n \"apiRequestId\": \"<catalog id>\", // required — points to api_requests row\n \"credentialId\": \"<uuid>\" | null, // null = unauthenticated\n \"credentialTemplateId\": \"<uuid>\" | null, // auth by persona/template (per-env); wins over credentialId\n \"credentialTemplateName\": \"<persona>\", // author-friendly alias for the above; resolved to the id on save\n \"method\": \"POST\", // optional; falls back to catalog default\n \"urlTemplate\": \"/projects/{{createProject.response.id}}\",\n \"queryParams\": [ { \"key\": \"limit\", \"value\": \"10\" } ],\n \"bodyTemplate\": \"{\\\"name\\\": \\\"$EMAIL()\\\"}\", // STRING (JSON), not object\n \"headersTemplate\": { \"X-Trace\": \"$UUID()\" }, // optional\n \"responseVariableName\": \"createProject\", // optional alias for stepId\n \"expectedStatusCodes\": [200, 201], // step fails if status is outside\n \"description\": \"...\", // optional UI label\n \"validations\": [ ... ], // assertions against the response\n \"validationMode\": \"all\" // \"all\" (default) or \"any\" validation must pass\n }\n\n CREDENTIAL ROLES — `credentialId` is overloaded; two distinct uses exist:\n\n - Step-level `credentialId` (and a top-level/default credential on the\n sequence) is AUTH: it's what the api-sequence runner uses to\n authenticate each HTTP call. Editing this changes who is calling.\n\n - Step-level `credentialTemplateId` is AUTH-by-persona: instead of a fixed\n credential id, name a credential template/persona and the runner resolves\n it to the credential bound for the run's environment (mirrors a login\n node's from_template). Takes precedence over `credentialId`. Use this when\n the same sequence must run across environments (e.g. Dev + a sandbox that\n mints its own per-instance credential) rather than a single static id.\n\n - `credentialId` appearing INSIDE a step's `bodyTemplate` JSON string is\n DATA: it gets written into the request body and becomes part of the\n provisioned resource (e.g. the credential a created workflow logs in\n with). Editing this changes what the call creates.\n\n The two are independent. Swapping the auth credential won't change what\n the call provisions; swapping the body credential won't change auth.\n Mixing them up silently produces confusing test failures.\n\n `apiRequestId` references a row in the `api_requests` table. Discover IDs\n from the API catalog for the target property. If `urlTemplate`, `method`,\n or `bodyTemplate` are omitted, they are backfilled from the catalog entry's\n defaults at execution time. A custom step that calls a URL directly uses\n `\"apiRequestId\": \"\"` and sets `method` and `urlTemplate` itself.\n\n `bodyTemplate` is a JSON *string*, not a JSON object — templates and macros\n are interpolated into it as raw text before parsing.\n\n MULTIPART UPLOADS — to send multipart/form-data instead of a raw body, set\n `formParts` on the step and omit `bodyTemplate`. The two are mutually\n exclusive; there is no separate mode field, a step is multipart exactly when\n `formParts` is present. Each part is either text or file:\n\n \"formParts\": [\n { \"kind\": \"text\", \"name\": \"projectId\", \"value\": \"$var.project_id\" },\n { \"kind\": \"file\", \"name\": \"file\", \"attachmentId\": \"<uuid>\" }\n ]\n\n A text part's `value` supports the same macro/variable/step-reference syntax\n as `bodyTemplate`. A file part names an attachment by UUID; add `filename`\n to override the name sent in Content-Disposition, otherwise the\n attachment's own filename is used. Don't set a `Content-Type` header on a\n multipart step: the transport generates its own boundary-bearing header at\n request time, so any author-supplied `Content-Type` is stripped rather than\n allowed to shadow it with a stale or missing boundary.\n\n API LOGIN CREDENTIALS — use these for token exchanges that precede the\n sequence request. POST bodies support application/json,\n application/x-www-form-urlencoded, and text/xml. With no body template,\n form fields are URL-encoded automatically. JSON token responses use normal\n dot paths such as access_token; XML/SOAP responses are parsed with namespace\n prefixes removed, so a Salesforce session path is\n Envelope.Body.loginResponse.result.sessionId. Keep returned tokens inside\n the Credential boundary — do not export them as workflow variables. Token\n request headers (for example Salesforce SOAPAction=login) are encrypted with\n the Credential and apply only to the token exchange.\n\nTEMPLATE SYNTAX\n\n Three ways to inject dynamic values. All three can appear in the same\n string (urlTemplate, bodyTemplate, headersTemplate values, or queryParams\n values).\n\n 1. STEP REFERENCES — outputs of prior steps.\n\n {{stepId.response.path.to.value}}\n\n The `.response.` segment is REQUIRED. This is the most common foot-gun:\n\n ✓ {{createProject.response.data.id}}\n ✗ {{createProject.data.id}} — missing .response., will warn at\n runtime and resolve to undefined\n ✗ {{projectId}} — no step reference at all\n\n The executor logs a warning for malformed references but does NOT\n block execution — a malformed template resolves to an empty string and\n the request goes out anyway. Always run once and check the run log.\n\n 2. MACROS — dynamic data generated fresh per run.\n\n $UUID() Random UUID v4\n $NOW() ISO 8601 timestamp (UTC)\n $NOW(+5m) ISO timestamp shifted by +5m / -2h / 30s / 1d\n $NOW(+5m, 'm H * * *') Shifted time rendered in UTC (see macros guide);\n single-quote the format inside a JSON bodyTemplate\n $TODAY() YYYY-MM-DD\n $DATE(N) Date offset by N days\n $TIMESTAMP() Unix ms\n $RANDOM_INT(min,max) Random integer in range\n $RANDOM_STRING(N) Alphanumeric string of length N\n $EMAIL() test-<rand>@example-test.com\n $FIRST_NAME() Random first name\n $LAST_NAME() Random last name\n $PHONE() +1-555-...\n $SEQUENCE(\"name\") Per-run auto-increment counter\n\n Each run resolves macros independently. $SEQUENCE counters persist\n across steps within a single run.\n\n 3. WORKFLOW VARIABLES — values injected by the workflow runtime.\n\n $var.<name>\n\n These come from two places:\n\n a) Upstream credential variables — pre-provisioned before the\n sequence runs. Typical names: `$var.credential.username`,\n `$var.credential.password`, `$var.credential.email`. If the\n workflow has a credential, USE these for the matching fields\n instead of `$EMAIL()` / `$RANDOM_STRING(6)` — the values must\n match the already-provisioned account.\n\n b) Upstream exports — values exported by earlier nodes in the same\n workflow (e.g. a seed step produced `$var.project_id`).\n\n Naming convention for exported variables: `resource_field` with\n underscores (e.g. `project_id`, `user_email`).\n\nEXPORTS\n\n After all steps finish, the sequence can register values from the final\n state into the workflow variable registry for downstream nodes:\n\n {\n \"exports\": [\n {\n \"variableName\": \"project_id\", // appears as $var.project_id\n \"stepId\": \"createProject\",\n \"responsePath\": \"data.id\",\n \"source\": \"response\", // \"response\" (default) | \"request\"\n \"description\": \"...\" // optional UI label\n }\n ]\n }\n\n Only export fields downstream nodes actually need. Use the\n `resource_field` naming convention.\n\nVALIDATIONS\n\n `validationMode` is \"all\" by default: every configured validation must pass.\n Set it to \"any\" to require at least one configured validation. The expected\n HTTP status requirement must still pass independently. Individual validation\n results retain their pass/fail values even when another alternative passes.\n\n Per-step assertions against the response. Each validation:\n\n {\n \"left\": \"createProject.response.data.status\", // \"stepId.path\" form\n \"operator\": \"equals\", // see list\n \"right\": \"active\", // omit for is_set/is_not_set\n \"description\": \"...\"\n }\n\n Operators: equals, not_equals, gt, lt, gte, lte, contains, not_contains,\n is_set, is_not_set.\n\nWHAT IS VALIDATED AT COMMIT TIME\n\n Save/update routes reject malformed step fields, unknown validation modes,\n invalid HTTP status codes, duplicate step IDs, invalid step references,\n unknown macros, and invalid exports. `validationMode` accepts only \"all\"\n or \"any\"; omitting it keeps all-checks behavior.\n\n Saving does not call the target API or prove its response. Smoke-test the\n sequence before relying on it in a workflow.\n\n CLI users: see `cofactor workflow guide cli-api-sequences` for command usage.\n\nEXAMPLE (3-step sequence, correct shape)\n\n {\n \"description\": \"Create an item, fetch it, delete it\",\n \"stopOnFailure\": true,\n \"steps\": [\n {\n \"stepId\": \"create\",\n \"apiRequestId\": \"<POST /items id>\",\n \"credentialId\": null,\n \"method\": \"POST\",\n \"bodyTemplate\": \"{\\\"name\\\": \\\"item-$RANDOM_STRING(6)\\\"}\",\n \"expectedStatusCodes\": [201]\n },\n {\n \"stepId\": \"fetch\",\n \"apiRequestId\": \"<GET /items/:id id>\",\n \"credentialId\": null,\n \"method\": \"GET\",\n \"urlTemplate\": \"/items/{{create.response.id}}\",\n \"expectedStatusCodes\": [200],\n \"validations\": [\n { \"left\": \"fetch.response.name\", \"operator\": \"is_set\" }\n ]\n },\n {\n \"stepId\": \"remove\",\n \"apiRequestId\": \"<DELETE /items/:id id>\",\n \"credentialId\": null,\n \"method\": \"DELETE\",\n \"urlTemplate\": \"/items/{{create.response.id}}\",\n \"expectedStatusCodes\": [200, 204]\n }\n ],\n \"exports\": [\n { \"variableName\": \"item_id\", \"stepId\": \"create\", \"responsePath\": \"id\" }\n ]\n }\n\nTIPS\n\n - Give every step a short, stable `stepId`. Later steps and exports\n reference it; the default `stepN` label is brittle to reordering.\n - Always include `.response.` in step references. The runtime won't\n error out, but values will silently resolve to empty strings.\n - Prefer `$EMAIL()`, `$UUID()`, `$RANDOM_STRING(6)` for fields that must\n be unique per run — unless the workflow has an upstream credential,\n in which case use `$var.credential.username` / `.password` / `.email`.\n - Set `expectedStatusCodes` on every step so unexpected 4xx/5xx fails\n the step instead of silently flowing into the next template.\n - `bodyTemplate` is a JSON STRING. Escape inner quotes.\n - `workflow api-sequence generate` can AI-draft a config from a prompt\n or a prior browser run; refine with `update`. The generated output is\n just JSON — review it before committing.\n","Use this section when you need to create or inspect seed workflows from the\nCofactor CLI. The conceptual model lives in `seed-workflows`; this section is\nonly command orientation. Prefer each command's `--help` output for the\ncurrent flag list.\n\nCREATE A SIMPLE SEED SHELL\n\n For simple data outputs, create the seed shell with repeatable output\n declarations:\n\n cofactor workflow seed create \\\n --name \"Create Test Project\" \\\n --description \"Provisions a test project with default settings\" \\\n --output \"project_id:Project ID:The created project's UUID\" \\\n --output \"project_name:Project Name\"\n\n This creates a draft seed workflow and, by default, a paired teardown\n workflow linked to the seed.\n\nCREATE A COMPLEX SEED SHELL\n\n For credential outputs or larger configs, put the full seed definition in a\n JSON file and create from that file:\n\n cofactor workflow seed create --from-file seed.json\n\n Keep generated credential fields in credentialOutput, and keep exported data\n fields in outputs. Downstream workflows consume both through $var.{dataKey}.\n `--outputs-json '<array>'` is the inline equivalent; to change the outputs\n of an existing seed use `cofactor workflow patch <id> --seed-outputs-file\n seed-outputs.json`. The credentialOutput shape and the end-to-end recipe\n are in `cofactor workflow guide seed-users`.\n\nAFTER CREATION\n\n Add the seed body through the Flow Designer or workflow sync, test both the\n seed and teardown, then publish. If the seed can be API-backed, generate or\n attach an API sequence after the browser version is proven.\n\n[cli >= 0.56.0]\nSET HOW OFTEN A SEED RUNS\n\n Share one seed execution across all consuming flows in a suite:\n\n cofactor workflow patch <id> --run-once-per-suite true\n\n Turn off the seed's forced suite scope (each consuming seed node's scope\n applies again):\n\n cofactor workflow patch <id> --run-once-per-suite false\n\n This flag accepts only true or false and is valid only for seed flows.\n Read the saved seedRunOncePerSuite setting with `cofactor workflow list\n --json`.\n[/cli]\n\nREFERENCE\n\n - Seed command help: `cofactor workflow seed --help`\n - Workflow sync help: `cofactor workflow sync --help`\n","Use this section when you need to drive API sequence authoring from the\nCofactor CLI. The conceptual model lives in `api-sequences`; this section is\nonly command orientation. Prefer each command's `--help` output for the\ncurrent flag list.\n\nCOMMON FLOW\n\n 1. Inspect the API catalog for candidate endpoints.\n\n cofactor api-catalog sources list\n cofactor api-catalog endpoints list --search <keyword>\n cofactor api-catalog endpoints get <apiRequestId>\n\n 2. Compose a sequence JSON file locally. Reference endpoints by\n apiRequestId and chain steps with {{stepId.response.path}}.\n\n 3. Create an API-only workflow from the sequence file.\n\n cofactor workflow create --api-only --name \"smoke: create project\" \\\n --sequence-from-file seq.json\n\n The command returns the workflow, sequence, and node IDs.\n\n To attach a sequence to an existing workflow that has none, run\n `cofactor workflow api-sequence update <workflowId> --from-file seq.json`;\n it creates the sequence when absent and replaces the config otherwise.\n\n 4. Execute immediately to surface template warnings and API failures.\n\n cofactor workflow api-sequence execute <workflowId>\n\n 5. Inspect the run and iterate on the sequence file.\n\n cofactor workflow runs list <workflowId> --side api\n cofactor workflow runs get <workflowId> <runId>\n cofactor workflow runs get <workflowId> <runId> --step <runStepId> --api-call <apiStepId>\n Add --json for JSON; the flags print stored (capped) request/response bodies.\n Get <runStepId> from runs get <workflowId> <runId> --json (steps[].id).\n cofactor workflow api-sequence update <workflowId> --from-file seq.json\n\nREFERENCE\n\n - Command help: `cofactor workflow api-sequence --help`\n - Workflow creation help: `cofactor workflow create --help`\n - API catalog help: `cofactor api-catalog --help`\n","Find out what failed, whether a suite is healthy, and what to do about a red\nrun. Read results first, then decide; do not guess from a status alone.\n\nRECENT SUITE RUNS\n\n cofactor test-suite executions\n cofactor test-suite get <executionId> --failures-only\n cofactor test-suite get <executionId> --failures-only --json\n\n `executions` lists recent suite runs; `get` shows one, and --failures-only\n keeps just the failing test runs. The execution id comes from `executions`,\n from `cofactor test-suite run`, or from a run page.\n\nONE FAILING TEST\n\n cofactor test-suite diagnostics <workflowId> <runId>\n cofactor test-suite diagnostics <workflowId> <runId> --thoughts\n cofactor workflow runs list <workflowId>\n\n `diagnostics` returns the failure diagnostic for one run (--thoughts adds the\n agent's reasoning trace). `workflow runs list` is the run history of one\n test: use it to see whether the failure is new, intermittent or constant.\n\nRUNS FROM YOUR OWN CI\n\n Results from a framework Cofactor ran for you (not a Cofactor Workflow) are\n external runs. Read `cofactor external-run guide` for get, failures, triage\n and rerun. For how tests are structured, read the other sections of this\n guide (`cofactor workflow guide`).\n\nREAL FAILURE OR FLAKE\n\n 1. A failure is real only when it reproduces with the same evidence: the\n same step, the same error, in more than one run.\n 2. An intermittent failure with different errors each time is a flake\n candidate. Check the run history before calling it one.\n 3. A failure that began right after a change is a real regression until\n shown otherwise.\n\nNEXT STEP\n\n - Real and new: file or find the issue (`cofactor issues guide`).\n - One unexplained failure: read the run's evidence (the diagnostic above),\n then hand it to `cofactor-debug-run` rather than guessing.\n - Flake: say so with the run history as evidence; do not delete the test.\n","Use this section to author a test straight from the CLI: plan the journey, check\nfor duplicates, create or update the workflow, run it, and hand back its URL.\nNo spec files are needed.\n\nAUTHORING LOOP\n\n 1. Understand the requirement. Read the ticket, PRD, or the user's intent, and\n explore the real UI first (see `best-practices` and `cofactor session guide`).\n 2. Avoid duplicates:\n cofactor workflow list --search \"<keywords>\"\n If a workflow already covers the journey, update it instead of creating one.\n 3. Create it (JSON on stdin or from a file; shapes are in `node-types`):\n cofactor workflow create --from-file test.json\n cofactor workflow create --from-file test.json --tags \"p0,checkout\"\n Input: { \"name\": \"...\", \"nodes\": [{ \"nodeType\": \"...\", \"configJson\": {...} }] }\n A node has nodeType, configJson and, for action nodes, a required \"title\"\n (plus id/orderIndex when reusing). The title is short and says what the user\n does; the detail goes in configJson.actionDescription:\n { \"nodeType\": \"action\", \"title\": \"Create a ticket\",\n \"configJson\": { \"actionDescription\": \"Fill in the required fields and save\" } }\n The title is what the run timeline, canvas and failure reports show. An\n action node without one is rejected: add \"title\": \"<short goal>\" to it.\n To act as a persona, make the first node a login node:\n {\"nodeType\": \"login\", \"configJson\": {\"credentialSource\": \"from_template\",\n \"credentialTemplateName\": \"<persona>\", \"performLogin\": true}}\n Use credentialTemplateId instead of the name when the persona name exists on\n more than one application. Without credentialSource \"from_template\" a login\n node expects a stored credentialId.\n 4. Change an existing workflow:\n cofactor workflow get <workflowId> (read it first)\n cofactor workflow sync <workflowId> --from-file test.json (replace all nodes)\n cofactor workflow node patch <workflowId> <nodeId> --config '{\"params\":{\"statement\":\"...\"}}'\n cofactor workflow patch <workflowId> --name \"...\" --description \"...\"\n cofactor workflow patch <workflowId> --preflight @preflight.md --cleanup @cleanup.md\n Reuse each existing node `id` in sync to keep a minimal diff. Every new or\n edited action step needs a \"title\"; an untitled legacy step you leave exactly\n as it is stays accepted. `workflow get --json` lists \"title\": null for such steps.\n Add --propose to sync to open a proposal for review instead of applying directly.\n `node patch --config` merges configJson one level deep: name the node's own\n keys (a sandbox_action's SQL is {\"params\":{\"statement\":\"...\"}}; an action's\n text is {\"actionDescription\":\"...\"}). A nested object is replaced whole, so\n \"params\" must list every param to keep. Run `workflow get` first and copy the\n key path from the node's configJson. `--set-input key=value` writes\n configJson.inputValues, which only action, seed and call_flow nodes read, and\n only for inputs the node (or the called flow) declares. On every other node\n type the value is never used: use --config.\n[cli >= 0.55.0]\n The CLI rejects --set-input on node types that ignore inputValues, and on an\n action input the node has no data requirement for.\n For multi-line SQL or prompts, put the JSON in a file: --config @patch.json\n[/cli]\n 5. Run it and look at the result:\n cofactor workflow run <workflowId> --stream\n cofactor workflow runs list <workflowId>\n cofactor workflow debug <runId> (when a run fails)\n To run on a sandbox you claimed: `cofactor workflow run <workflowId>\n --sandbox <sandboxId>`. The run targets the sandbox's own environment, so\n no --environment is needed. `cofactor sandbox claims` lists your live\n claims (ids, expiry) if you lost an id; `cofactor sandbox destroy <id>`.\n 6. Return the workflow URL printed by the CLI to the user verbatim. Never\n build the URL yourself.\n\n Approving a test asserts that a human verified it checks the right thing. Do\n not pass --yes to `workflow approve` until the user has confirmed.\n\nTHE RUBRIC: EVERY TEST MUST SATISFY ALL OF THESE\n\n 1. One coherent journey per workflow\n One user objective, possibly with several verifications along the way.\n \"Atomic\" means coherent, not minimal. If isolating a check would force you\n to duplicate most of another test's setup, fold it in as an inline\n assertion on the larger journey.\n Good: create org -> invite a user -> claim invite -> land on dashboard.\n Good: submit a ticket and confirm it appears in the queue.\n Bad: submit a ticket, edit a user, generate a report (three journeys,\n three workflows).\n Split only when the setup is genuinely different or the failure modes are\n independent. A long single journey stays together; independent journeys\n become separate workflows so they run in parallel and fail independently.\n\n 2. Specific, observable expected results\n Every assertion is something a stranger could verify without judgement,\n and comes from the requirement or confirmed user intent, not incidental\n DOM details seen while exploring. Keep conditions about user-visible\n behavior; do not invent ARIA/SVG checks or pixel thresholds.\n Good: \"URL ends with /dashboard.\" \"A row named X appears in the table.\"\n Bad: \"The page looks correct.\" \"It works.\"\n Attach assertions inline to the action that produced the result (see\n `assertions`). Pick severity deliberately: fatal ends the run (gating\n preconditions), fail marks the run failed but continues (feature claims),\n warn records without failing (cosmetics). A test with no run-failing\n assertion proves nothing.\n Relative checks (\"the count dropped by 2\") must reference a value captured\n upstream and used via $var.<name>, never rely on memory of the earlier state.\n\n 3. Idempotent, self-owned, isolated\n - Rerunning passes without a manual reset: use macros ($EMAIL(), $UUID(),\n \"QA Test $RANDOM_STRING(6)\") for anything that must be unique (see `macros`).\n - The test creates the records it operates on, inline. A seed workflow is\n for data that several tests share; a seed with one consumer is overhead.\n Data the test only reads may pre-exist, but never mutate it.\n - Never modify shared resources (billing, integrations, API keys, org\n config) as a side effect.\n - Tests that depend on one another run as ordered suite layers, never by\n implicit ordering.\n - A fatal assertion halts the run before trailing cleanup. Anything that\n must be gone after a failure belongs in a seed with a teardown (see\n `seed-workflows`, `data-lifecycle`).\n\n 4. Traceable\n Link the test to the product-map capability it proves:\n cofactor product-map link <CAP-code> <workflowId>\n Traceability comes from that link, not from text: do not write capability\n codes, ticket ids or requirement references into the name or description.\n If no capability covers the test, tell the user; untraceable tests rot.\n\n 5. Risk-prioritized\n Tag each test p0, p1, or p2 with --tags / --tags-add:\n p0 happy path of the core promise; if it breaks the feature is broken\n p1 primary edge cases, validation, role variants, common error states\n p2 rare conditions, visual or agentic checks, low-frequency edges\n\n 6. Preconditions, data, and cleanup are explicit\n Auth and role: use a persona (cofactor persona list --application \"<app>\")\n rather than raw usernames and passwords. Shared setup: a seed node, placed\n before login. Test data: workflow inputs with macros. Anything else the\n test depends on (pre-existing data it reads, flags, third-party systems)\n goes in the workflow's preflight, and how its side effects get reset goes\n in its cleanup. Write each as a short markdown bullet list:\n cofactor workflow patch <workflowId> --preflight @preflight.md\n cofactor workflow patch <workflowId> --cleanup @cleanup.md\n\n 7. The description says what the test is for\n Read together with the name, the description gives the spirit of the\n test: what we are doing and why it matters, in one or two plain\n sentences. It never holds capability codes, preconditions, issue ids,\n status (\"expected red\", \"blocked\"), step narration, endpoints, payloads,\n timings or cache details. Each of those lives elsewhere: the product-map\n link, the preflight, the linked issue, the run history, the nodes.\n People read the description, preflight and cleanup in the web UI, so\n never put a raw ID in them: no UUID, UUID prefix, wf_/prule_ or other\n prefixed id, issue id or run id. Name the thing instead (the seed\n workflow's name, the teardown's name, the connection's name, the\n record's name).\n Good: name \"Invite a teammate to Acme\"\n description \"An admin invites a new teammate by email and the\n teammate can sign in. Growing a team depends on invites\n reaching people.\"\n Bad: description \"CAP-ORG-004. Needs seed org + admin persona. Steps:\n open /settings/team, POST /api/invites, wait 5s, check inbox.\n Expected red until ISSUE-812 ships. Cache warm.\"\n Bad preflight:\n Seed 5e055090 creates an empty layer. Paired teardown d9a44386\n deletes the flow.\n Good preflight:\n - The \"Acme empty layer\" seed creates an empty layer\n - Its paired teardown, \"Acme empty layer teardown\", deletes the flow\n\n 8. Written as human intent, not UI mechanics\n Action titles say what the user is trying to do (\"Assign the ticket to\n Support\"), not click-by-click steps. Each action is one coherent,\n assertable step: usually broader than one click, ends at a visible\n boundary (modal gone, URL changed, row appeared), and has no conditionals\n inside it. If the data might not exist, seed it instead of branching.\n\nCHOOSING VALUES\n\n - Names existing data in the target app (a property, persona, credential, a\n seeded record): do not guess. Look it up with `cofactor property list`,\n `cofactor persona list --application \"<app>\"`, or\n `cofactor workflow list --search <term>`, and ask the user when several\n candidates fit.\n - Synthesizable (email, UUID, throwaway string, date): use a macro and move on.\n - Auth or role: prefer a persona over a raw credential.\n\nCOVERAGE CHECKLIST\n\n Before handing work back, for each item either include a test or say why it\n does not apply: happy path (p0); primary edges and boundary inputs; negative\n paths and validation errors; permission and role variants; empty, loading,\n and error states; cross-entity side effects (does this change something\n visible elsewhere?); cleanup or undo.\n\n Aim for the smallest set that proves the feature works. Five sharp tests beat\n twenty fuzzy ones.\n","Visual checkpoints capture a meaningful screen state and compare it with the\nrolling baseline for that checkpoint. Add one checkpoint at each state whose\nappearance matters to the user; avoid taking several screenshots of the same\nstate just because the journey has several steps.\n\nA pull request that changes how a screen looks needs one of these as evidence:\nsee `cofactor proofs guide`.\n\n[cli >= 0.57.0]\nLINK CHECKPOINTS TO WHAT THEY SHOW\n\n Link a screen that proves one rule with:\n cofactor product-map rule link <prule_…> --workflow <wf_…> --node <checkpointNodeId>\n Link a screen that shows a capability in general with:\n cofactor product-map link <CAP_CODE> <wf_…> --node <checkpointNodeId>\n Remove that direct capability link with unlink and the same arguments.\n --node requires exactly one flow. Link directly whenever the flow covers\n more than one capability; otherwise Screens uses the flow's capability.\n\n Suite runs at the default branch supply rolling baselines. A person can\n review a PR's head capture in Screens: if accepted, it joins the baseline\n when the PR merges, like flow proofs. Before merge it stays pending and\n never changes the live atlas. An unapproved fork is reachable through that\n PR review link even though it has no live tile.\n\n A checkpoint proves a rule only after a person has accepted the screen.\n It proves how the screen looks, not its values: dates and numbers are\n ignored, so prove values with an assertion. A later mask change needs\n acceptance before it can count as rule proof, even when the tile matches.\n[/cli]\n\nNATIVE WORKFLOW CHECKPOINTS\n\n First ask: where must this test run?\n - Sandbox only: a SQL fixture is fine. This is the usual case for a visual\n regression test comparing an exact state at a commit.\n - Also against staging or prod: seed only through the app's API, generic\n API-backed actions, or the test's own UI steps. Only a sandbox exposes its\n database. Keep checkpoints on regions that do not depend on exact data,\n or rely on masking (automatic/healed for native checkpoints; see below).\n A test that loads SQL is sandbox-only.\n\n Seed the chosen state first, then capture it.\n Choose among three kinds of test data:\n - Seeds: shared, thin, generic base data nearly every test needs.\n - Actions: typed, parameterized, generic operations; prefer the app's API.\n - Visual-state fixtures: one SQL file per screen state, kept beside its test.\n Use fixed, neutral values and timestamps relative to now() (for example,\n now() - interval '2 days') so relative-time text stays stable. Fixtures are\n never parameterized or shared; do not build a library of per-test seeds.\n\n SQL fixtures are the default for sandbox-only visual data, only inserting\n rows with no encryption and no side effects. For encrypted rows or side\n effects, use the app's API or a generic action that calls it. Use UI steps for\n interaction states such as an open menu or validation feedback. A visual test\n prepares one meaningful state and takes its checkpoint.\n\n The `target` is optional plain English describing an element, such as\n `the invoices table`; omit it to capture the whole page. Never put a CSS,\n XPath, or Playwright selector in `target`.\n\n `expected` is optional plain language describing the correct visible state.\n It guides the AI in deciding whether the current state is correct and whether\n a visual change is real or can be healed. Use a specific observable\n description, for example, \"Three invoices are listed, each marked Paid, with\n no error banner.\"\n\n `record` is the default. It records visual findings without changing the\n workflow result, and compares after the suite finishes. Choose `fail` when an\n unresolved visual change should fail this workflow step; it compares inline.\n Under either severity, a `target` that cannot be found is reported as a\n checkpoint that could not be compared, and under `fail` it fails the step.\n Both severities appear in Visual results.\n\n Dynamic text is masked automatically. If the AI heals a difference, it saves\n an element mask and its reason on the checkpoint. The review shows the heal;\n a reviewer can revert the mask or mark the change as a regression. Masks are\n automatic or healed; never author mask rules yourself.\n\nBEFORE YOUR FIRST VISUAL TEST\n\n For a sandbox-only SQL test, check cofactor.sandbox.yaml for a generic\n SQL-loading action. If absent, add one to the app's definition. For Postgres,\n this action loads a repo-relative .sql path from the claimed commit. Claim\n the commit containing both the test and its fixture. Install git and psql\n in the sandbox; DATABASE_URL must already name its sandbox database in env\n (or repository secrets). Use your app's database URL variable if different.\n\n actions:\n load-sql:\n description: Load a test's SQL fixture into the sandbox database.\n run: sandbox/actions/load-sql.sh\n input:\n path: { type: string, required: true, description: Repo-relative .sql file }\n output:\n ok: { type: boolean }\n\n # sandbox/actions/load-sql.sh\n #!/usr/bin/env bash\n set -euo pipefail\n git show \"HEAD:$INPUT_PATH\" | \\\n psql \"$DATABASE_URL\" -X --set=ON_ERROR_STOP=1 --single-transaction >&2\n printf '{\"ok\":true}\\n'\n\n The generic loader is shared; each screen's fixture belongs only to its test.\n SQL errors fail the action; success is the last JSON line on stdout.\n\nSTANDALONE VISUAL REGRESSION SPECS\n\n These complete `.cofactor.md` examples use the current spec grammar. Replace\n the fixture path, persona, and routes with your application's. Prepare one\n meaningful state and capture it. Sandboxes v2 supplies the run.\n\n Keep invoice-list.sql beside invoice-list.cofactor.md. It inserts three Paid\n invoices with neutral, fixed values and dates relative to now(). Load it,\n then checkpoint the populated list:\n\n ---\n name: Invoice list visual regression\n actor: admin\n authenticated: true\n ---\n\n ## Spirit\n > An admin can see three invoices and their payment status.\n\n ## Journey\n\n **Sandbox action — load-sql**\n - param.path: tests/visual/invoice-list.sql\n\n **Navigate to** `/invoices`\n\n **Visual checkpoint — Populated invoice list**\n - target: the invoices table\n - expected: Three invoices are listed, each marked Paid, with no error banner.\n - severity: fail\n\n **End**.\n\n Submit invalid form data and checkpoint the validation state:\n\n ---\n name: Signup validation visual regression\n authenticated: false\n ---\n\n ## Spirit\n > A visitor sees clear validation feedback when required signup fields are missing.\n\n ## Journey\n\n **Navigate to** `/signup`\n\n **Action — Submit the empty signup form**\n > Submit without entering any values so the required-field messages appear.\n - assert (fail): A required-field validation message is visible.\n\n **Visual checkpoint — Required-field validation**\n - target: the signup form\n - expected: Required fields have clear validation messages and the form has not been submitted.\n - severity: record\n\n **End**.\n\nPLAYWRIGHT ATTACHMENT CHECKPOINTS\n\n In an external Playwright suite running on Sandboxes v2, an attached PNG is a\n visual checkpoint:\n\n await testInfo.attach(name, { body: await page.screenshot({ mask }), contentType: 'image/png' })\n\n Its identity is the spec file, test title, and attachment name. Keep masking\n in the test code with Playwright's `mask` option. The service explains and\n reports Playwright changes but never fails the Playwright test. Failure\n screenshots, trace frames, and `toHaveScreenshot` actuals are excluded. Do\n not use `toHaveScreenshot` for this feature; attach the screenshot instead.\n\nREVIEW RESULTS\n\n Review changes and comparisons on the Visual page. From the CLI, use\n `cofactor visual list` to find captures. Mark or clear a regression with\n `cofactor visual regression <captureId>` or\n `cofactor visual regression <captureId> --clear`.\n","import type { GuideSection } from '../types.js';\n\nimport overview from './overview.txt';\nimport nodeTypes from './node-types.txt';\nimport specGrammar from './spec-grammar.txt';\nimport actionNodes from './action-nodes.txt';\nimport playbook from './playbook.txt';\nimport bestPractices from './best-practices.txt';\nimport macros from './macros.txt';\nimport seedWorkflows from './seed-workflows.txt';\nimport seedUsers from './seed-users.txt';\nimport dataLifecycle from './data-lifecycle.txt';\nimport assertions from './assertions.txt';\nimport apiSequences from './api-sequences.txt';\nimport cliSeeds from './cli-seeds.txt';\nimport cliApiSequences from './cli-api-sequences.txt';\nimport triageResults from './triage-results.txt';\nimport writeATest from './write-a-test.txt';\nimport visualCheckpoints from './visual-checkpoints.txt';\n\nexport const WORKFLOW_GUIDE_CONCEPT_SECTIONS: GuideSection[] = [\n { title: 'Workflow Overview', slug: 'overview', content: overview },\n { title: 'Node Types', slug: 'node-types', content: nodeTypes },\n { title: 'Spec file grammar (.cofactor.md)', slug: 'spec-grammar', content: specGrammar },\n { title: 'Action Nodes', slug: 'action-nodes', content: actionNodes },\n { title: 'Playbooks & Cache', slug: 'playbook', content: playbook },\n { title: 'Best Practices', slug: 'best-practices', content: bestPractices },\n { title: 'Macros & Variables', slug: 'macros', content: macros },\n { title: 'Seed Workflows', slug: 'seed-workflows', content: seedWorkflows },\n { title: 'Seeding a Test User', slug: 'seed-users', content: seedUsers },\n { title: 'Test Data Lifecycle', slug: 'data-lifecycle', content: dataLifecycle },\n { title: 'Assertions', slug: 'assertions', content: assertions },\n { title: 'API Sequences', slug: 'api-sequences', content: apiSequences },\n { title: 'Visual Checkpoints', slug: 'visual-checkpoints', content: visualCheckpoints },\n];\n\nexport const WORKFLOW_GUIDE_CLI_SECTIONS: GuideSection[] = [\n { title: 'CLI: Seed Workflows', slug: 'cli-seeds', content: cliSeeds },\n { title: 'CLI: API Sequences', slug: 'cli-api-sequences', content: cliApiSequences },\n { title: 'Triage Test Results', slug: 'triage-results', content: triageResults },\n { title: 'Write a Test from the CLI', slug: 'write-a-test', content: writeATest },\n];\n\nexport const WORKFLOW_GUIDE_SECTIONS: GuideSection[] = [\n ...WORKFLOW_GUIDE_CONCEPT_SECTIONS,\n ...WORKFLOW_GUIDE_CLI_SECTIONS,\n];\n","import { filterGuideForCli, isCliVersion } from './cli-version-filter.js';\nimport { DEV_GUIDE_SECTIONS } from './dev-guide/index.js';\nimport { EVAL_GUIDE_SECTIONS } from './eval-guide/index.js';\nimport { EXPLORATORY_TEST_GUIDE_SECTIONS } from './exploratory-test-guide/index.js';\nimport { EXTERNAL_RUN_GUIDE_SECTIONS } from './external-run-guide/index.js';\nimport { ISSUES_GUIDE_SECTIONS } from './issues-guide/index.js';\nimport { PERF_GUIDE_SECTIONS } from './perf-guide/index.js';\nimport { PRODUCT_MAP_GUIDE_SECTIONS } from './product-map-guide/index.js';\nimport { PROOFS_GUIDE_SECTIONS } from './proofs-guide/index.js';\nimport { SANDBOX_GUIDE_SECTIONS } from './sandbox-guide.js';\nimport { SANDBOX_V2_GUIDE_SECTIONS } from './sandbox-v2-guide/index.js';\nimport { SESSION_GUIDE_SECTIONS } from './session-guide/index.js';\nimport type { GuideSection } from './types.js';\nimport { WORKFLOW_GUIDE_SECTIONS } from './workflow-guide/index.js';\n\n/**\n * Every CLI guide, keyed by domain. The CLI (bundled fallback) and the API\n * (`GET /cli/guides/:domain`) both read this registry, so a domain cannot\n * exist on one side only.\n */\nexport const GUIDES = {\n workflow: WORKFLOW_GUIDE_SECTIONS,\n sandbox: SANDBOX_GUIDE_SECTIONS,\n 'sandbox-v2': SANDBOX_V2_GUIDE_SECTIONS,\n 'product-map': PRODUCT_MAP_GUIDE_SECTIONS,\n 'exploratory-test': EXPLORATORY_TEST_GUIDE_SECTIONS,\n 'external-run': EXTERNAL_RUN_GUIDE_SECTIONS,\n issues: ISSUES_GUIDE_SECTIONS,\n session: SESSION_GUIDE_SECTIONS,\n proofs: PROOFS_GUIDE_SECTIONS,\n perf: PERF_GUIDE_SECTIONS,\n eval: EVAL_GUIDE_SECTIONS,\n dev: DEV_GUIDE_SECTIONS,\n} as const satisfies Record<string, GuideSection[]>;\n\nexport { isCliVersion };\n\nexport type GuideDomain = keyof typeof GUIDES;\n\nexport function isGuideDomain(value: string): value is GuideDomain {\n return Object.hasOwn(GUIDES, value);\n}\n\n/** Apply `[cli >= X]` filtering to every section of a guide. */\nexport function filterGuideSections(\n sections: readonly GuideSection[],\n cliVersion: string\n): GuideSection[] {\n // Validated once, outside the per-section wrapper, so a caller fault is never reported as a guide fault.\n if (!isCliVersion(cliVersion)) throw new Error('Invalid cliVersion');\n return sections.map((s) => {\n try {\n return { ...s, content: filterGuideForCli(s.content, cliVersion) };\n } catch (cause) {\n throw new Error(\n `Guide section \"${s.slug}\": ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause }\n );\n }\n });\n}\n","/**\n * `<domain> guide` printing: fetch the latest guide from the API, fall back to\n * the copy bundled in this binary.\n *\n * Guides are served so guidance can improve without a CLI release. The bundled\n * copy is the fallback for agents with no network, in CI and in sandboxes\n * without egress. Both copies pass through the same `[cli >= X]` filter with\n * this binary's version. There is no disk cache: a cache would reintroduce the\n * staleness this exists to remove.\n */\nimport { stripControlCharacters, type GuideSection } from '@chatsdet/content';\nimport { GUIDES, filterGuideSections, type GuideDomain } from '@chatsdet/content/guides';\nimport { hasFlag, resolveApiUrl } from './auth.js';\nimport { command, version } from './brand.js';\nimport { getGuideSectionArg } from './cli-helpers.js';\n\nconst GUIDE_FETCH_TIMEOUT_MS = 3000;\n\n/** A 200 that is not a guide (captive portal, proxy, shape change) must fall back, not print nothing. */\nfunction isGuideSections(value: unknown): value is GuideSection[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every(\n (s) =>\n typeof s?.title === 'string' && typeof s?.slug === 'string' && typeof s?.content === 'string'\n )\n );\n}\n\ntype Served = { sections: GuideSection[] } | { unavailable: string };\n\nasync function fetchServedSections(\n apiUrl: string,\n domain: GuideDomain,\n section: string | undefined\n): Promise<Served> {\n try {\n let url = `${apiUrl.replace(/\\/+$/, '')}/cli/guides/${domain}?cliVersion=${encodeURIComponent(version)}`;\n if (section) url += `&section=${encodeURIComponent(section)}`;\n const res = await fetch(url, { signal: AbortSignal.timeout(GUIDE_FETCH_TIMEOUT_MS) });\n if (!res.ok) return { unavailable: `http ${res.status}` };\n const body = (await res.json()) as { data?: { sections?: unknown } };\n if (!isGuideSections(body.data?.sections)) return { unavailable: 'unexpected response' };\n // Served text is untrusted (any --api-url): it reaches a terminal and an agent.\n return {\n sections: body.data.sections.map((s) => ({\n title: stripControlCharacters(s.title),\n slug: stripControlCharacters(s.slug),\n content: stripControlCharacters(s.content),\n })),\n };\n } catch (error) {\n // No network, DNS failure, timeout or unparseable body: the caller prints the bundled copy and says why.\n return { unavailable: error instanceof Error ? error.name : 'network error' };\n }\n}\n\n/** Print `<domain> guide [section] [--json]` from the served copy, else the bundled one. */\nexport async function printServedGuide(domain: GuideDomain, argv: string[]): Promise<void> {\n const section = getGuideSectionArg(argv);\n const served = await fetchServedSections(await resolveApiUrl(argv), domain, section);\n // stderr so stdout and --json stay unchanged; names which copy the agent read.\n console.error(\n 'sections' in served\n ? `guide: latest (served for ${command} ${version})`\n : `guide: bundled with ${command} ${version} (served copy unavailable: ${served.unavailable})`\n );\n renderGuide(\n 'sections' in served ? served.sections : filterGuideSections(GUIDES[domain], version),\n section,\n hasFlag(argv, '--json')\n );\n}\n\n/** Print the sections, or the one named by `section` (exit 1 listing the available slugs if unknown). */\nfunction renderGuide(\n allSections: GuideSection[],\n section: string | undefined,\n json: boolean,\n): void {\n const sections = section\n ? allSections.filter((s) => s.slug === section)\n : allSections;\n\n if (section && sections.length === 0) {\n console.error(\n `Unknown guide section: \"${section}\". Available: ${allSections.map((s) => s.slug).join(', ')}`,\n );\n process.exit(1);\n }\n\n if (json) {\n console.log(JSON.stringify(sections, null, 2));\n return;\n }\n\n for (const s of sections) {\n console.log(`\\n${'='.repeat(72)}`);\n console.log(` ${s.title}`);\n console.log(`${'='.repeat(72)}\\n`);\n console.log(s.content);\n console.log('');\n }\n console.log(GUIDE_FEEDBACK_LINE);\n}\n\n/** Printed after every CLI guide: how an agent tells us the guide fell short. */\nconst GUIDE_FEEDBACK_LINE =\n 'Feedback: if this guide confused you or a tool was missing, run `cofactor feedback \"<what happened, what would have helped>\" --topic instructions`. No secrets, customer data, real names or URLs; use neutral examples (Acme, example.com).';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkBO,IAAM,4BAAqD;AAAA;AAAA,EAEhE;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA,EAAE,MAAM,aAAa,MAAM,UAAU,SAAS,UAAU,KAAK,mDAAmD;AAAA,EAChH;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA,EAAE,MAAM,aAAa,MAAM,kBAAkB,SAAS,UAAU,KAAK,uBAAuB;AAAA,EAC5F;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA,EAAE,MAAM,qBAAqB,MAAM,uBAAuB,SAAS,UAAU,KAAK,uBAAuB;AAAA,EACzG;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA,EAAE,MAAM,kBAAkB,MAAM,sBAAsB,SAAS,UAAU,KAAK,4BAA4B;AAAA;AAAA,EAG1G;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AACF;AAGA,IAAM,gBAAgB,cAAc,SAAS;AAC7C,IAAM,aAAa;AAGZ,SAAS,sBAA8B;AAC5C,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,0BAA0B,IAAI,CAAC,UAAU,MAAM,IAAI,CAAC,CAAC;AAC/E,SAAO,MACJ,IAAI,CAAC,SAAS;AACb,UAAM,OAAO,0BAA0B,OAAO,CAAC,UAAU,MAAM,SAAS,IAAI,EAAE;AAAA,MAAI,CAAC,UACjF;AAAA,QACE,GAAG,KAAK,MAAM,MAAM,KAAK,MAAM,QAAQ,OAAO,aAAa,CAAC,EAAE;AAAA,QAC9D,GAAG,KAAK,MAAM,KAAK,IAAI,OAAO,gBAAgB,CAAC,CAAC;AAAA,MAClD,EAAE,KAAK,IAAI;AAAA,IACb;AACA,WAAO,GAAG,KAAK,YAAY,CAAC;AAAA;AAAA,EAAO,KAAK,KAAK,IAAI,CAAC;AAAA,EACpD,CAAC,EACA,KAAK,MAAM;AAChB;AAGA,SAAS,KAAK,MAAc,OAAyB;AACnD,QAAM,SAAS,IAAI,OAAO,MAAM,MAAM;AACtC,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAO;AACX,aAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,QAAI,KAAK,SAAS,OAAO,UAAU,KAAK,SAAS,KAAK,SAAS,IAAI,YAAY;AAC7E,YAAM,KAAK,IAAI;AACf,aAAO,SAAS;AAAA,IAClB,OAAO;AACL,cAAQ,KAAK,SAAS,OAAO,SAAS,IAAI,IAAI,KAAK;AAAA,IACrD;AAAA,EACF;AACA,QAAM,KAAK,IAAI;AACf,SAAO;AACT;;;AC3RA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACyBO,IAAM,4BAA4C;AAAA,EACvD,EAAE,OAAO,yBAAyB,MAAM,YAAY,SAAS,iBAAS;AAAA;AAAA,EAEtE,EAAE,OAAO,+CAA+C,MAAM,eAAe,SAAS,sBAAc,oBAAoB,EAAE;AAAA,EAC1H,EAAE,OAAO,sCAAsC,MAAM,cAAc,SAAS,mBAAW;AAAA,EACvF,EAAE,OAAO,0BAA0B,MAAM,gBAAgB,SAAS,qBAAa;AAAA,EAC/E,EAAE,OAAO,qBAAqB,MAAM,qBAAqB,SAAS,0BAAgB;AAAA,EAClF,EAAE,OAAO,iDAAiD,MAAM,qBAAqB,SAAS,0BAAgB;AAAA,EAC9G,EAAE,OAAO,YAAY,MAAM,YAAY,SAAS,iBAAS;AAAA,EACzD,EAAE,OAAO,kBAAkB,MAAM,kBAAkB,SAAS,uBAAe;AAAA,EAC3E,EAAE,OAAO,+BAA+B,MAAM,OAAO,SAAS,YAAI;AAAA,EAClE,EAAE,OAAO,8BAA8B,MAAM,WAAW,SAAS,gBAAQ;AAAA,EACzE,EAAE,OAAO,aAAa,MAAM,aAAa,SAAS,kBAAU;AAAA,EAC5D,EAAE,OAAO,8BAA8B,MAAM,kBAAkB,SAAS,uBAAc;AAAA,EACtF,EAAE,OAAO,SAAS,MAAM,SAAS,SAAS,cAAM;AAAA,EAChD,EAAE,OAAO,wCAAwC,MAAM,UAAU,SAAS,eAAO;AAAA,EACjF,EAAE,OAAO,gDAAgD,MAAM,SAAS,SAAS,cAAM;AAAA,EACvF,EAAE,OAAO,gBAAgB,MAAM,UAAU,SAAS,eAAO;AAAA,EACzD,EAAE,OAAO,iBAAiB,MAAM,OAAO,SAAS,YAAI;AACtD;;;AC5BA,IAAM,OAAO;AACb,IAAM,QAAQ;AAEd,IAAM,cAAc;AACpB,IAAM,SAAS;AAKf,SAAS,YAAY,OAA8B;AACjD,QAAM,QAAQ,OAAO,KAAK,KAAK;AAC/B,SAAO,QAAQ,CAAC,OAAO,MAAM,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI;AAC1E;AAGO,SAAS,aAAa,OAAwB;AACnD,SAAO,YAAY,KAAK,MAAM;AAChC;AAEA,SAAS,QAAQ,GAAW,GAAoB;AAC9C,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAQ,EAAE,CAAC,IAAgB,EAAE,CAAC;AAAA,EACnD;AACA,SAAO;AACT;AAOO,SAAS,kBAAkB,SAAiB,YAA4B;AAC7E,QAAM,MAAM,YAAY,UAAU;AAClC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,oBAAoB;AAAA,EACtC;AAEA,MAAI,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,SAAS,OAAO,EAAG,QAAO;AACpE,QAAM,MAAgB,CAAC;AACvB,MAAI,SAAwB;AAC5B,MAAI,OAAO;AAEX,QAAM,QAAQ,QAAQ,MAAM,OAAO;AACnC,aAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,UAAM,SAAS,QAAQ;AACvB,UAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,QAAI,MAAM;AACR,UAAI,WAAW,MAAM;AACnB,cAAM,IAAI,MAAM,6BAA6B,MAAM,0BAA0B,MAAM,GAAG;AAAA,MACxF;AACA,eAAS;AACT,YAAM,UAAU,KAAK,CAAC;AACtB,aAAO,QAAQ,KAAK,YAAY,OAAO,CAAW;AAClD,UAAI,CAAC,MAAM;AACT,YAAI,KAAK,mBAAmB,OAAO,mDAAmD;AAAA,MACxF;AAAA,IACF,WAAW,MAAM,KAAK,IAAI,GAAG;AAC3B,UAAI,WAAW,MAAM;AACnB,cAAM,IAAI,MAAM,kBAAkB,MAAM,gCAAgC;AAAA,MAC1E;AACA,eAAS;AACT,aAAO;AAAA,IACT,WAAW,YAAY,KAAK,IAAI,GAAG;AACjC,YAAM,IAAI,MAAM,gCAAgC,MAAM,MAAM,KAAK,KAAK,CAAC,GAAG;AAAA,IAC5E,WAAW,MAAM;AACf,UAAI,KAAK,IAAI;AAAA,IACf;AAAA,EACF;AAEA,MAAI,WAAW,MAAM;AACnB,UAAM,IAAI,MAAM,6CAA6C,MAAM,EAAE;AAAA,EACvE;AACA,SAAO,IAAI,KAAK,IAAI;AACtB;;;ACzFA;;;ACGO,IAAM,qBAAqC;AAAA,EAChD,EAAE,OAAO,+BAA+B,MAAM,SAAS,SAAS,cAAM;AACxE;;;ACLA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,oBAAA;;;ACWO,IAAM,6BAA6C;AAAA,EACxD,EAAE,OAAO,SAAS,MAAM,SAAS,SAAS,cAAM;AAAA,EAChD,EAAE,OAAO,aAAa,MAAM,aAAa,SAAS,kBAAU;AAAA,EAC5D,EAAE,OAAO,UAAU,MAAM,UAAU,SAAS,eAAO;AAAA,EACnD,EAAE,OAAO,qBAAqB,MAAM,UAAU,SAAS,eAAO;AAAA,EAC9D,EAAE,OAAO,SAAS,MAAM,SAAS,SAAS,cAAM;AAAA,EAChD,EAAE,OAAO,YAAY,MAAM,YAAY,SAAS,iBAAS;AAAA,EACzD,EAAE,OAAO,eAAe,MAAM,eAAe,SAAS,oBAAW;AAAA,EACjE,EAAE,OAAO,eAAe,MAAM,eAAe,SAAS,oBAAY;AAAA,EAClE,EAAE,OAAO,YAAY,MAAM,YAAY,SAASC,kBAAS;AAC3D;;;ACrBA,IAAAC,oBAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACAA;;;ACoBO,IAAM,kCAAkD;AAAA,EAC7D,EAAE,OAAO,qBAAqB,MAAM,YAAY,SAASC,kBAAS;AAAA,EAClE,EAAE,OAAO,cAAc,MAAM,cAAc,SAAS,mBAAU;AAAA,EAC9D,EAAE,OAAO,oCAAoC,MAAM,gBAAgB,SAAS,qBAAY;AAAA,EACxF,EAAE,OAAO,gBAAgB,MAAM,gBAAgB,SAAS,qBAAY;AAAA,EACpE,EAAE,OAAO,qBAAqB,MAAM,YAAY,SAAS,iBAAS;AAAA,EAClE,EAAE,OAAO,kBAAkB,MAAM,kBAAkB,SAAS,uBAAc;AAAA,EAC1E,EAAE,OAAO,sBAAsB,MAAM,UAAU,SAAS,eAAO;AAAA,EAC/D,EAAE,OAAO,kBAAkB,MAAM,kBAAkB,SAAS,uBAAc;AAAA,EAC1E,EAAE,OAAO,uBAAuB,MAAM,cAAc,SAAS,mBAAU;AAAA,EACvE,EAAE,OAAO,uBAAuB,MAAM,kBAAkB,SAAS,uBAAc;AAAA,EAC/E,EAAE,OAAO,cAAc,MAAM,cAAc,SAAS,mBAAW;AAAA,EAC/D,EAAE,OAAO,iBAAiB,MAAM,iBAAiB,SAAS,sBAAa;AAAA,EACvE,EAAE,OAAO,sBAAsB,MAAM,sBAAsB,SAAS,2BAAkB;AACxF;AAEO,IAAM,8BAA8C;AAAA,EACzD,EAAE,OAAO,uBAAuB,MAAM,aAAa,SAAS,kBAAS;AAAA,EACrE,EAAE,OAAO,sBAAsB,MAAM,qBAAqB,SAAS,0BAAgB;AAAA,EACnF,EAAE,OAAO,uBAAuB,MAAM,kBAAkB,SAAS,uBAAc;AAAA,EAC/E,EAAE,OAAO,6BAA6B,MAAM,gBAAgB,SAAS,qBAAW;AAClF;AAEO,IAAM,0BAA0C;AAAA,EACrD,GAAG;AAAA,EACH,GAAG;AACL;;;AC1BO,IAAM,SAAS;AAAA,EACpB,UAAU;AAAA,EACV,SAAS;AAAA,EACT,cAAc;AAAA,EACd,eAAe;AAAA,EACf,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AACP;AAWO,SAAS,oBACd,UACA,YACgB;AAEhB,MAAI,CAAC,aAAa,UAAU,EAAG,OAAM,IAAI,MAAM,oBAAoB;AACnE,SAAO,SAAS,IAAI,CAAC,MAAM;AACzB,QAAI;AACF,aAAO,EAAE,GAAG,GAAG,SAAS,kBAAkB,EAAE,SAAS,UAAU,EAAE;AAAA,IACnE,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,kBAAkB,EAAE,IAAI,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACpF,EAAE,MAAM;AAAA,MACV;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AC5CA,IAAM,yBAAyB;AAG/B,SAAS,gBAAgB,OAAyC;AAChE,SACE,MAAM,QAAQ,KAAK,KACnB,MAAM,SAAS,KACf,MAAM;AAAA,IACJ,CAAC,MACC,OAAO,GAAG,UAAU,YAAY,OAAO,GAAG,SAAS,YAAY,OAAO,GAAG,YAAY;AAAA,EACzF;AAEJ;AAIA,eAAe,oBACb,QACA,QACA,SACiB;AACjB,MAAI;AACF,QAAI,MAAM,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,eAAe,MAAM,eAAe,mBAAmB,OAAO,CAAC;AACtG,QAAI,QAAS,QAAO,YAAY,mBAAmB,OAAO,CAAC;AAC3D,UAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,sBAAsB,EAAE,CAAC;AACpF,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,aAAa,QAAQ,IAAI,MAAM,GAAG;AACxD,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,CAAC,gBAAgB,KAAK,MAAM,QAAQ,EAAG,QAAO,EAAE,aAAa,sBAAsB;AAEvF,WAAO;AAAA,MACL,UAAU,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO;AAAA,QACvC,OAAO,uBAAuB,EAAE,KAAK;AAAA,QACrC,MAAM,uBAAuB,EAAE,IAAI;AAAA,QACnC,SAAS,uBAAuB,EAAE,OAAO;AAAA,MAC3C,EAAE;AAAA,IACJ;AAAA,EACF,SAAS,OAAO;AAEd,WAAO,EAAE,aAAa,iBAAiB,QAAQ,MAAM,OAAO,gBAAgB;AAAA,EAC9E;AACF;AAGA,eAAsB,iBAAiB,QAAqB,MAA+B;AACzF,QAAM,UAAU,mBAAmB,IAAI;AACvC,QAAM,SAAS,MAAM,oBAAoB,MAAM,cAAc,IAAI,GAAG,QAAQ,OAAO;AAEnF,UAAQ;AAAA,IACN,cAAc,SACV,6BAA6B,OAAO,IAAI,OAAO,MAC/C,uBAAuB,OAAO,IAAI,OAAO,8BAA8B,OAAO,WAAW;AAAA,EAC/F;AACA;AAAA,IACE,cAAc,SAAS,OAAO,WAAW,oBAAoB,OAAO,MAAM,GAAG,OAAO;AAAA,IACpF;AAAA,IACA,QAAQ,MAAM,QAAQ;AAAA,EACxB;AACF;AAGA,SAAS,YACP,aACA,SACA,MACM;AACN,QAAM,WAAW,UACb,YAAY,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,IAC5C;AAEJ,MAAI,WAAW,SAAS,WAAW,GAAG;AACpC,YAAQ;AAAA,MACN,2BAA2B,OAAO,iBAAiB,YAAY,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;AAAA,IAC9F;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,MAAM;AACR,YAAQ,IAAI,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAC7C;AAAA,EACF;AAEA,aAAW,KAAK,UAAU;AACxB,YAAQ,IAAI;AAAA,EAAK,IAAI,OAAO,EAAE,CAAC,EAAE;AACjC,YAAQ,IAAI,KAAK,EAAE,KAAK,EAAE;AAC1B,YAAQ,IAAI,GAAG,IAAI,OAAO,EAAE,CAAC;AAAA,CAAI;AACjC,YAAQ,IAAI,EAAE,OAAO;AACrB,YAAQ,IAAI,EAAE;AAAA,EAChB;AACA,UAAQ,IAAI,mBAAmB;AACjC;AAGA,IAAM,sBACJ;","names":["personas_default","personas_default","overview_default","overview_default"]}