@treeseed/sdk 0.10.28 → 0.12.4

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 (509) hide show
  1. package/README.md +213 -7
  2. package/dist/agent-capacity.d.ts +916 -0
  3. package/dist/agent-capacity.js +1600 -0
  4. package/dist/agent-tools.d.ts +37 -0
  5. package/dist/agent-tools.js +301 -0
  6. package/dist/api/app.d.ts +1 -1
  7. package/dist/api/auth/d1-database.d.ts +2 -2
  8. package/dist/api/auth/d1-provider.d.ts +4 -4
  9. package/dist/api/auth/d1-store.d.ts +2 -2
  10. package/dist/api/auth/memory-provider.d.ts +1 -1
  11. package/dist/api/auth/tokens.d.ts +1 -1
  12. package/dist/api/config.d.ts +3 -1
  13. package/dist/api/config.js +34 -10
  14. package/dist/api/http.d.ts +2 -2
  15. package/dist/api/index.d.ts +10 -10
  16. package/dist/api/index.js +4 -2
  17. package/dist/api/operations-routes.d.ts +3 -3
  18. package/dist/api/operations.d.ts +1 -1
  19. package/dist/api/providers.d.ts +1 -1
  20. package/dist/api/providers.js +21 -4
  21. package/dist/api/railway.d.ts +3 -2
  22. package/dist/api/sdk-dispatch.d.ts +4 -4
  23. package/dist/api/sdk-routes.d.ts +2 -2
  24. package/dist/api/templates.d.ts +2 -2
  25. package/dist/api/types.d.ts +3 -2
  26. package/dist/capacity-provider.d.ts +118 -59
  27. package/dist/capacity-provider.js +235 -43
  28. package/dist/capacity.d.ts +2 -2
  29. package/dist/cli-tools.d.ts +1 -1
  30. package/dist/content-operations.d.ts +97 -0
  31. package/dist/content-operations.js +250 -0
  32. package/dist/content-store.d.ts +4 -4
  33. package/dist/control-plane-client.d.ts +13 -18
  34. package/dist/control-plane-client.js +8 -86
  35. package/dist/control-plane.d.ts +3 -2
  36. package/dist/control-plane.js +38 -13
  37. package/dist/copilot.d.ts +3 -0
  38. package/dist/copilot.js +16 -1
  39. package/dist/d1-http.d.ts +1 -1
  40. package/dist/d1-store.d.ts +32 -57
  41. package/dist/d1-store.js +115 -318
  42. package/dist/db/d1.d.ts +1 -232
  43. package/dist/db/index.d.ts +3 -3
  44. package/dist/db/market-schema.d.ts +50900 -11018
  45. package/dist/db/market-schema.js +2313 -467
  46. package/dist/db/node-sqlite.d.ts +1 -232
  47. package/dist/db/node-sqlite.js +7 -2
  48. package/dist/db/schema.d.ts +0 -462
  49. package/dist/db/schema.js +2 -22
  50. package/dist/dispatch.d.ts +1 -1
  51. package/dist/dispatch.js +0 -11
  52. package/dist/fixture-support.js +52 -4
  53. package/dist/governance.d.ts +133 -0
  54. package/dist/governance.js +285 -0
  55. package/dist/graph/build.d.ts +2 -2
  56. package/dist/graph/context-query-contracts.d.ts +1 -1
  57. package/dist/graph/dsl.d.ts +1 -1
  58. package/dist/graph/query.d.ts +4 -4
  59. package/dist/graph/ranking.d.ts +1 -1
  60. package/dist/graph/schema.d.ts +1 -1
  61. package/dist/graph.d.ts +11 -11
  62. package/dist/guarantees/index.d.ts +425 -0
  63. package/dist/guarantees/index.js +1146 -0
  64. package/dist/hosting/apps.d.ts +12 -0
  65. package/dist/hosting/apps.js +107 -0
  66. package/dist/hosting/builtins.d.ts +25 -0
  67. package/dist/hosting/builtins.js +602 -0
  68. package/dist/hosting/contracts.d.ts +208 -0
  69. package/dist/hosting/contracts.js +0 -0
  70. package/dist/hosting/graph.d.ts +195 -0
  71. package/dist/hosting/graph.js +1092 -0
  72. package/dist/hosting/index.d.ts +4 -0
  73. package/dist/hosting/index.js +4 -0
  74. package/dist/index.d.ts +88 -67
  75. package/dist/index.js +142 -14
  76. package/dist/local-dev/managed-dev.d.ts +85 -0
  77. package/dist/local-dev/managed-dev.js +368 -0
  78. package/dist/managed-dependencies.d.ts +9 -0
  79. package/dist/managed-dependencies.js +10 -9
  80. package/dist/market-client.d.ts +327 -9
  81. package/dist/market-client.js +464 -20
  82. package/dist/model-registry.d.ts +1 -1
  83. package/dist/model-registry.js +19 -19
  84. package/dist/operations/agent-tools.d.ts +2 -11
  85. package/dist/operations/agent-tools.js +2 -14
  86. package/dist/operations/providers/default.d.ts +1 -1
  87. package/dist/operations/providers/default.js +20 -37
  88. package/dist/operations/repository-operations.d.ts +26 -2
  89. package/dist/operations/repository-operations.js +159 -1
  90. package/dist/operations/runtime.d.ts +3 -3
  91. package/dist/operations/services/bootstrap-runner.d.ts +4 -2
  92. package/dist/operations/services/bootstrap-runner.js +22 -2
  93. package/dist/operations/services/build-warning-policy.d.ts +34 -0
  94. package/dist/operations/services/build-warning-policy.js +81 -82
  95. package/dist/operations/services/commit-message-provider.js +4 -4
  96. package/dist/operations/services/config-runtime.d.ts +83 -24
  97. package/dist/operations/services/config-runtime.js +449 -130
  98. package/dist/operations/services/deploy.d.ts +106 -624
  99. package/dist/operations/services/deploy.js +746 -263
  100. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  101. package/dist/operations/services/deployment-readiness.js +206 -0
  102. package/dist/operations/services/git-remote-policy.js +12 -4
  103. package/dist/operations/services/git-runner.d.ts +66 -0
  104. package/dist/operations/services/git-runner.js +479 -0
  105. package/dist/operations/services/git-workflow.d.ts +45 -6
  106. package/dist/operations/services/git-workflow.js +251 -23
  107. package/dist/operations/services/github-actions-verification.d.ts +4 -1
  108. package/dist/operations/services/github-actions-verification.js +12 -3
  109. package/dist/operations/services/github-api.js +22 -6
  110. package/dist/operations/services/github-automation.d.ts +4 -4
  111. package/dist/operations/services/github-automation.js +19 -21
  112. package/dist/operations/services/github-credentials.d.ts +13 -0
  113. package/dist/operations/services/github-credentials.js +59 -0
  114. package/dist/operations/services/hosted-service-checks.d.ts +81 -0
  115. package/dist/operations/services/hosted-service-checks.js +478 -0
  116. package/dist/operations/services/hosting-audit.d.ts +2 -2
  117. package/dist/operations/services/hosting-audit.js +7 -7
  118. package/dist/operations/services/hub-launch.d.ts +1 -1
  119. package/dist/operations/services/hub-provider-launch.d.ts +5 -5
  120. package/dist/operations/services/hub-provider-launch.js +22 -17
  121. package/dist/operations/services/live-hosted-service-checks.d.ts +26 -0
  122. package/dist/operations/services/live-hosted-service-checks.js +463 -0
  123. package/dist/operations/services/local-dev.js +0 -1
  124. package/dist/operations/services/managed-host-security.d.ts +1 -1
  125. package/dist/operations/services/managed-host-security.js +1 -1
  126. package/dist/operations/services/managed-repositories.d.ts +37 -0
  127. package/dist/operations/services/managed-repositories.js +177 -0
  128. package/dist/operations/services/operations-runner-smoke.d.ts +31 -0
  129. package/dist/operations/services/operations-runner-smoke.js +198 -0
  130. package/dist/operations/services/package-adapters.d.ts +170 -0
  131. package/dist/operations/services/package-adapters.js +859 -0
  132. package/dist/operations/services/package-reference-policy.d.ts +13 -88
  133. package/dist/operations/services/package-reference-policy.js +96 -215
  134. package/dist/operations/services/project-host-operations.d.ts +4 -4
  135. package/dist/operations/services/project-platform.d.ts +57 -173
  136. package/dist/operations/services/project-platform.js +80 -111
  137. package/dist/operations/services/project-web-monitor.d.ts +2 -2
  138. package/dist/operations/services/project-web-monitor.js +122 -5
  139. package/dist/operations/services/railway-api.d.ts +117 -5
  140. package/dist/operations/services/railway-api.js +915 -93
  141. package/dist/operations/services/railway-deploy.d.ts +30 -112
  142. package/dist/operations/services/railway-deploy.js +521 -961
  143. package/dist/operations/services/release-history.js +3 -4
  144. package/dist/operations/services/release-proof-ledger.d.ts +13 -0
  145. package/dist/operations/services/release-proof-ledger.js +81 -0
  146. package/dist/operations/services/release-proof-planner.d.ts +48 -0
  147. package/dist/operations/services/release-proof-planner.js +195 -0
  148. package/dist/operations/services/release-proof-runner.d.ts +16 -0
  149. package/dist/operations/services/release-proof-runner.js +169 -0
  150. package/dist/operations/services/release-proof.d.ts +81 -0
  151. package/dist/operations/services/release-proof.js +79 -0
  152. package/dist/operations/services/repository-save-orchestrator.d.ts +8 -7
  153. package/dist/operations/services/repository-save-orchestrator.js +604 -212
  154. package/dist/operations/services/runtime-paths.d.ts +1 -3
  155. package/dist/operations/services/runtime-paths.js +0 -4
  156. package/dist/operations/services/runtime-tools.d.ts +1 -191
  157. package/dist/operations/services/runtime-tools.js +4 -443
  158. package/dist/operations/services/save-deploy-preflight.js +9 -1
  159. package/dist/operations/services/template-host-bindings.d.ts +2 -2
  160. package/dist/operations/services/template-host-bindings.js +3 -2
  161. package/dist/operations/services/template-registry.d.ts +6 -6
  162. package/dist/operations/services/template-registry.js +33 -14
  163. package/dist/operations/services/template-secret-sync.d.ts +3 -3
  164. package/dist/operations/services/verification-cache.d.ts +25 -0
  165. package/dist/operations/services/verification-cache.js +71 -0
  166. package/dist/operations/services/workflow-timing.d.ts +25 -0
  167. package/dist/operations/services/workflow-timing.js +79 -0
  168. package/dist/operations/services/workspace-dependency-mode.js +50 -5
  169. package/dist/operations/services/workspace-preflight.js +8 -8
  170. package/dist/operations/services/workspace-save-script.d.ts +4 -4
  171. package/dist/operations/services/workspace-save.d.ts +1 -0
  172. package/dist/operations/services/workspace-save.js +42 -19
  173. package/dist/operations/services/workspace-tools.js +42 -7
  174. package/dist/operations-registry.d.ts +1 -1
  175. package/dist/operations-registry.js +5 -6
  176. package/dist/operations-types.d.ts +1 -1
  177. package/dist/operations.d.ts +17 -11
  178. package/dist/operations.js +57 -3
  179. package/dist/platform/book-export.js +2 -0
  180. package/dist/platform/books-data.d.ts +1 -1
  181. package/dist/platform/config.d.ts +27 -0
  182. package/dist/platform/config.js +100 -0
  183. package/dist/platform/content-runtime-source.d.ts +54 -0
  184. package/dist/platform/content-runtime-source.js +169 -0
  185. package/dist/platform/contracts.d.ts +38 -2
  186. package/dist/platform/deploy-config.d.ts +1 -1
  187. package/dist/platform/deploy-config.js +80 -9
  188. package/dist/platform/deploy-runtime.d.ts +1 -1
  189. package/dist/platform/desired-state.d.ts +63 -0
  190. package/dist/platform/desired-state.js +1021 -0
  191. package/dist/platform/env.yaml +568 -45
  192. package/dist/platform/environment.d.ts +3 -2
  193. package/dist/platform/environment.js +122 -21
  194. package/dist/platform/local-content-materialization.d.ts +9 -0
  195. package/dist/platform/local-content-materialization.js +212 -0
  196. package/dist/platform/plugin.d.ts +13 -4
  197. package/dist/platform/plugins/runtime.d.ts +3 -3
  198. package/dist/platform/plugins/runtime.js +62 -1
  199. package/dist/platform/plugins.d.ts +6 -6
  200. package/dist/platform/published-content-pipeline.d.ts +3 -3
  201. package/dist/platform/published-content-pipeline.js +5 -1
  202. package/dist/platform/published-content.d.ts +4 -3
  203. package/dist/platform/site-config-schema.d.ts +1 -0
  204. package/dist/platform/site-config-schema.js +4 -1
  205. package/dist/platform/tenant/runtime-config.d.ts +152 -2
  206. package/dist/platform/tenant-config.d.ts +1 -1
  207. package/dist/platform/utils/site-config-schema.d.ts +154 -0
  208. package/dist/platform/utils/site-config-schema.js +400 -461
  209. package/dist/platform-operation-store.d.ts +1 -1
  210. package/dist/platform-operation-store.js +2 -2
  211. package/dist/platform-operations.js +1 -1
  212. package/dist/plugin-default.d.ts +1 -1
  213. package/dist/plugin-default.js +27 -12
  214. package/dist/project-import.d.ts +91 -0
  215. package/dist/project-import.js +257 -0
  216. package/dist/project-workflow.d.ts +1 -1
  217. package/dist/reconcile/bootstrap-systems.d.ts +2 -2
  218. package/dist/reconcile/bootstrap-systems.js +5 -5
  219. package/dist/reconcile/builtin-adapters.d.ts +7 -1
  220. package/dist/reconcile/builtin-adapters.js +3190 -429
  221. package/dist/reconcile/contracts.d.ts +23 -6
  222. package/dist/reconcile/desired-state.d.ts +2 -192
  223. package/dist/reconcile/desired-state.js +29 -38
  224. package/dist/reconcile/engine.d.ts +61 -395
  225. package/dist/reconcile/engine.js +229 -44
  226. package/dist/reconcile/index.d.ts +10 -8
  227. package/dist/reconcile/index.js +2 -0
  228. package/dist/reconcile/live-acceptance.d.ts +100 -0
  229. package/dist/reconcile/live-acceptance.js +2027 -0
  230. package/dist/reconcile/platform.d.ts +104 -0
  231. package/dist/reconcile/platform.js +100 -0
  232. package/dist/reconcile/providers/docker-private.d.ts +54 -0
  233. package/dist/reconcile/providers/docker-private.js +101 -0
  234. package/dist/reconcile/providers/github-private.d.ts +65 -0
  235. package/dist/reconcile/providers/github-private.js +119 -0
  236. package/dist/reconcile/providers/local-private.d.ts +28 -0
  237. package/dist/reconcile/providers/local-private.js +75 -0
  238. package/dist/reconcile/providers/railway-iac.d.ts +66 -0
  239. package/dist/reconcile/providers/railway-iac.js +275 -0
  240. package/dist/reconcile/providers/release-private.d.ts +92 -0
  241. package/dist/reconcile/providers/release-private.js +152 -0
  242. package/dist/reconcile/registry.d.ts +2 -2
  243. package/dist/reconcile/registry.js +16 -1
  244. package/dist/reconcile/state.d.ts +1 -1
  245. package/dist/reconcile/state.js +7 -44
  246. package/dist/reconcile/units.d.ts +1 -1
  247. package/dist/reconcile/units.js +9 -3
  248. package/dist/remote.d.ts +6 -6
  249. package/dist/scenes/artifacts.d.ts +16 -0
  250. package/dist/scenes/artifacts.js +61 -0
  251. package/dist/scenes/auth.d.ts +2 -0
  252. package/dist/scenes/auth.js +28 -0
  253. package/dist/scenes/base-url.d.ts +12 -0
  254. package/dist/scenes/base-url.js +123 -0
  255. package/dist/scenes/builtin-plugins.d.ts +2 -0
  256. package/dist/scenes/builtin-plugins.js +447 -0
  257. package/dist/scenes/checkpoints.d.ts +14 -0
  258. package/dist/scenes/checkpoints.js +33 -0
  259. package/dist/scenes/device-matrix.d.ts +2 -0
  260. package/dist/scenes/device-matrix.js +97 -0
  261. package/dist/scenes/devices.d.ts +11 -0
  262. package/dist/scenes/devices.js +22 -0
  263. package/dist/scenes/diagnostics.d.ts +5 -0
  264. package/dist/scenes/diagnostics.js +22 -0
  265. package/dist/scenes/diagram-providers.d.ts +2 -0
  266. package/dist/scenes/diagram-providers.js +181 -0
  267. package/dist/scenes/diagram-validation.d.ts +22 -0
  268. package/dist/scenes/diagram-validation.js +74 -0
  269. package/dist/scenes/environment.d.ts +2 -0
  270. package/dist/scenes/environment.js +65 -0
  271. package/dist/scenes/evidence-bundle.d.ts +5 -0
  272. package/dist/scenes/evidence-bundle.js +42 -0
  273. package/dist/scenes/evidence-report.d.ts +10 -0
  274. package/dist/scenes/evidence-report.js +73 -0
  275. package/dist/scenes/evidence.d.ts +16 -0
  276. package/dist/scenes/evidence.js +277 -0
  277. package/dist/scenes/index.d.ts +48 -0
  278. package/dist/scenes/index.js +312 -0
  279. package/dist/scenes/inspect.d.ts +9 -0
  280. package/dist/scenes/inspect.js +64 -0
  281. package/dist/scenes/loader.d.ts +3 -0
  282. package/dist/scenes/loader.js +38 -0
  283. package/dist/scenes/logs.d.ts +2 -0
  284. package/dist/scenes/logs.js +47 -0
  285. package/dist/scenes/operations.d.ts +3 -0
  286. package/dist/scenes/operations.js +128 -0
  287. package/dist/scenes/phase0.d.ts +9 -0
  288. package/dist/scenes/phase0.js +637 -0
  289. package/dist/scenes/planner.d.ts +12 -0
  290. package/dist/scenes/planner.js +166 -0
  291. package/dist/scenes/playwright-adapter.d.ts +2 -0
  292. package/dist/scenes/playwright-adapter.js +63 -0
  293. package/dist/scenes/plugins.d.ts +5 -0
  294. package/dist/scenes/plugins.js +115 -0
  295. package/dist/scenes/progress.d.ts +17 -0
  296. package/dist/scenes/progress.js +24 -0
  297. package/dist/scenes/publish-destinations.d.ts +12 -0
  298. package/dist/scenes/publish-destinations.js +99 -0
  299. package/dist/scenes/publish-plan-report.d.ts +10 -0
  300. package/dist/scenes/publish-plan-report.js +70 -0
  301. package/dist/scenes/publish-plan.d.ts +15 -0
  302. package/dist/scenes/publish-plan.js +248 -0
  303. package/dist/scenes/publish-redaction.d.ts +16 -0
  304. package/dist/scenes/publish-redaction.js +148 -0
  305. package/dist/scenes/publish-release-record.d.ts +13 -0
  306. package/dist/scenes/publish-release-record.js +34 -0
  307. package/dist/scenes/publish-report.d.ts +10 -0
  308. package/dist/scenes/publish-report.js +67 -0
  309. package/dist/scenes/publish.d.ts +16 -0
  310. package/dist/scenes/publish.js +298 -0
  311. package/dist/scenes/registry.d.ts +12 -0
  312. package/dist/scenes/registry.js +65 -0
  313. package/dist/scenes/remotion-adapter.d.ts +3 -0
  314. package/dist/scenes/remotion-adapter.js +94 -0
  315. package/dist/scenes/remotion-composition-registry.d.ts +2 -0
  316. package/dist/scenes/remotion-composition-registry.js +12 -0
  317. package/dist/scenes/remotion-compositions.d.ts +15 -0
  318. package/dist/scenes/remotion-compositions.js +464 -0
  319. package/dist/scenes/remotion-diagrams.d.ts +9 -0
  320. package/dist/scenes/remotion-diagrams.js +188 -0
  321. package/dist/scenes/remotion-input.d.ts +13 -0
  322. package/dist/scenes/remotion-input.js +291 -0
  323. package/dist/scenes/remotion-visuals.d.ts +29 -0
  324. package/dist/scenes/remotion-visuals.js +198 -0
  325. package/dist/scenes/render-media-assets.d.ts +9 -0
  326. package/dist/scenes/render-media-assets.js +117 -0
  327. package/dist/scenes/render-report.d.ts +10 -0
  328. package/dist/scenes/render-report.js +27 -0
  329. package/dist/scenes/render.d.ts +2 -0
  330. package/dist/scenes/render.js +218 -0
  331. package/dist/scenes/reporter.d.ts +3 -0
  332. package/dist/scenes/reporter.js +101 -0
  333. package/dist/scenes/resume.d.ts +2 -0
  334. package/dist/scenes/resume.js +79 -0
  335. package/dist/scenes/runner.d.ts +2 -0
  336. package/dist/scenes/runner.js +674 -0
  337. package/dist/scenes/schema.d.ts +6 -0
  338. package/dist/scenes/schema.js +994 -0
  339. package/dist/scenes/seed.d.ts +2 -0
  340. package/dist/scenes/seed.js +83 -0
  341. package/dist/scenes/segments.d.ts +15 -0
  342. package/dist/scenes/segments.js +79 -0
  343. package/dist/scenes/selectors.d.ts +3 -0
  344. package/dist/scenes/selectors.js +21 -0
  345. package/dist/scenes/timeline.d.ts +11 -0
  346. package/dist/scenes/timeline.js +26 -0
  347. package/dist/scenes/timeouts.d.ts +6 -0
  348. package/dist/scenes/timeouts.js +17 -0
  349. package/dist/scenes/training-captions.d.ts +14 -0
  350. package/dist/scenes/training-captions.js +98 -0
  351. package/dist/scenes/training-report.d.ts +10 -0
  352. package/dist/scenes/training-report.js +78 -0
  353. package/dist/scenes/training-transcript.d.ts +24 -0
  354. package/dist/scenes/training-transcript.js +204 -0
  355. package/dist/scenes/training.d.ts +10 -0
  356. package/dist/scenes/training.js +134 -0
  357. package/dist/scenes/types.d.ts +1872 -0
  358. package/dist/scenes/types.js +8 -0
  359. package/dist/scenes/visual-audit-contact-sheets.d.ts +6 -0
  360. package/dist/scenes/visual-audit-contact-sheets.js +119 -0
  361. package/dist/scenes/visual-audit-fixtures.d.ts +25 -0
  362. package/dist/scenes/visual-audit-fixtures.js +210 -0
  363. package/dist/scenes/visual-audit-report.d.ts +11 -0
  364. package/dist/scenes/visual-audit-report.js +85 -0
  365. package/dist/scenes/visual-audit-review.d.ts +23 -0
  366. package/dist/scenes/visual-audit-review.js +829 -0
  367. package/dist/scenes/visual-audit-routes.d.ts +13 -0
  368. package/dist/scenes/visual-audit-routes.js +308 -0
  369. package/dist/scenes/visual-audit.d.ts +3 -0
  370. package/dist/scenes/visual-audit.js +596 -0
  371. package/dist/scripts/build-dist.js +154 -40
  372. package/dist/scripts/check-build-warnings.js +42 -53
  373. package/dist/scripts/cleanup-markdown.js +1 -1
  374. package/dist/scripts/cli-test-fixtures.js +24 -28
  375. package/dist/scripts/deployment-readiness.js +20 -0
  376. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  377. package/dist/scripts/migrate-journey-csv-to-guarantees.js +140 -0
  378. package/dist/scripts/operations-runner-smoke.js +16 -0
  379. package/dist/scripts/package-tools.js +12 -1
  380. package/dist/scripts/prepare.js +3 -7
  381. package/dist/scripts/release-verify.js +6 -6
  382. package/dist/scripts/run-fixture-astro-command.js +1 -1
  383. package/dist/scripts/template-catalog.test.js +68 -82
  384. package/dist/scripts/tenant-dev.js +0 -10
  385. package/dist/scripts/tenant-workflow-action.js +10 -1
  386. package/dist/scripts/test-cloudflare-local.js +1 -1
  387. package/dist/scripts/test-scaffold.js +0 -1
  388. package/dist/scripts/test-smoke.js +3 -3
  389. package/dist/scripts/treeseed-build-dist.js +4 -7
  390. package/dist/scripts/treeseed-release-verify.js +1 -1
  391. package/dist/scripts/verify-driver.js +30 -21
  392. package/dist/scripts/workflow-commands.test.js +18 -27
  393. package/dist/scripts/workspace-command-e2e.js +1 -1
  394. package/dist/scripts/workspace-lint.js +6 -0
  395. package/dist/scripts/workspace-release-test.js +90 -1
  396. package/dist/scripts/workspace-test.js +6 -0
  397. package/dist/sdk-dispatch.d.ts +1 -1
  398. package/dist/sdk-dispatch.js +0 -16
  399. package/dist/sdk-fields.d.ts +1 -1
  400. package/dist/sdk-filters.d.ts +1 -1
  401. package/dist/sdk-types.d.ts +1352 -116
  402. package/dist/sdk-types.js +413 -2
  403. package/dist/sdk.d.ts +106 -100
  404. package/dist/sdk.js +211 -51
  405. package/dist/secrets-capability.d.ts +421 -0
  406. package/dist/secrets-capability.js +530 -0
  407. package/dist/seeds/normalize.js +1 -0
  408. package/dist/seeds/planner.js +38 -0
  409. package/dist/seeds/schema.js +293 -4
  410. package/dist/seeds/types.d.ts +60 -0
  411. package/dist/seeds/types.js +9 -1
  412. package/dist/service-credentials.d.ts +54 -0
  413. package/dist/service-credentials.js +112 -0
  414. package/dist/stores/cursor-store.d.ts +2 -2
  415. package/dist/stores/envelopes.d.ts +1 -1
  416. package/dist/stores/helpers.d.ts +1 -1
  417. package/dist/stores/lease-store.d.ts +2 -2
  418. package/dist/stores/message-store.d.ts +2 -2
  419. package/dist/stores/operational-store.d.ts +3 -26
  420. package/dist/stores/operational-store.js +0 -225
  421. package/dist/stores/project-workflow-store.d.ts +2 -2
  422. package/dist/stores/run-store.d.ts +2 -3
  423. package/dist/stores/run-store.js +0 -98
  424. package/dist/stores/subscription-store.d.ts +2 -3
  425. package/dist/stores/subscription-store.js +0 -100
  426. package/dist/template-catalog.d.ts +1 -1
  427. package/dist/template-launch-requirements.d.ts +2 -2
  428. package/dist/template-launch-requirements.js +9 -0
  429. package/dist/template-launch-ui.d.ts +2 -2
  430. package/dist/treedx/adapters.d.ts +6 -0
  431. package/dist/treedx/adapters.js +36 -0
  432. package/dist/treedx/client.d.ts +222 -0
  433. package/dist/treedx/client.js +871 -0
  434. package/dist/treedx/errors.d.ts +13 -0
  435. package/dist/treedx/errors.js +17 -0
  436. package/dist/treedx/federated-client.d.ts +27 -0
  437. package/dist/treedx/federated-client.js +158 -0
  438. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  439. package/dist/treedx/generated/openapi-types.js +0 -0
  440. package/dist/treedx/graph-adapter.d.ts +33 -0
  441. package/dist/treedx/graph-adapter.js +156 -0
  442. package/dist/treedx/index.d.ts +14 -0
  443. package/dist/treedx/index.js +48 -0
  444. package/dist/treedx/market-integration.d.ts +27 -0
  445. package/dist/treedx/market-integration.js +131 -0
  446. package/dist/treedx/ports.d.ts +166 -0
  447. package/dist/treedx/ports.js +231 -0
  448. package/dist/treedx/query-adapter.d.ts +19 -0
  449. package/dist/treedx/query-adapter.js +62 -0
  450. package/dist/treedx/registry-client.d.ts +11 -0
  451. package/dist/treedx/registry-client.js +19 -0
  452. package/dist/treedx/repository-adapter.d.ts +45 -0
  453. package/dist/treedx/repository-adapter.js +319 -0
  454. package/dist/treedx/sdk-integration.d.ts +27 -0
  455. package/dist/treedx/sdk-integration.js +63 -0
  456. package/dist/treedx/types.d.ts +1084 -0
  457. package/dist/treedx/types.js +8 -0
  458. package/dist/treedx/workspace-adapter.d.ts +27 -0
  459. package/dist/treedx/workspace-adapter.js +65 -0
  460. package/dist/treedx-backends.d.ts +220 -0
  461. package/dist/treedx-backends.js +634 -0
  462. package/dist/treedx-client.d.ts +89 -0
  463. package/dist/treedx-client.js +184 -0
  464. package/dist/treeseed/services/compose.yml +2 -3
  465. package/dist/treeseed/template-catalog/catalog.fixture.json +27 -27
  466. package/dist/types/agents.d.ts +256 -0
  467. package/dist/types/agents.js +42 -1
  468. package/dist/verification.d.ts +1 -0
  469. package/dist/verification.js +143 -14
  470. package/dist/workflow/operations.d.ts +580 -523
  471. package/dist/workflow/operations.js +2084 -1279
  472. package/dist/workflow/policy.js +19 -7
  473. package/dist/workflow/runs.d.ts +9 -3
  474. package/dist/workflow/runs.js +66 -26
  475. package/dist/workflow/session.d.ts +4 -1
  476. package/dist/workflow/session.js +50 -10
  477. package/dist/workflow/worktrees.d.ts +6 -2
  478. package/dist/workflow/worktrees.js +98 -15
  479. package/dist/workflow-state.d.ts +17 -4
  480. package/dist/workflow-state.js +68 -41
  481. package/dist/workflow-support.d.ts +26 -17
  482. package/dist/workflow-support.js +84 -24
  483. package/dist/workflow.d.ts +51 -16
  484. package/dist/workflow.js +19 -7
  485. package/drizzle/d1/0000_treeseed_d1.sql +0 -18
  486. package/drizzle/market/0000_market_control_plane.sql +1793 -1499
  487. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  488. package/drizzle/market/0005_agent_capacity_coordination.sql +145 -0
  489. package/drizzle/market/0006_provider_assignment_lifecycle.sql +9 -0
  490. package/drizzle/market/0007_agent_architecture_gap_closure.sql +157 -0
  491. package/drizzle/market/0008_agent_capacity_operational_closure.sql +27 -0
  492. package/drizzle/market/0009_agent_capacity_runtime_hardening.sql +29 -0
  493. package/drizzle/market/0010_secrets_capability_persistence.sql +130 -0
  494. package/drizzle/market/0011_github_app_adapter.sql +47 -0
  495. package/drizzle/market/0012_workday_test_runs.sql +44 -0
  496. package/drizzle/market/0013_runtime_task_lifecycle.sql +39 -0
  497. package/drizzle/market/0014_governance_pipeline.sql +189 -0
  498. package/package.json +106 -18
  499. package/scripts/{verify-driver.mjs → verify-driver.ts} +19 -4
  500. package/templates/github/deploy-web.workflow.yml +49 -12
  501. package/dist/operations/services/mailpit-runtime.d.ts +0 -20
  502. package/dist/operations/services/mailpit-runtime.js +0 -67
  503. package/dist/operations/services/release-candidate.d.ts +0 -41
  504. package/dist/operations/services/release-candidate.js +0 -616
  505. package/dist/scripts/ensure-mailpit.js +0 -48
  506. package/dist/scripts/logs-mailpit.js +0 -2
  507. package/dist/scripts/run-ts.js +0 -45
  508. package/dist/scripts/stop-mailpit.js +0 -5
  509. package/dist/scripts/treeseed-run-ts.js +0 -45
@@ -0,0 +1,2027 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createServer } from "node:net";
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { readFileSync } from "node:fs";
5
+ import { performance } from "node:perf_hooks";
6
+ import { join, resolve } from "node:path";
7
+ import {
8
+ createTreeseedCanonicalReconcileReport
9
+ } from "./platform.js";
10
+ import { runTreeseedGit } from "../operations/services/git-runner.js";
11
+ import {
12
+ deleteRailwayProject,
13
+ ensureRailwayEnvironment,
14
+ ensureRailwayGeneratedServiceDomain,
15
+ ensureRailwayPostgresService,
16
+ ensureRailwayProject,
17
+ ensureRailwayService,
18
+ ensureRailwayServiceVolume,
19
+ listRailwayEnvironments,
20
+ listRailwayProjects,
21
+ listRailwayServices,
22
+ listRailwayVariables,
23
+ listRailwayVolumes,
24
+ railwayGraphqlRequest,
25
+ resolveRailwayWorkspaceContext,
26
+ upsertRailwayVariables
27
+ } from "../operations/services/railway-api.js";
28
+ import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
29
+ import { MarketClient } from "../market-client.js";
30
+ import { MarketProviderClient } from "../capacity-provider.js";
31
+ const PROVIDER_CAPABILITIES = {
32
+ railway: ["project", "environment", "service", "image-service", "postgres", "volume", "domain", "variables", "deployment-health", "capacity-provider-runtime-assignment-proof"],
33
+ cloudflare: ["pages", "worker", "d1", "r2", "kv", "queue", "dns", "turnstile", "secrets", "cache-rules"],
34
+ github: ["environment", "secret", "variable", "workflow-dispatch", "workflow-observation", "repository-scoped-token"],
35
+ local: ["process", "port", "local-db", "local-runner", "docker-compose-capacity-provider", "capacity-provider-assignment-proof"]
36
+ };
37
+ function isCapacityRuntimeProofCapability(capability) {
38
+ return capability === "capacity-provider-assignment-proof" || capability === "capacity-provider-runtime-assignment-proof";
39
+ }
40
+ function scenarioResourceKind(provider, capability) {
41
+ if (provider === "railway") return capability === "volume" ? "railway-volume" : "railway-service";
42
+ if (provider === "cloudflare") return "cloudflare-resource";
43
+ if (provider === "github") {
44
+ if (capability === "environment") return "github-environment";
45
+ if (capability === "secret") return "github-secret-binding";
46
+ if (capability === "variable") return "github-secret-binding";
47
+ return "package-workflow";
48
+ }
49
+ if (isCapacityRuntimeProofCapability(capability)) return "capacity-provider";
50
+ if (capability === "docker-compose-capacity-provider") return "local-docker-compose";
51
+ if (capability === "process") return "local-process";
52
+ if (capability === "local-runner") return "capacity-provider";
53
+ return "local-process";
54
+ }
55
+ function scenarioResourceProvider(provider, capability) {
56
+ if (provider === "cloudflare") return "cloudflare";
57
+ if (provider === "github") return "github";
58
+ if (provider === "local") return "local";
59
+ return "railway";
60
+ }
61
+ function liveAcceptanceDesiredResource(input) {
62
+ const id = `live-acceptance:${input.environment}:${input.provider}:${input.capability}:${input.runId}`;
63
+ const kind = scenarioResourceKind(input.provider, input.capability);
64
+ return {
65
+ id,
66
+ kind,
67
+ provider: scenarioResourceProvider(input.provider, input.capability),
68
+ environment: input.environment,
69
+ packageId: null,
70
+ serviceId: input.capability,
71
+ logicalName: input.capability,
72
+ spec: {
73
+ liveAcceptance: true,
74
+ provider: input.provider,
75
+ capability: input.capability,
76
+ runId: input.runId,
77
+ prefix: providerPrefix(input.environment, input.provider, input.runId),
78
+ tenantRoot: input.tenantRoot
79
+ },
80
+ dependencies: [],
81
+ source: { type: "package-adapter", id }
82
+ };
83
+ }
84
+ function compileTreeseedLiveAcceptanceScenarios(input) {
85
+ const providers = input.provider === "all" ? ["railway", "cloudflare", "github", "local"] : [input.provider];
86
+ return providers.flatMap((provider) => PROVIDER_CAPABILITIES[provider].map((capability) => {
87
+ const probeOnly = input.mode === "smoke" || provider === "github" && ["workflow-observation", "repository-scoped-token"].includes(capability) || provider === "cloudflare" && capability === "cache-rules" || isCapacityRuntimeProofCapability(capability);
88
+ const desiredResources = probeOnly ? [] : [liveAcceptanceDesiredResource({
89
+ tenantRoot: input.tenantRoot,
90
+ environment: input.environment,
91
+ provider,
92
+ capability,
93
+ runId: input.runId
94
+ })];
95
+ const selector = {
96
+ environment: input.environment,
97
+ host: [provider],
98
+ resourceKind: desiredResources.map((resource) => resource.kind),
99
+ serviceType: [capability]
100
+ };
101
+ return {
102
+ id: `live-acceptance:${input.environment}:${provider}:${capability}:${input.runId}`,
103
+ provider,
104
+ capability,
105
+ desiredResources,
106
+ selector,
107
+ expectedActions: input.mode === "cleanup" ? ["delete", "noop"] : probeOnly ? ["noop"] : ["create", "update", "replace", "reattach", "noop"],
108
+ cleanupSelector: selector,
109
+ required: true,
110
+ probeOnly,
111
+ cleanupRequired: input.mode !== "smoke" && !probeOnly
112
+ };
113
+ }));
114
+ }
115
+ function configuredValue(env, keys) {
116
+ for (const key of keys) {
117
+ const value = env[key];
118
+ if (typeof value === "string" && value.trim()) return value.trim();
119
+ }
120
+ return "";
121
+ }
122
+ function capacityAcceptanceConfig(env, environment) {
123
+ const local = environment === "local";
124
+ const apiUrl = configuredValue(env, [
125
+ "TREESEED_CAPACITY_ACCEPTANCE_API_URL",
126
+ "TREESEED_MARKET_URL",
127
+ "TREESEED_MANAGEMENT_API_URL",
128
+ "TREESEED_API_BASE_URL"
129
+ ]) || (local ? "http://127.0.0.1:3000" : "");
130
+ const adminToken = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN"]) || (local ? "tsk_local_treeseed_acceptance_admin" : "");
131
+ const teamId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID"]) || (local ? "d8a613c2-bbdb-4474-9c96-31e985beafd4" : "");
132
+ const projectId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID"]) || (local ? "90764af2-5a13-42b9-a2fa-fb3af5882323" : "");
133
+ const providerId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID", "TREESEED_CAPACITY_PROVIDER_ID"]) || (local ? "ff48ce97-6959-46b5-be9f-9b7062161fe3" : "");
134
+ const agentClassId = configuredValue(env, ["TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID"]) || (local ? "planning" : "");
135
+ const providerApiKey = configuredValue(env, ["TREESEED_CAPACITY_PROVIDER_API_KEY"]) || (local ? "tsp_local_treeseed_demo_capacity_provider" : "");
136
+ const missing = [
137
+ ["TREESEED_CAPACITY_ACCEPTANCE_API_URL or TREESEED_MARKET_URL", apiUrl],
138
+ ["TREESEED_CAPACITY_ACCEPTANCE_ADMIN_TOKEN", adminToken],
139
+ ["TREESEED_CAPACITY_ACCEPTANCE_TEAM_ID", teamId],
140
+ ["TREESEED_CAPACITY_ACCEPTANCE_PROJECT_ID", projectId],
141
+ ["TREESEED_CAPACITY_ACCEPTANCE_PROVIDER_ID or TREESEED_CAPACITY_PROVIDER_ID", providerId],
142
+ ["TREESEED_CAPACITY_ACCEPTANCE_AGENT_CLASS_ID", agentClassId],
143
+ ["TREESEED_CAPACITY_PROVIDER_API_KEY", providerApiKey]
144
+ ].filter(([, value]) => !value).map(([key]) => String(key));
145
+ return {
146
+ apiUrl,
147
+ adminToken,
148
+ teamId,
149
+ projectId,
150
+ providerId,
151
+ agentClassId,
152
+ providerApiKey,
153
+ missing
154
+ };
155
+ }
156
+ function capacityGrantForAcceptance(grants, config, environment) {
157
+ return grants.map((entry) => entry && typeof entry === "object" ? entry : null).find(
158
+ (grant) => grant && grant.id && grant.capacityProviderId === config.providerId && grant.projectId === config.projectId && (grant.teamId === config.teamId || !grant.teamId) && (grant.state === "active" || !grant.state) && (grant.environment === environment || !grant.environment)
159
+ ) ?? null;
160
+ }
161
+ async function createTreeDxProxyAuditEvidence(input) {
162
+ const response = await input.fetchImpl(
163
+ `${input.apiUrl.replace(/\/$/u, "")}/v1/dx/projects/${encodeURIComponent(input.projectId)}/repos/${encodeURIComponent(`capacity-proof-${input.runId}`)}/files/read`,
164
+ {
165
+ method: "POST",
166
+ headers: {
167
+ accept: "application/json",
168
+ authorization: `Bearer ${input.providerApiKey}`,
169
+ "content-type": "application/json",
170
+ "x-treeseed-assignment-id": input.assignmentId
171
+ },
172
+ body: JSON.stringify({
173
+ paths: [`src/content/agent-platform-proof/${input.runId}.mdx`],
174
+ ref: "HEAD"
175
+ })
176
+ }
177
+ );
178
+ if (response.status !== 403) {
179
+ const text = await response.text().catch(() => "");
180
+ throw new Error(`Capacity acceptance TreeDX proxy audit probe expected 403 but received ${response.status}${text ? `: ${text}` : ""}.`);
181
+ }
182
+ }
183
+ async function runCapacityProviderAssignmentProof(input) {
184
+ const config = capacityAcceptanceConfig(input.env, input.environment);
185
+ if (config.missing.length > 0) {
186
+ throw new Error(`Missing capacity acceptance configuration: ${config.missing.join(", ")}.`);
187
+ }
188
+ const runnerId = `${input.prefix}-runner`;
189
+ const assignmentId = `${input.prefix}-assignment`;
190
+ const mode = configuredValue(input.env, ["TREESEED_CAPACITY_ACCEPTANCE_MODE"]) || "planning";
191
+ const metadata = {
192
+ liveAcceptance: true,
193
+ runId: input.runId,
194
+ prefix: input.prefix,
195
+ provider: input.provider,
196
+ capability: input.provider === "railway" ? "capacity-provider-runtime-assignment-proof" : "capacity-provider-assignment-proof",
197
+ priority: 1e6
198
+ };
199
+ const adminClient = new MarketClient({
200
+ profile: {
201
+ id: "capacity-acceptance",
202
+ label: "Capacity Acceptance",
203
+ baseUrl: config.apiUrl,
204
+ kind: "specialized",
205
+ teamId: config.teamId
206
+ },
207
+ accessToken: config.adminToken,
208
+ fetchImpl: input.fetchImpl,
209
+ userAgent: `treeseed-live-acceptance/${input.runId}`
210
+ });
211
+ const providerClient = new MarketProviderClient({
212
+ marketUrl: config.apiUrl,
213
+ marketId: input.environment,
214
+ apiKey: config.providerApiKey,
215
+ fetchImpl: input.fetchImpl,
216
+ userAgent: `treeseed-live-acceptance/${input.runId}`
217
+ });
218
+ const existingClasses = await adminClient.projectAgentClasses(config.projectId).catch(() => ({ payload: [] }));
219
+ const hasAgentClass = Array.isArray(existingClasses.payload) && existingClasses.payload.some((entry) => {
220
+ const record = entry && typeof entry === "object" ? entry : {};
221
+ return record.id === config.agentClassId || record.slug === config.agentClassId;
222
+ });
223
+ if (!hasAgentClass) {
224
+ await adminClient.createProjectAgentClass(config.projectId, {
225
+ id: config.agentClassId,
226
+ slug: config.agentClassId,
227
+ name: "Planning",
228
+ status: "active",
229
+ allowedModes: ["planning"],
230
+ requiredCapabilities: ["agent_mode_run"],
231
+ metadata
232
+ });
233
+ }
234
+ const grants = await adminClient.capacityGrants(config.teamId).catch(() => ({ payload: [] }));
235
+ const acceptanceGrant = capacityGrantForAcceptance(Array.isArray(grants.payload) ? grants.payload : [], config, input.environment);
236
+ if (!acceptanceGrant) {
237
+ throw new Error(`Capacity acceptance did not find an active checked-in grant for project ${config.projectId} and provider ${config.providerId}.`);
238
+ }
239
+ const reservation = await adminClient.createCapacityReservation(config.projectId, {
240
+ id: `${input.prefix}-reservation`,
241
+ capacityProviderId: config.providerId,
242
+ laneId: typeof acceptanceGrant.laneId === "string" && acceptanceGrant.laneId ? acceptanceGrant.laneId : `${config.providerId}:agent-capacity`,
243
+ reservedCredits: 1,
244
+ nativeUnit: "request",
245
+ reservedNativeAmount: 1,
246
+ mode,
247
+ metadata
248
+ });
249
+ const reservationId = String(reservation.payload?.id ?? "");
250
+ if (!reservationId) throw new Error("Capacity acceptance reservation creation did not return an id.");
251
+ const checkIn = await providerClient.checkIn({
252
+ id: `${input.prefix}-session`,
253
+ environment: input.environment,
254
+ status: "open",
255
+ capabilities: ["repo_read", "agent_mode_run", "usage_report"],
256
+ grants: [{
257
+ grantId: String(acceptanceGrant.id),
258
+ projectId: config.projectId,
259
+ teamId: config.teamId,
260
+ grantScope: typeof acceptanceGrant.grantScope === "string" ? acceptanceGrant.grantScope : "project"
261
+ }],
262
+ nativeLimits: { wallMinutes: { session: 30 } },
263
+ runnerPressure: { activeRunners: 0, maxConcurrentRunners: 1 },
264
+ constraints: { liveAcceptance: true, outboundOnly: true },
265
+ metadata
266
+ });
267
+ const sessionId = String(checkIn.payload.id ?? "");
268
+ if (!sessionId) throw new Error("Capacity acceptance check-in did not return a session id.");
269
+ await adminClient.createProviderAssignment(config.teamId, {
270
+ id: assignmentId,
271
+ projectId: config.projectId,
272
+ capacityProviderId: config.providerId,
273
+ providerSessionId: sessionId,
274
+ projectAgentClassId: config.agentClassId,
275
+ reservationId,
276
+ mode,
277
+ capacityEnvelope: {
278
+ teamId: config.teamId,
279
+ projectId: config.projectId,
280
+ providerId: config.providerId,
281
+ laneId: typeof acceptanceGrant.laneId === "string" && acceptanceGrant.laneId ? acceptanceGrant.laneId : `${config.providerId}:agent-capacity`,
282
+ reservationId,
283
+ mode,
284
+ limits: { wallMinutes: 5 },
285
+ metadata
286
+ },
287
+ decisionInput: {
288
+ kind: "capacity_acceptance_diagnostic",
289
+ runId: input.runId,
290
+ mode,
291
+ instructions: "Execute the isolated capacity acceptance diagnostic without widening project or provider scope."
292
+ },
293
+ workspaceContext: {
294
+ liveAcceptance: true,
295
+ runId: input.runId
296
+ },
297
+ metadata
298
+ });
299
+ let leased = null;
300
+ for (let attempt = 0; attempt < 10; attempt += 1) {
301
+ leased = await providerClient.nextAssignment({
302
+ sessionId,
303
+ runnerId,
304
+ leaseSeconds: 120,
305
+ metadata
306
+ });
307
+ const leasedId = leased.payload?.id ? String(leased.payload.id) : "";
308
+ if (!leasedId) throw new Error("Capacity acceptance did not lease an assignment.");
309
+ if (leasedId === assignmentId) break;
310
+ const leasedMetadata = leased.payload?.metadata && typeof leased.payload.metadata === "object" ? leased.payload.metadata : {};
311
+ if (leasedMetadata.liveAcceptance !== true) {
312
+ throw new Error(`Capacity acceptance leased ${leasedId} instead of diagnostic assignment ${assignmentId}.`);
313
+ }
314
+ const staleLeaseToken = leased.leaseToken ?? leased.payload.leaseToken ?? null;
315
+ if (!staleLeaseToken) throw new Error(`Capacity acceptance leased stale diagnostic ${leasedId} without a lease token.`);
316
+ await providerClient.completeAssignment(leasedId, {
317
+ runnerId,
318
+ leaseToken: staleLeaseToken,
319
+ output: { summary: "Retired stale capacity acceptance diagnostic.", runId: input.runId },
320
+ summary: { runId: input.runId, retiredAssignmentId: leasedId },
321
+ metadata: {
322
+ ...metadata,
323
+ retiredStaleAcceptanceAssignment: true,
324
+ retiredAssignmentId: leasedId
325
+ }
326
+ });
327
+ leased = null;
328
+ }
329
+ if (!leased?.payload?.id) throw new Error("Capacity acceptance did not lease an assignment.");
330
+ if (leased.payload.id !== assignmentId) {
331
+ throw new Error(`Capacity acceptance leased stale diagnostics but did not reach diagnostic assignment ${assignmentId}.`);
332
+ }
333
+ const leaseToken = leased.leaseToken ?? leased.payload.leaseToken ?? null;
334
+ if (!leaseToken) throw new Error("Capacity acceptance lease did not include a lease token.");
335
+ await createTreeDxProxyAuditEvidence({
336
+ fetchImpl: input.fetchImpl,
337
+ apiUrl: config.apiUrl,
338
+ providerApiKey: config.providerApiKey,
339
+ projectId: config.projectId,
340
+ assignmentId,
341
+ runId: input.runId
342
+ });
343
+ const modeRun = await providerClient.createAssignmentModeRun(assignmentId, {
344
+ status: "succeeded",
345
+ selectedInput: { kind: "capacity_acceptance_diagnostic", runId: input.runId, mode },
346
+ outputs: { summary: "Capacity acceptance diagnostic completed.", runId: input.runId },
347
+ usageActual: {
348
+ actualCredits: 0,
349
+ nativeUsage: { nativeUnit: "request", amount: 1, source: "capacity_acceptance" },
350
+ metadata
351
+ },
352
+ traceRefs: { liveAcceptanceRunId: input.runId },
353
+ metadata
354
+ });
355
+ const modeRunId = String(modeRun.payload.id ?? "");
356
+ if (!modeRunId) throw new Error("Capacity acceptance mode-run creation did not return an id.");
357
+ const completed = await providerClient.completeAssignment(assignmentId, {
358
+ runnerId,
359
+ leaseToken,
360
+ modeRunId,
361
+ actualCredits: 0.25,
362
+ nativeUsage: { nativeUnit: "request", amount: 1, source: "capacity_acceptance" },
363
+ output: { summary: "Capacity acceptance diagnostic completed.", runId: input.runId },
364
+ summary: { runId: input.runId, modeRunId },
365
+ metadata
366
+ });
367
+ const finalStatus = String(completed.payload?.status ?? "");
368
+ if (finalStatus !== "completed") {
369
+ throw new Error(`Capacity acceptance assignment finished with status "${finalStatus || "unknown"}".`);
370
+ }
371
+ const modeRuns = await adminClient.projectAgentModeRuns(config.projectId, { assignmentId });
372
+ const modeRunCount = Array.isArray(modeRuns.payload) ? modeRuns.payload.length : 0;
373
+ if (!modeRunCount) throw new Error("Capacity acceptance mode-run was not visible through project mode-run inspection.");
374
+ return {
375
+ sessionId,
376
+ assignmentId,
377
+ modeRunId,
378
+ finalStatus,
379
+ mode,
380
+ runnerId,
381
+ modeRunCount
382
+ };
383
+ }
384
+ function shortRunId(now = /* @__PURE__ */ new Date()) {
385
+ return now.toISOString().replace(/[^0-9]/gu, "").slice(0, 14);
386
+ }
387
+ function providerPrefix(environment, provider, runId) {
388
+ if (provider === "railway") return `trsd-rail-${runId}`.toLowerCase();
389
+ return `trsd-live-${environment}-${provider}-${runId}`.toLowerCase();
390
+ }
391
+ function providerPrefixRoot(environment, provider) {
392
+ if (provider === "railway") return "trsd-rail-";
393
+ return `trsd-live-${environment}-${provider}-`.toLowerCase();
394
+ }
395
+ function emitProgress(onProgress, event) {
396
+ if (!onProgress) return;
397
+ onProgress({
398
+ ...event,
399
+ message: event.message ?? [
400
+ event.provider,
401
+ event.capability,
402
+ event.phase
403
+ ].filter(Boolean).join(":")
404
+ });
405
+ }
406
+ function sleep(ms) {
407
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
408
+ }
409
+ async function waitForLiveObservation(description, observe, isReady, options = {}) {
410
+ const attempts = Math.max(1, options.attempts ?? 8);
411
+ const intervalMs = Math.max(0, options.intervalMs ?? 750);
412
+ let lastValue;
413
+ let lastError;
414
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
415
+ try {
416
+ lastValue = await observe();
417
+ if (isReady(lastValue)) return lastValue;
418
+ } catch (error) {
419
+ lastError = error;
420
+ }
421
+ if (attempt < attempts) await sleep(intervalMs);
422
+ }
423
+ if (lastError) {
424
+ throw new Error(`${description} was not observed live: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
425
+ }
426
+ throw new Error(`${description} was not observed live after ${attempts} attempt(s).`);
427
+ }
428
+ function parseGitHubRepository(value) {
429
+ const raw = value.trim().replace(/^https?:\/\/github\.com\//iu, "").replace(/^git@github\.com:/iu, "").replace(/^ssh:\/\/git@github\.com\//iu, "").replace(/\.git$/iu, "").replace(/^\/+|\/+$/gu, "");
430
+ const [owner, repo, ...extra] = raw.split("/").filter(Boolean);
431
+ if (!owner || !repo || extra.length > 0) {
432
+ throw new Error(`Invalid GitHub repository "${value}". Expected owner/name.`);
433
+ }
434
+ return `${owner}/${repo}`;
435
+ }
436
+ function resolveCurrentGitHubRepository(cwd, env) {
437
+ const configured = configuredValue(env, ["TREESEED_REPOSITORY", "GITHUB_REPOSITORY"]);
438
+ if (configured) return parseGitHubRepository(configured);
439
+ const remote = runTreeseedGit(["config", "--get", "remote.origin.url"], {
440
+ cwd,
441
+ mode: "read"
442
+ }).stdout.trim();
443
+ return parseGitHubRepository(remote);
444
+ }
445
+ function domainFromWorkspace(cwd) {
446
+ try {
447
+ const manifest = readFileSync(join(cwd, "treeseed.site.yaml"), "utf8");
448
+ const match = /^siteUrl:\s*(\S+)/gmu.exec(manifest);
449
+ if (!match?.[1]) return "";
450
+ const url = new URL(match[1]);
451
+ return url.hostname.replace(/^www\./iu, "");
452
+ } catch {
453
+ return "";
454
+ }
455
+ }
456
+ function resolveLiveTestDomain(cwd, env) {
457
+ return configuredValue(env, ["TREESEED_LIVE_TEST_DOMAIN"]) || configuredValue(env, ["TREESEED_DOMAIN"]) || domainFromWorkspace(cwd);
458
+ }
459
+ async function resolveCloudflareZoneId(domain, env, fetchImpl) {
460
+ const configured = configuredValue(env, ["TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID", "CLOUDFLARE_ZONE_ID"]);
461
+ if (configured) return configured;
462
+ const zones = await cloudflareRequest("/zones?per_page=100", env, fetchImpl).catch(() => []);
463
+ if (!Array.isArray(zones)) return "";
464
+ const candidates = zones.map((entry) => entry && typeof entry === "object" ? entry : null).filter(Boolean);
465
+ const matched = candidates.find((entry) => typeof entry.name === "string" && (domain === entry.name || domain.endsWith(`.${entry.name}`)));
466
+ return typeof matched?.id === "string" ? matched.id : "";
467
+ }
468
+ function scenario({
469
+ provider,
470
+ mode,
471
+ prefix,
472
+ capability,
473
+ ok,
474
+ phase,
475
+ action,
476
+ reason,
477
+ locators = {},
478
+ createdResources = [],
479
+ updatedResources = [],
480
+ replacedResources = [],
481
+ destroyedResources = [],
482
+ retainedResources = [],
483
+ issues = [],
484
+ startedAt,
485
+ completedAt,
486
+ durationMs
487
+ }) {
488
+ const completed = completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
489
+ const started = startedAt ?? completed;
490
+ return {
491
+ id: `live-test:${provider}:${capability}`,
492
+ provider,
493
+ capability,
494
+ mode,
495
+ ok,
496
+ phase,
497
+ action,
498
+ reason,
499
+ startedAt: started,
500
+ completedAt: completed,
501
+ durationMs: typeof durationMs === "number" ? Math.max(1, Math.ceil(durationMs)) : Math.max(1, Date.parse(completed) - Date.parse(started)),
502
+ locators,
503
+ createdResources,
504
+ updatedResources,
505
+ replacedResources,
506
+ destroyedResources,
507
+ retainedResources,
508
+ issues
509
+ };
510
+ }
511
+ function node(provider, environment, type, id, state = {}) {
512
+ return {
513
+ id,
514
+ provider,
515
+ type,
516
+ owner: "reconcile-live-test",
517
+ environment,
518
+ state
519
+ };
520
+ }
521
+ function redactProviderState(value) {
522
+ if (Array.isArray(value)) return value.map((entry) => redactProviderState(entry));
523
+ if (value && typeof value === "object") {
524
+ const redacted = {};
525
+ for (const [key, entry] of Object.entries(value)) {
526
+ redacted[key] = /secret|token|password|private|key/iu.test(key) ? "[redacted]" : redactProviderState(entry);
527
+ }
528
+ return redacted;
529
+ }
530
+ return value;
531
+ }
532
+ function providerNode(provider, environment, type, id, state = {}) {
533
+ return node(provider, environment, type, id, redactProviderState(state));
534
+ }
535
+ function blocking(provider, type, reason) {
536
+ return {
537
+ id: `live-test:${provider}:${type}:blocked`,
538
+ resourceId: `live-test:${provider}:${type}`,
539
+ severity: "blocking",
540
+ reason,
541
+ provider,
542
+ type
543
+ };
544
+ }
545
+ async function measuredScenario(input, fn) {
546
+ const started = /* @__PURE__ */ new Date();
547
+ const startedMs = performance.now();
548
+ emitProgress(input.onProgress, {
549
+ provider: input.provider,
550
+ mode: input.mode,
551
+ environment: input.environment,
552
+ runId: input.runId,
553
+ resourcePrefix: input.prefix,
554
+ capability: input.capability,
555
+ phase: input.phase === "verify" ? "verify" : input.phase === "cleanup" ? "cleanup" : input.phase === "destroy" ? "destroy" : "create",
556
+ message: input.startMessage ?? `${input.provider}:${input.capability}: ${input.phase} started`
557
+ });
558
+ try {
559
+ const value = await fn();
560
+ const completed = /* @__PURE__ */ new Date();
561
+ const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
562
+ const resourcesFor = (resources) => typeof resources === "function" ? resources(value) : resources ?? [];
563
+ emitProgress(input.onProgress, {
564
+ provider: input.provider,
565
+ mode: input.mode,
566
+ environment: input.environment,
567
+ runId: input.runId,
568
+ resourcePrefix: input.prefix,
569
+ capability: input.capability,
570
+ phase: "complete",
571
+ elapsedMs: durationMs,
572
+ message: `${input.provider}:${input.capability}: ok in ${durationMs}ms`
573
+ });
574
+ return scenario({
575
+ provider: input.provider,
576
+ mode: input.mode,
577
+ prefix: input.prefix,
578
+ capability: input.capability,
579
+ ok: true,
580
+ phase: input.phase,
581
+ action: input.action,
582
+ reason: typeof input.successReason === "function" ? input.successReason(value) : input.successReason,
583
+ locators: input.locators,
584
+ createdResources: resourcesFor(input.createdResources),
585
+ updatedResources: resourcesFor(input.updatedResources),
586
+ replacedResources: resourcesFor(input.replacedResources),
587
+ destroyedResources: resourcesFor(input.destroyedResources),
588
+ retainedResources: resourcesFor(input.retainedResources),
589
+ startedAt: started.toISOString(),
590
+ completedAt: completed.toISOString(),
591
+ durationMs
592
+ });
593
+ } catch (error) {
594
+ const completed = /* @__PURE__ */ new Date();
595
+ const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
596
+ const reason = error instanceof Error ? error.message : String(error);
597
+ emitProgress(input.onProgress, {
598
+ provider: input.provider,
599
+ mode: input.mode,
600
+ environment: input.environment,
601
+ runId: input.runId,
602
+ resourcePrefix: input.prefix,
603
+ capability: input.capability,
604
+ phase: "blocked",
605
+ elapsedMs: durationMs,
606
+ message: `${input.provider}:${input.capability}: blocked after ${durationMs}ms - ${reason}`
607
+ });
608
+ return scenario({
609
+ provider: input.provider,
610
+ mode: input.mode,
611
+ prefix: input.prefix,
612
+ capability: input.capability,
613
+ ok: false,
614
+ phase: "blocked",
615
+ action: "blocked",
616
+ reason,
617
+ locators: input.locators,
618
+ startedAt: started.toISOString(),
619
+ completedAt: completed.toISOString(),
620
+ durationMs
621
+ });
622
+ }
623
+ }
624
+ function reportForProvider({
625
+ provider,
626
+ mode,
627
+ runId,
628
+ prefix,
629
+ environment,
630
+ results,
631
+ cleanupDrift = []
632
+ }) {
633
+ const capabilities = PROVIDER_CAPABILITIES[provider];
634
+ const desiredGraph = capabilities.map((capability) => ({
635
+ id: `live-test:${provider}:${capability}`,
636
+ provider,
637
+ type: capability,
638
+ owner: "reconcile-live-test",
639
+ environment,
640
+ spec: { prefix, isolated: true, mode }
641
+ }));
642
+ const resultByCapability = new Map(results.map((result) => [result.capability, result]));
643
+ const blockedDrift = [
644
+ ...desiredGraph.filter((entry) => !resultByCapability.get(String(entry.type))?.ok).map((entry) => blocking(provider, String(entry.type), resultByCapability.get(String(entry.type))?.reason ?? "Live scenario did not run.")),
645
+ ...cleanupDrift
646
+ ];
647
+ const actions = desiredGraph.map((entry) => {
648
+ const result = resultByCapability.get(String(entry.type));
649
+ return {
650
+ id: `${entry.id}:${result?.action ?? "blocked"}`,
651
+ kind: result?.ok ? result.action : "blocked",
652
+ resourceId: entry.id,
653
+ reason: result?.reason ?? "Live scenario did not run.",
654
+ provider,
655
+ type: entry.type
656
+ };
657
+ });
658
+ const postconditions = desiredGraph.map((entry) => {
659
+ const result = resultByCapability.get(String(entry.type));
660
+ return {
661
+ id: `${entry.id}:postcondition`,
662
+ resourceId: entry.id,
663
+ description: `${mode} live reconciliation postconditions pass for ${provider}:${entry.type}.`,
664
+ source: provider === "local" ? "local" : "api",
665
+ required: true,
666
+ ok: Boolean(result?.ok),
667
+ issues: result?.ok ? [] : [result?.reason ?? "Live scenario did not run."],
668
+ observed: result?.locators ?? {}
669
+ };
670
+ });
671
+ const createdResources = results.flatMap((result) => result.createdResources);
672
+ const updatedResources = results.flatMap((result) => result.updatedResources);
673
+ const replacedResources = results.flatMap((result) => result.replacedResources);
674
+ const destroyedResources = results.flatMap((result) => result.destroyedResources);
675
+ const retainedResources = results.flatMap((result) => result.retainedResources);
676
+ const report = createTreeseedCanonicalReconcileReport({
677
+ desiredGraph,
678
+ observedGraph: desiredGraph.filter((entry) => resultByCapability.has(String(entry.type))).map((entry) => ({
679
+ ...entry,
680
+ state: {
681
+ verified: Boolean(resultByCapability.get(String(entry.type))?.ok),
682
+ locators: resultByCapability.get(String(entry.type))?.locators ?? {}
683
+ }
684
+ })),
685
+ diff: blockedDrift,
686
+ actions,
687
+ postconditions,
688
+ blockedDrift,
689
+ retainedResources,
690
+ destroyedResources,
691
+ liveVerification: {
692
+ ok: blockedDrift.length === 0,
693
+ source: `reconcile-live-test:${mode}`,
694
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
695
+ issues: blockedDrift.map((entry) => entry.reason)
696
+ }
697
+ });
698
+ return {
699
+ provider,
700
+ mode,
701
+ runId,
702
+ resourcePrefix: prefix,
703
+ scenarioResults: results,
704
+ coverage: {
705
+ total: capabilities.length,
706
+ passed: results.filter((result) => result.ok).length,
707
+ failed: capabilities.length - results.filter((result) => result.ok).length,
708
+ capabilities
709
+ },
710
+ createdResources,
711
+ updatedResources,
712
+ replacedResources,
713
+ destroyedResources,
714
+ retainedResources,
715
+ cleanupDrift,
716
+ report,
717
+ ok: report.ok
718
+ };
719
+ }
720
+ async function cloudflareRequestPayload(path, env, fetchImpl, init = {}) {
721
+ const token = configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]);
722
+ if (!token) throw new Error("Missing TREESEED_CLOUDFLARE_API_TOKEN.");
723
+ const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
724
+ ...init,
725
+ headers: {
726
+ Accept: "application/json",
727
+ ...init.body ? { "Content-Type": "application/json" } : {},
728
+ Authorization: `Bearer ${token}`,
729
+ ...init.headers ?? {}
730
+ }
731
+ });
732
+ const payload = await response.json().catch(() => ({}));
733
+ if (!response.ok || payload.success === false) {
734
+ const errors = Array.isArray(payload.errors) ? payload.errors.map((entry) => entry.message).filter(Boolean).join("; ") : "";
735
+ throw new Error(`${response.status} ${response.statusText}${errors ? `: ${errors}` : ""}`);
736
+ }
737
+ return payload;
738
+ }
739
+ async function cloudflareRequest(path, env, fetchImpl, init = {}) {
740
+ const payload = await cloudflareRequestPayload(path, env, fetchImpl, init);
741
+ return payload.result;
742
+ }
743
+ function isTransientCloudflareError(error) {
744
+ const message = error instanceof Error ? error.message : String(error);
745
+ return /\b(500|502|503|504|520|521|522|523|524)\b|internal server error|unknown error occurred|temporar(?:y|ily)|timeout|rate limit/iu.test(message);
746
+ }
747
+ async function withCloudflareTransientRetry(operation, options = {}) {
748
+ const attempts = Math.max(1, options.attempts ?? 4);
749
+ const delayMs = Math.max(0, options.delayMs ?? 1500);
750
+ let lastError;
751
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
752
+ try {
753
+ return await operation();
754
+ } catch (error) {
755
+ lastError = error;
756
+ if (!isTransientCloudflareError(error) || attempt >= attempts) break;
757
+ await sleep(delayMs * attempt);
758
+ }
759
+ }
760
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
761
+ }
762
+ async function cloudflareRawRequest(path, env, fetchImpl, init = {}) {
763
+ const token = configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]);
764
+ if (!token) throw new Error("Missing TREESEED_CLOUDFLARE_API_TOKEN.");
765
+ const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
766
+ ...init,
767
+ headers: {
768
+ Accept: "*/*",
769
+ Authorization: `Bearer ${token}`,
770
+ ...init.headers ?? {}
771
+ }
772
+ });
773
+ const body = await response.text().catch(() => "");
774
+ if (!response.ok) {
775
+ throw new Error(`${response.status} ${response.statusText}${body ? `: ${body.slice(0, 300)}` : ""}`);
776
+ }
777
+ return body;
778
+ }
779
+ async function githubRequest(path, token, fetchImpl, init = {}) {
780
+ const response = await fetchImpl(`https://api.github.com${path}`, {
781
+ ...init,
782
+ headers: {
783
+ Accept: "application/vnd.github+json",
784
+ ...init.body ? { "Content-Type": "application/json" } : {},
785
+ Authorization: `Bearer ${token}`,
786
+ "X-GitHub-Api-Version": "2022-11-28",
787
+ ...init.headers ?? {}
788
+ }
789
+ });
790
+ const payload = await response.json().catch(() => ({}));
791
+ if (!response.ok) {
792
+ throw new Error(`${response.status} ${response.statusText}${payload.message ? `: ${payload.message}` : ""}`);
793
+ }
794
+ return payload;
795
+ }
796
+ async function runSmokeProvider({
797
+ provider,
798
+ environment,
799
+ prefix,
800
+ mode,
801
+ cwd,
802
+ env,
803
+ fetchImpl
804
+ }) {
805
+ if (provider === "railway") {
806
+ if (!configuredValue(env, ["TREESEED_RAILWAY_API_TOKEN"])) {
807
+ return PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
808
+ provider,
809
+ mode,
810
+ prefix,
811
+ capability,
812
+ ok: false,
813
+ phase: "blocked",
814
+ action: "blocked",
815
+ reason: "Missing TREESEED_RAILWAY_API_TOKEN for Railway live reconciliation tests."
816
+ }));
817
+ }
818
+ try {
819
+ const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
820
+ const projects = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
821
+ const apiProject = projects.find((project) => project.name === "treeseed-api") ?? projects[0] ?? null;
822
+ const environments = apiProject ? await listRailwayEnvironments({ projectId: apiProject.id, env, fetchImpl }) : [];
823
+ const selectedEnvironment = environments.find((candidate) => candidate.name === environment) ?? environments.find((candidate) => candidate.name === (environment === "prod" ? "production" : "staging")) ?? environments[0] ?? null;
824
+ const services = apiProject ? await listRailwayServices({ projectId: apiProject.id, env, fetchImpl }) : [];
825
+ const variables = apiProject && selectedEnvironment ? await listRailwayVariables({ projectId: apiProject.id, environmentId: selectedEnvironment.id, env, fetchImpl }).catch(() => ({})) : {};
826
+ const volumes = apiProject ? await listRailwayVolumes({ projectId: apiProject.id, env, fetchImpl }).catch(() => []) : [];
827
+ const serviceNames = services.map((service) => service.name);
828
+ const base = {
829
+ workspaceId: workspace.id,
830
+ projectId: apiProject?.id ?? null,
831
+ environmentId: selectedEnvironment?.id ?? null
832
+ };
833
+ return [
834
+ scenario({ provider, mode, prefix, capability: "project", ok: projects.length > 0, phase: "smoke", action: "noop", reason: projects.length ? "Railway projects are observable." : "No Railway projects are visible.", locators: base }),
835
+ scenario({ provider, mode, prefix, capability: "environment", ok: Boolean(selectedEnvironment), phase: "smoke", action: "noop", reason: selectedEnvironment ? "Railway environments are observable." : "No Railway environment is visible.", locators: base }),
836
+ scenario({ provider, mode, prefix, capability: "service", ok: services.length > 0, phase: "smoke", action: "noop", reason: services.length ? "Railway services are observable." : "No Railway services are visible.", locators: base }),
837
+ scenario({ provider, mode, prefix, capability: "image-service", ok: serviceNames.some((name) => /api|runner|treedx/iu.test(name)), phase: "smoke", action: "noop", reason: "Railway image service observation completed.", locators: base }),
838
+ scenario({ provider, mode, prefix, capability: "postgres", ok: serviceNames.some((name) => /postgres/iu.test(name)), phase: "smoke", action: "noop", reason: "Railway PostgreSQL observation completed.", locators: base }),
839
+ scenario({ provider, mode, prefix, capability: "volume", ok: volumes.length > 0, phase: "smoke", action: "noop", reason: "Railway volume observation completed.", locators: base }),
840
+ scenario({ provider, mode, prefix, capability: "domain", ok: true, phase: "smoke", action: "noop", reason: "Railway domain API uses authenticated provider surface.", locators: base }),
841
+ scenario({ provider, mode, prefix, capability: "variables", ok: Boolean(selectedEnvironment), phase: "smoke", action: "noop", reason: `Railway variables API observed ${Object.keys(variables).length} variables.`, locators: base }),
842
+ scenario({ provider, mode, prefix, capability: "deployment-health", ok: services.length > 0, phase: "smoke", action: "noop", reason: "Railway deployment-health inspection has observable services.", locators: base }),
843
+ scenario({ provider, mode, prefix, capability: "capacity-provider-runtime-assignment-proof", ok: true, phase: "smoke", action: "noop", reason: "Railway capacity runtime proof is available through explicit acceptance mode.", locators: base })
844
+ ];
845
+ } catch (error) {
846
+ const reason = error instanceof Error ? error.message : String(error);
847
+ return PROVIDER_CAPABILITIES.railway.map((capability) => scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason }));
848
+ }
849
+ }
850
+ if (provider === "cloudflare") {
851
+ const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
852
+ if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) || !accountId) {
853
+ return PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({
854
+ provider,
855
+ mode,
856
+ prefix,
857
+ capability,
858
+ ok: false,
859
+ phase: "blocked",
860
+ action: "blocked",
861
+ reason: "Missing TREESEED_CLOUDFLARE_API_TOKEN or TREESEED_CLOUDFLARE_ACCOUNT_ID for Cloudflare live reconciliation tests."
862
+ }));
863
+ }
864
+ const checks = [
865
+ ["pages", `/accounts/${accountId}/pages/projects?per_page=1`],
866
+ ["worker", `/accounts/${accountId}/workers/services?per_page=1`],
867
+ ["d1", `/accounts/${accountId}/d1/database?per_page=1`],
868
+ ["r2", `/accounts/${accountId}/r2/buckets?per_page=1`],
869
+ ["kv", `/accounts/${accountId}/storage/kv/namespaces?per_page=1`],
870
+ ["queue", `/accounts/${accountId}/queues?per_page=1`],
871
+ ["dns", "/zones?per_page=1"],
872
+ ["turnstile", `/accounts/${accountId}/challenges/widgets?per_page=1`],
873
+ ["secrets", `/accounts/${accountId}/workers/services?per_page=1`],
874
+ ["cache-rules", "/zones?per_page=1"]
875
+ ];
876
+ return Promise.all(checks.map(async ([capability, path]) => {
877
+ try {
878
+ await cloudflareRequest(path, env, fetchImpl);
879
+ return scenario({ provider, mode, prefix, capability, ok: true, phase: "smoke", action: "noop", reason: "Cloudflare API surface is reachable.", locators: { accountId } });
880
+ } catch (error) {
881
+ return scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: error instanceof Error ? error.message : String(error), locators: { accountId } });
882
+ }
883
+ }));
884
+ }
885
+ if (provider === "github") {
886
+ let repository = "";
887
+ try {
888
+ repository = resolveCurrentGitHubRepository(cwd, env);
889
+ const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
890
+ if (!credential.token) {
891
+ throw new Error(`Missing GitHub credential for ${repository}; expected ${credential.envName} or TREESEED_GITHUB_TOKEN fallback.`);
892
+ }
893
+ const [owner, repo] = credential.repository.split("/");
894
+ const checks = [
895
+ ["environment", `/repos/${owner}/${repo}/environments?per_page=1`],
896
+ ["secret", `/repos/${owner}/${repo}/actions/secrets?per_page=1`],
897
+ ["variable", `/repos/${owner}/${repo}/actions/variables?per_page=1`],
898
+ ["workflow-dispatch", `/repos/${owner}/${repo}/actions/workflows?per_page=1`],
899
+ ["workflow-observation", `/repos/${owner}/${repo}/actions/runs?per_page=1`]
900
+ ];
901
+ const results = await Promise.all(checks.map(async ([capability, path]) => {
902
+ try {
903
+ await githubRequest(path, credential.token ?? "", fetchImpl);
904
+ return scenario({ provider, mode, prefix, capability, ok: true, phase: "smoke", action: "noop", reason: "GitHub API surface is reachable.", locators: { repository: credential.repository, credentialKey: credential.envName } });
905
+ } catch (error) {
906
+ return scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: error instanceof Error ? error.message : String(error), locators: { repository: credential.repository, credentialKey: credential.envName } });
907
+ }
908
+ }));
909
+ results.push(scenario({ provider, mode, prefix, capability: "repository-scoped-token", ok: true, phase: "smoke", action: "noop", reason: credential.fallbackUsed ? "GitHub credential resolved through fallback." : "GitHub credential resolved through repository-scoped key.", locators: { repository: credential.repository, credentialKey: credential.envName } }));
910
+ return results;
911
+ } catch (error) {
912
+ const reason = error instanceof Error ? error.message : String(error);
913
+ return PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider, mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { repository } }));
914
+ }
915
+ }
916
+ return PROVIDER_CAPABILITIES.local.map((capability) => scenario({
917
+ provider,
918
+ mode,
919
+ prefix,
920
+ capability,
921
+ ok: true,
922
+ phase: "smoke",
923
+ action: "noop",
924
+ reason: "Local reconciliation live-test capability is available in-process."
925
+ }));
926
+ }
927
+ async function requireAcceptanceConfig(provider, cwd, env, fetchImpl) {
928
+ const missing = [];
929
+ if (provider === "railway") {
930
+ if (!configuredValue(env, ["TREESEED_RAILWAY_API_TOKEN"])) missing.push("TREESEED_RAILWAY_API_TOKEN");
931
+ if (!resolveLiveTestDomain(cwd, env)) missing.push("TREESEED_LIVE_TEST_DOMAIN or treeseed.site.yaml siteUrl");
932
+ missing.push(...capacityAcceptanceConfig(env).missing);
933
+ }
934
+ if (provider === "cloudflare") {
935
+ if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) missing.push("TREESEED_CLOUDFLARE_API_TOKEN");
936
+ if (!configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"])) missing.push("TREESEED_CLOUDFLARE_ACCOUNT_ID");
937
+ const domain = resolveLiveTestDomain(cwd, env);
938
+ if (!domain) {
939
+ missing.push("TREESEED_LIVE_TEST_DOMAIN or treeseed.site.yaml siteUrl");
940
+ } else if (configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) && !await resolveCloudflareZoneId(domain, env, fetchImpl)) {
941
+ missing.push("TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID or visible Cloudflare zone for live-test domain");
942
+ }
943
+ }
944
+ if (provider === "github") {
945
+ if (!configuredValue(env, ["TREESEED_GITHUB_TOKEN"])) {
946
+ try {
947
+ const repository = configuredValue(env, ["TREESEED_REPOSITORY", "GITHUB_REPOSITORY"]);
948
+ const credential = repository ? resolveGitHubCredentialForRepository(repository, { values: env, env }) : null;
949
+ if (!credential?.token) missing.push("TREESEED_GITHUB_TOKEN or repository-scoped TREESEED_GITHUB_TOKEN_*");
950
+ } catch {
951
+ missing.push("TREESEED_GITHUB_TOKEN or repository-scoped TREESEED_GITHUB_TOKEN_*");
952
+ }
953
+ }
954
+ }
955
+ return missing;
956
+ }
957
+ async function cleanupRailwayPrefixedProjects(environment, env, fetchImpl) {
958
+ const prefixRoot = providerPrefixRoot(environment, "railway");
959
+ const workspace = await resolveRailwayWorkspaceContext({ env, fetchImpl });
960
+ const projects = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
961
+ const prefixed = projects.filter((project) => !project.deletedAt && project.name.startsWith(prefixRoot));
962
+ const destroyed = [];
963
+ for (const project of prefixed) {
964
+ await deleteRailwayProject({ projectId: project.id, env, fetchImpl });
965
+ destroyed.push(node("railway", environment, "project", project.name, { id: project.id, deleted: true }));
966
+ }
967
+ const refreshed = await listRailwayProjects({ workspaceId: workspace.id, env, fetchImpl });
968
+ const remaining = refreshed.filter((project) => !project.deletedAt && project.name.startsWith(prefixRoot));
969
+ return { workspace, destroyed, remaining };
970
+ }
971
+ async function runRailwayCleanup(environment, prefix, mode, env, fetchImpl) {
972
+ try {
973
+ const cleanup = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl);
974
+ const results = PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
975
+ provider: "railway",
976
+ mode,
977
+ prefix,
978
+ capability,
979
+ ok: cleanup.remaining.length === 0,
980
+ phase: "cleanup",
981
+ action: cleanup.destroyed.length > 0 ? "delete" : "noop",
982
+ reason: cleanup.remaining.length === 0 ? `Railway cleanup removed ${cleanup.destroyed.length} prefixed test project(s).` : `Railway cleanup left ${cleanup.remaining.length} prefixed test project(s).`,
983
+ destroyedResources: cleanup.destroyed,
984
+ issues: cleanup.remaining.map((project) => `Remaining project ${project.name} (${project.id})`)
985
+ }));
986
+ const cleanupDrift = cleanup.remaining.map((project) => blocking("railway", "project", `Railway live-test project ${project.name} (${project.id}) remained after cleanup.`));
987
+ return { results, cleanupDrift };
988
+ } catch (error) {
989
+ const reason = error instanceof Error ? error.message : String(error);
990
+ return {
991
+ results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({ provider: "railway", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason })),
992
+ cleanupDrift: [blocking("railway", "project", reason)]
993
+ };
994
+ }
995
+ }
996
+ async function runRailwayAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
997
+ const mode = "acceptance";
998
+ const missing = await requireAcceptanceConfig("railway", cwd, env, fetchImpl);
999
+ if (missing.length > 0) {
1000
+ return {
1001
+ results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
1002
+ provider: "railway",
1003
+ mode,
1004
+ prefix,
1005
+ capability,
1006
+ ok: false,
1007
+ phase: "blocked",
1008
+ action: "blocked",
1009
+ reason: `Missing Railway acceptance configuration: ${missing.join(", ")}.`
1010
+ })),
1011
+ cleanupDrift: []
1012
+ };
1013
+ }
1014
+ const cleanupBefore = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl);
1015
+ if (cleanupBefore.remaining.length > 0) {
1016
+ return {
1017
+ results: PROVIDER_CAPABILITIES.railway.map((capability) => scenario({
1018
+ provider: "railway",
1019
+ mode,
1020
+ prefix,
1021
+ capability,
1022
+ ok: false,
1023
+ phase: "blocked",
1024
+ action: "blocked",
1025
+ reason: `Railway acceptance refused to create a project because ${cleanupBefore.remaining.length} prefixed project(s) remain after cleanup.`
1026
+ })),
1027
+ cleanupDrift: cleanupBefore.remaining.map((project) => blocking("railway", "project", `Prefixed Railway project ${project.name} (${project.id}) remained before acceptance.`))
1028
+ };
1029
+ }
1030
+ const domainRoot = resolveLiveTestDomain(cwd, env);
1031
+ const projectName = prefix;
1032
+ const envName = "staging";
1033
+ const serviceName = `${prefix}-web`;
1034
+ const statefulName = `${prefix}-s01`;
1035
+ const volumeName = `${statefulName}-volume`;
1036
+ const postgresName = `${prefix}-pg`;
1037
+ const customDomain = `${prefix}.${domainRoot}`.replace(/_/gu, "-");
1038
+ const results = [];
1039
+ const cleanupDrift = [];
1040
+ let projectId = "";
1041
+ try {
1042
+ const project = await ensureRailwayProject({ projectName, defaultEnvironmentName: envName, env, fetchImpl });
1043
+ projectId = project.project.id;
1044
+ results.push(await measuredScenario({
1045
+ provider: "railway",
1046
+ mode,
1047
+ environment,
1048
+ runId,
1049
+ prefix,
1050
+ capability: "project",
1051
+ phase: "create",
1052
+ action: project.created ? "create" : "adopt",
1053
+ startMessage: "railway:project: create/adopt started",
1054
+ successReason: "Railway acceptance created exactly one test project for all Railway scenarios and observed it by id.",
1055
+ locators: { projectId },
1056
+ createdResources: [providerNode("railway", environment, "project", projectName, { id: projectId })],
1057
+ onProgress
1058
+ }, async () => waitForLiveObservation(
1059
+ `Railway project ${projectName}`,
1060
+ () => listRailwayProjects({ env, fetchImpl }).catch(() => [project.project]),
1061
+ (projects) => projects.some((candidate) => candidate.id === projectId)
1062
+ )));
1063
+ const environmentResult = await ensureRailwayEnvironment({ projectId, environmentName: envName, env, fetchImpl });
1064
+ const environmentId = environmentResult.environment.id;
1065
+ results.push(await measuredScenario({
1066
+ provider: "railway",
1067
+ mode,
1068
+ environment,
1069
+ runId,
1070
+ prefix,
1071
+ capability: "environment",
1072
+ phase: "create",
1073
+ action: environmentResult.created ? "create" : "adopt",
1074
+ startMessage: "railway:environment: create/adopt started",
1075
+ successReason: "Railway acceptance created the project-scoped test environment and observed it live.",
1076
+ locators: { projectId, environmentId },
1077
+ createdResources: [providerNode("railway", environment, "environment", envName, { id: environmentId })],
1078
+ onProgress
1079
+ }, async () => waitForLiveObservation(
1080
+ `Railway environment ${envName}`,
1081
+ () => listRailwayEnvironments({ projectId, env, fetchImpl }),
1082
+ (environments) => environments.some((candidate) => candidate.id === environmentId)
1083
+ )));
1084
+ const service = await ensureRailwayService({
1085
+ projectId,
1086
+ environmentId,
1087
+ serviceName,
1088
+ imageRef: configuredValue(env, ["TREESEED_LIVE_TEST_RAILWAY_IMAGE"]) || "nginxdemos/hello:latest",
1089
+ env,
1090
+ fetchImpl
1091
+ });
1092
+ const serviceId = service.service.id;
1093
+ const stateful = await ensureRailwayService({
1094
+ projectId,
1095
+ environmentId,
1096
+ serviceName: statefulName,
1097
+ imageRef: configuredValue(env, ["TREESEED_LIVE_TEST_RAILWAY_STATEFUL_IMAGE"]) || "nginxdemos/hello:latest",
1098
+ env,
1099
+ fetchImpl
1100
+ });
1101
+ const statefulId = stateful.service.id;
1102
+ results.push(await measuredScenario({
1103
+ provider: "railway",
1104
+ mode,
1105
+ environment,
1106
+ runId,
1107
+ prefix,
1108
+ capability: "service",
1109
+ phase: "create",
1110
+ action: service.created || stateful.created ? "create" : "adopt",
1111
+ startMessage: "railway:service: create/adopt started",
1112
+ successReason: "Railway acceptance created image and stateful services inside the single test project and observed both live.",
1113
+ locators: { projectId, environmentId, serviceId, statefulId },
1114
+ createdResources: [
1115
+ providerNode("railway", environment, "service", serviceName, { id: serviceId }),
1116
+ providerNode("railway", environment, "service", statefulName, { id: statefulId })
1117
+ ],
1118
+ onProgress
1119
+ }, async () => waitForLiveObservation(
1120
+ `Railway services ${serviceName}, ${statefulName}`,
1121
+ () => listRailwayServices({ projectId, env, fetchImpl }),
1122
+ (services) => services.some((candidate) => candidate.id === serviceId) && services.some((candidate) => candidate.id === statefulId)
1123
+ )));
1124
+ await upsertRailwayVariables({
1125
+ projectId,
1126
+ environmentId,
1127
+ serviceId,
1128
+ variables: {
1129
+ TREESEED_LIVE_TEST_RUN_ID: runId,
1130
+ TREESEED_LIVE_TEST_PHASE: "created"
1131
+ },
1132
+ env,
1133
+ fetchImpl
1134
+ });
1135
+ await upsertRailwayVariables({
1136
+ projectId,
1137
+ environmentId,
1138
+ serviceId,
1139
+ variables: {
1140
+ TREESEED_LIVE_TEST_PHASE: "updated"
1141
+ },
1142
+ env,
1143
+ fetchImpl
1144
+ });
1145
+ results.push(await measuredScenario({
1146
+ provider: "railway",
1147
+ mode,
1148
+ environment,
1149
+ runId,
1150
+ prefix,
1151
+ capability: "variables",
1152
+ phase: "update",
1153
+ action: "update",
1154
+ startMessage: "railway:variables: read-back started",
1155
+ successReason: "Railway acceptance created, updated, and observed service variables.",
1156
+ locators: { projectId, environmentId, serviceId },
1157
+ updatedResources: (value) => [providerNode("railway", environment, "variables", `${serviceName}:variables`, { keys: Object.keys(value).sort() })],
1158
+ onProgress
1159
+ }, async () => waitForLiveObservation(
1160
+ "Railway updated service variables",
1161
+ () => listRailwayVariables({ projectId, environmentId, serviceId, env, fetchImpl }),
1162
+ (variables) => variables.TREESEED_LIVE_TEST_PHASE === "updated"
1163
+ )));
1164
+ const volume = await ensureRailwayServiceVolume({
1165
+ projectId,
1166
+ environmentId,
1167
+ serviceId: statefulId,
1168
+ name: volumeName,
1169
+ mountPath: "/data",
1170
+ env,
1171
+ fetchImpl,
1172
+ settleAttempts: 6,
1173
+ settleDelayMs: 2500
1174
+ });
1175
+ results.push(await measuredScenario({
1176
+ provider: "railway",
1177
+ mode,
1178
+ environment,
1179
+ runId,
1180
+ prefix,
1181
+ capability: "volume",
1182
+ phase: volume.created ? "create" : "replace",
1183
+ action: volume.created ? "create" : "reattach",
1184
+ startMessage: "railway:volume: live read-back started",
1185
+ successReason: "Railway acceptance attached/reconciled a stateful service volume and observed it live.",
1186
+ locators: { projectId, environmentId, serviceId: statefulId, volumeId: volume.volume.id },
1187
+ createdResources: [providerNode("railway", environment, "volume", volumeName, { id: volume.volume.id, mountPath: "/data" })],
1188
+ onProgress
1189
+ }, async () => waitForLiveObservation(
1190
+ `Railway volume ${volumeName}`,
1191
+ () => listRailwayVolumes({ projectId, env, fetchImpl }),
1192
+ (volumes) => volumes.some((candidate) => candidate.id === volume.volume.id || candidate.name === volumeName)
1193
+ )));
1194
+ const postgres = await ensureRailwayPostgresService({ projectId, environmentId, serviceName: postgresName, env, fetchImpl, maxAttempts: 80 });
1195
+ results.push(await measuredScenario({
1196
+ provider: "railway",
1197
+ mode,
1198
+ environment,
1199
+ runId,
1200
+ prefix,
1201
+ capability: "postgres",
1202
+ phase: "create",
1203
+ action: postgres.created ? "create" : "adopt",
1204
+ startMessage: "railway:postgres: live read-back started",
1205
+ successReason: postgres.proof.message,
1206
+ locators: { projectId, environmentId, serviceId: postgres.service.id },
1207
+ createdResources: [providerNode("railway", environment, "postgres", postgresName, { id: postgres.service.id, proof: postgres.proof })],
1208
+ onProgress
1209
+ }, async () => {
1210
+ if (!postgres.proof.ok) throw new Error(postgres.proof.message);
1211
+ return waitForLiveObservation(
1212
+ `Railway Postgres service ${postgresName}`,
1213
+ () => listRailwayServices({ projectId, env, fetchImpl }),
1214
+ (services) => services.some((candidate) => candidate.id === postgres.service.id)
1215
+ );
1216
+ }));
1217
+ const generatedDomain = await ensureRailwayGeneratedServiceDomain({ projectId, environmentId, serviceId, targetPort: 80, env, fetchImpl });
1218
+ let customDomainCreated = false;
1219
+ try {
1220
+ await railwayGraphqlRequest({
1221
+ query: `
1222
+ mutation TreeseedLiveRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
1223
+ customDomainCreate(input: $input) { id domain serviceId environmentId }
1224
+ }
1225
+ `.trim(),
1226
+ variables: { input: { projectId, environmentId, serviceId, domain: customDomain } },
1227
+ env,
1228
+ fetchImpl
1229
+ });
1230
+ customDomainCreated = true;
1231
+ } catch {
1232
+ customDomainCreated = false;
1233
+ }
1234
+ results.push(await measuredScenario({
1235
+ provider: "railway",
1236
+ mode,
1237
+ environment,
1238
+ runId,
1239
+ prefix,
1240
+ capability: "domain",
1241
+ phase: "create",
1242
+ action: "create",
1243
+ startMessage: "railway:domain: live read-back started",
1244
+ successReason: customDomainCreated ? "Railway acceptance created generated and custom domain resources." : "Railway acceptance created a generated domain but custom domain creation did not converge.",
1245
+ locators: { projectId, environmentId, serviceId, generatedDomain: generatedDomain.domain.domain, customDomain },
1246
+ createdResources: [providerNode("railway", environment, "domain", generatedDomain.domain.domain, { id: generatedDomain.domain.id })],
1247
+ onProgress
1248
+ }, async () => {
1249
+ if (!generatedDomain.domain.domain || !customDomainCreated) throw new Error("Railway generated/custom domain postconditions did not converge.");
1250
+ return generatedDomain;
1251
+ }));
1252
+ results.push(await measuredScenario({
1253
+ provider: "railway",
1254
+ mode,
1255
+ environment,
1256
+ runId,
1257
+ prefix,
1258
+ capability: "image-service",
1259
+ phase: "verify",
1260
+ action: "noop",
1261
+ startMessage: "railway:image-service: verifying image-backed service",
1262
+ successReason: "Railway acceptance verified image service creation through the project-scoped service API.",
1263
+ locators: { projectId, environmentId, serviceId },
1264
+ onProgress
1265
+ }, async () => waitForLiveObservation(
1266
+ `Railway image service ${serviceName}`,
1267
+ () => listRailwayServices({ projectId, env, fetchImpl }),
1268
+ (services) => services.some((candidate) => candidate.id === serviceId)
1269
+ )));
1270
+ results.push(await measuredScenario({
1271
+ provider: "railway",
1272
+ mode,
1273
+ environment,
1274
+ runId,
1275
+ prefix,
1276
+ capability: "deployment-health",
1277
+ phase: "verify",
1278
+ action: "noop",
1279
+ startMessage: "railway:deployment-health: verifying deployment observation",
1280
+ successReason: "Railway acceptance observed image services after deployment submission; app-specific deep HTTP health remains enforced by hosting verify/apply.",
1281
+ locators: { projectId, environmentId, serviceId },
1282
+ onProgress
1283
+ }, async () => waitForLiveObservation(
1284
+ `Railway deployment service ${serviceName}`,
1285
+ () => listRailwayServices({ projectId, env, fetchImpl }),
1286
+ (services) => services.some((candidate) => candidate.id === serviceId)
1287
+ )));
1288
+ results.push(await measuredScenario({
1289
+ provider: "railway",
1290
+ mode,
1291
+ environment,
1292
+ runId,
1293
+ prefix,
1294
+ capability: "capacity-provider-runtime-assignment-proof",
1295
+ phase: "verify",
1296
+ action: "noop",
1297
+ startMessage: "railway:capacity-provider-runtime-assignment-proof: running assignment lifecycle proof",
1298
+ successReason: "Railway capacity acceptance created a diagnostic assignment, leased it through the provider protocol, emitted mode-run telemetry, and completed it.",
1299
+ locators: { projectId, environmentId },
1300
+ retainedResources: (value) => {
1301
+ const proof = value;
1302
+ return [providerNode("railway", environment, "capacity-runtime-proof", proof.assignmentId, {
1303
+ sessionId: proof.sessionId,
1304
+ modeRunId: proof.modeRunId,
1305
+ finalStatus: proof.finalStatus,
1306
+ mode: proof.mode,
1307
+ modeRunCount: proof.modeRunCount
1308
+ })];
1309
+ },
1310
+ onProgress
1311
+ }, async () => runCapacityProviderAssignmentProof({ provider: "railway", environment, runId, prefix, env, fetchImpl })));
1312
+ } catch (error) {
1313
+ const reason = error instanceof Error ? error.message : String(error);
1314
+ for (const capability of PROVIDER_CAPABILITIES.railway) {
1315
+ if (!results.some((result) => result.capability === capability)) {
1316
+ results.push(scenario({ provider: "railway", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { projectId: projectId || null } }));
1317
+ }
1318
+ }
1319
+ } finally {
1320
+ if (projectId) {
1321
+ await deleteRailwayProject({ projectId, env, fetchImpl }).catch((error) => {
1322
+ cleanupDrift.push(blocking("railway", "project", `Railway acceptance cleanup failed for project ${projectId}: ${error instanceof Error ? error.message : String(error)}`));
1323
+ });
1324
+ }
1325
+ const cleanupAfter = await cleanupRailwayPrefixedProjects(environment, env, fetchImpl).catch((error) => {
1326
+ cleanupDrift.push(blocking("railway", "project", `Railway acceptance final cleanup scan failed: ${error instanceof Error ? error.message : String(error)}`));
1327
+ return null;
1328
+ });
1329
+ if (cleanupAfter) {
1330
+ cleanupDrift.push(...cleanupAfter.remaining.map((project) => blocking("railway", "project", `Railway live-test project ${project.name} (${project.id}) remained after acceptance cleanup.`)));
1331
+ if (cleanupAfter.destroyed.length > 0) {
1332
+ for (const result of results) {
1333
+ result.destroyedResources.push(...cleanupAfter.destroyed);
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ return { results, cleanupDrift };
1339
+ }
1340
+ function cloudflareName(value) {
1341
+ if (value && typeof value === "object") {
1342
+ const record = value;
1343
+ for (const key of ["name", "title", "queue_name"]) {
1344
+ const candidate = record[key];
1345
+ if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
1346
+ }
1347
+ }
1348
+ return "";
1349
+ }
1350
+ function cloudflareId(value) {
1351
+ if (value && typeof value === "object") {
1352
+ const record = value;
1353
+ for (const key of ["id", "uuid", "queue_id"]) {
1354
+ const candidate = record[key];
1355
+ if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
1356
+ }
1357
+ }
1358
+ return "";
1359
+ }
1360
+ function cloudflareListItems(value, keys = []) {
1361
+ if (Array.isArray(value)) return value;
1362
+ if (!value || typeof value !== "object") return [];
1363
+ const record = value;
1364
+ for (const key of [...keys, "items", "buckets", "databases", "queues", "widgets", "namespaces"]) {
1365
+ const candidate = record[key];
1366
+ if (Array.isArray(candidate)) return candidate;
1367
+ }
1368
+ return [];
1369
+ }
1370
+ async function runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl) {
1371
+ const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
1372
+ const domain = resolveLiveTestDomain(cwd, env);
1373
+ const zoneId = domain ? await resolveCloudflareZoneId(domain, env, fetchImpl) : configuredValue(env, ["TREESEED_LIVE_TEST_CLOUDFLARE_ZONE_ID", "CLOUDFLARE_ZONE_ID"]);
1374
+ const destroyed = [];
1375
+ const cleanupDrift = [];
1376
+ const prefixRoot = mode === "cleanup" ? providerPrefixRoot(environment, "cloudflare") : prefix;
1377
+ const attempt = async (type, id, fn) => {
1378
+ try {
1379
+ await fn();
1380
+ destroyed.push(node("cloudflare", environment, type, id, { deleted: true }));
1381
+ } catch (error) {
1382
+ cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup failed for ${id}: ${error instanceof Error ? error.message : String(error)}`));
1383
+ }
1384
+ };
1385
+ const list = async (type, path, keys = []) => {
1386
+ try {
1387
+ return cloudflareListItems(await cloudflareRequest(path, env, fetchImpl), keys);
1388
+ } catch (error) {
1389
+ cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup could not inspect ${path}: ${error instanceof Error ? error.message : String(error)}`));
1390
+ return [];
1391
+ }
1392
+ };
1393
+ const listPaginated = async (type, path, keys = [], perPage = 10) => {
1394
+ const items = [];
1395
+ let totalPages = 1;
1396
+ for (let page = 1; page <= totalPages; page += 1) {
1397
+ const separator = path.includes("?") ? "&" : "?";
1398
+ const pagePath = `${path}${separator}page=${page}&per_page=${perPage}`;
1399
+ try {
1400
+ const payload = await cloudflareRequestPayload(pagePath, env, fetchImpl);
1401
+ items.push(...cloudflareListItems(payload.result, keys));
1402
+ const reportedTotalPages = payload.result_info?.total_pages;
1403
+ if (typeof reportedTotalPages === "number" && Number.isFinite(reportedTotalPages) && reportedTotalPages > totalPages) {
1404
+ totalPages = Math.min(Math.ceil(reportedTotalPages), 100);
1405
+ }
1406
+ } catch (error) {
1407
+ cleanupDrift.push(blocking("cloudflare", type, `Cloudflare cleanup could not inspect ${pagePath}: ${error instanceof Error ? error.message : String(error)}`));
1408
+ break;
1409
+ }
1410
+ }
1411
+ return items;
1412
+ };
1413
+ if (!configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"]) || !accountId) {
1414
+ cleanupDrift.push(blocking("cloudflare", "account", "Cloudflare cleanup requires TREESEED_CLOUDFLARE_API_TOKEN and TREESEED_CLOUDFLARE_ACCOUNT_ID."));
1415
+ }
1416
+ if (accountId && configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) {
1417
+ if (mode === "acceptance") {
1418
+ await attempt("worker", prefix, () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl, { method: "DELETE" }).catch((error) => {
1419
+ const message = error instanceof Error ? error.message : String(error);
1420
+ if (/404|not found/iu.test(message)) return null;
1421
+ throw error;
1422
+ }));
1423
+ }
1424
+ for (const worker of await list("worker", `/accounts/${accountId}/workers/services?per_page=100`)) {
1425
+ const name = cloudflareName(worker);
1426
+ if (name.startsWith(prefixRoot)) await attempt("worker", name, () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${name}`, env, fetchImpl, { method: "DELETE" }));
1427
+ }
1428
+ for (const project of await listPaginated("pages", `/accounts/${accountId}/pages/projects`)) {
1429
+ const name = cloudflareName(project);
1430
+ if (name.startsWith(prefixRoot)) await attempt("pages", name, () => cloudflareRequest(`/accounts/${accountId}/pages/projects/${name}`, env, fetchImpl, { method: "DELETE" }));
1431
+ }
1432
+ for (const bucket of await list("r2", `/accounts/${accountId}/r2/buckets?per_page=100`, ["buckets"])) {
1433
+ const name = cloudflareName(bucket);
1434
+ if (name.startsWith(prefixRoot)) await attempt("r2", name, () => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${name}`, env, fetchImpl, { method: "DELETE" }));
1435
+ }
1436
+ for (const namespace of await list("kv", `/accounts/${accountId}/storage/kv/namespaces?per_page=100`)) {
1437
+ const name = cloudflareName(namespace);
1438
+ const id = cloudflareId(namespace);
1439
+ if (name.startsWith(prefixRoot) && id) await attempt("kv", id, () => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces/${id}`, env, fetchImpl, { method: "DELETE" }));
1440
+ }
1441
+ for (const database of await list("d1", `/accounts/${accountId}/d1/database?per_page=100`)) {
1442
+ const name = cloudflareName(database);
1443
+ const id = cloudflareId(database);
1444
+ if (name.startsWith(prefixRoot) && id) await attempt("d1", id, () => cloudflareRequest(`/accounts/${accountId}/d1/database/${id}`, env, fetchImpl, { method: "DELETE" }));
1445
+ }
1446
+ for (const queue of await list("queue", `/accounts/${accountId}/queues?per_page=100`, ["queues"])) {
1447
+ const name = cloudflareName(queue);
1448
+ const id = cloudflareId(queue);
1449
+ if (name.startsWith(prefixRoot) && id) await attempt("queue", id, () => cloudflareRequest(`/accounts/${accountId}/queues/${id}`, env, fetchImpl, { method: "DELETE" }));
1450
+ }
1451
+ for (const widget of await list("turnstile", `/accounts/${accountId}/challenges/widgets?per_page=100`)) {
1452
+ const name = cloudflareName(widget);
1453
+ const id = cloudflareId(widget);
1454
+ if (name.startsWith(prefixRoot) && id) await attempt("turnstile", id, () => cloudflareRequest(`/accounts/${accountId}/challenges/widgets/${id}`, env, fetchImpl, { method: "DELETE" }));
1455
+ }
1456
+ }
1457
+ if (zoneId && configuredValue(env, ["TREESEED_CLOUDFLARE_API_TOKEN"])) {
1458
+ for (const record of await list("dns", `/zones/${zoneId}/dns_records?per_page=100`)) {
1459
+ const name = cloudflareName(record);
1460
+ const id = cloudflareId(record);
1461
+ if (name.startsWith(prefixRoot) && id) await attempt("dns", id, () => cloudflareRequest(`/zones/${zoneId}/dns_records/${id}`, env, fetchImpl, { method: "DELETE" }));
1462
+ }
1463
+ }
1464
+ const results = PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({
1465
+ provider: "cloudflare",
1466
+ mode,
1467
+ prefix,
1468
+ capability,
1469
+ ok: cleanupDrift.length === 0,
1470
+ phase: "cleanup",
1471
+ action: destroyed.some((resource) => resource.type === capability) ? "delete" : "noop",
1472
+ reason: cleanupDrift.length === 0 ? `Cloudflare cleanup removed ${destroyed.filter((resource) => resource.type === capability).length} ${capability} resource(s).` : "Cloudflare cleanup left blocking drift.",
1473
+ destroyedResources: destroyed.filter((resource) => resource.type === capability)
1474
+ }));
1475
+ return { results, cleanupDrift };
1476
+ }
1477
+ async function runCloudflareAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
1478
+ const mode = "acceptance";
1479
+ const missing = await requireAcceptanceConfig("cloudflare", cwd, env, fetchImpl);
1480
+ if (missing.length > 0) {
1481
+ return {
1482
+ results: PROVIDER_CAPABILITIES.cloudflare.map((capability) => scenario({ provider: "cloudflare", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason: `Missing Cloudflare acceptance configuration: ${missing.join(", ")}.` })),
1483
+ cleanupDrift: []
1484
+ };
1485
+ }
1486
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, phase: "cleanup", message: `cloudflare: removing old live-test resources for ${prefix}` });
1487
+ await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl);
1488
+ const accountId = configuredValue(env, ["TREESEED_CLOUDFLARE_ACCOUNT_ID"]);
1489
+ const domain = resolveLiveTestDomain(cwd, env);
1490
+ const zoneId = await resolveCloudflareZoneId(domain, env, fetchImpl);
1491
+ const results = [];
1492
+ const created = [];
1493
+ const attempt = async (capability, type, create, verify) => {
1494
+ const started = /* @__PURE__ */ new Date();
1495
+ const startedMs = performance.now();
1496
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "create", message: `cloudflare:${capability}: create/update started` });
1497
+ try {
1498
+ const result = await create();
1499
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "verify", message: `cloudflare:${capability}: waiting for live observation` });
1500
+ const observed = await verify(result);
1501
+ const completed = /* @__PURE__ */ new Date();
1502
+ const createdNode = providerNode("cloudflare", environment, type, `${prefix}:${type}`, { result, observed });
1503
+ created.push(createdNode);
1504
+ const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
1505
+ results.push(scenario({
1506
+ provider: "cloudflare",
1507
+ mode,
1508
+ prefix,
1509
+ capability,
1510
+ ok: true,
1511
+ phase: capability === "cache-rules" ? "verify" : "create",
1512
+ action: capability === "cache-rules" ? "noop" : "create",
1513
+ reason: capability === "cache-rules" ? "Cloudflare acceptance observed cache-rules API access." : `Cloudflare acceptance created ${capability} and verified it with a live read-back.`,
1514
+ locators: { accountId, zoneId },
1515
+ createdResources: capability === "cache-rules" ? [] : [createdNode],
1516
+ startedAt: started.toISOString(),
1517
+ completedAt: completed.toISOString(),
1518
+ durationMs
1519
+ }));
1520
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "complete", elapsedMs: durationMs, message: `cloudflare:${capability}: ok in ${durationMs}ms` });
1521
+ } catch (error) {
1522
+ const completed = /* @__PURE__ */ new Date();
1523
+ const durationMs = Math.max(1, Math.ceil(performance.now() - startedMs));
1524
+ const reason = error instanceof Error ? error.message : String(error);
1525
+ const providerLimited = capability === "cache-rules" && /403|forbidden|authentication/iu.test(reason);
1526
+ results.push(scenario({
1527
+ provider: "cloudflare",
1528
+ mode,
1529
+ prefix,
1530
+ capability,
1531
+ ok: false,
1532
+ phase: "blocked",
1533
+ action: "blocked",
1534
+ reason: providerLimited ? `${reason}. Cloudflare cache-rules acceptance requires Cloudflare token permissions: target zone Cache Settings Write and Zone Read, plus account Account Rulesets Write and Account Rule Lists Write. Cloudflare API docs may call these Cache Rules and Account Filter Lists.` : reason,
1535
+ locators: { accountId, zoneId },
1536
+ startedAt: started.toISOString(),
1537
+ completedAt: completed.toISOString(),
1538
+ durationMs
1539
+ }));
1540
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, capability, phase: "blocked", elapsedMs: durationMs, message: `cloudflare:${capability}: blocked after ${durationMs}ms - ${providerLimited ? "missing cache-rules permissions" : reason}` });
1541
+ }
1542
+ };
1543
+ await attempt("worker", "worker", () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl, {
1544
+ method: "PUT",
1545
+ headers: { "Content-Type": "application/javascript" },
1546
+ body: 'addEventListener("fetch", event => event.respondWith(new Response("treeseed-live-test-worker")));'
1547
+ }), () => waitForLiveObservation(
1548
+ `Cloudflare worker ${prefix}`,
1549
+ () => cloudflareRawRequest(`/accounts/${accountId}/workers/scripts/${prefix}`, env, fetchImpl),
1550
+ (value) => typeof value === "string" && value.includes("treeseed-live-test-worker")
1551
+ ));
1552
+ await attempt("pages", "pages", () => withCloudflareTransientRetry(() => cloudflareRequest(`/accounts/${accountId}/pages/projects`, env, fetchImpl, {
1553
+ method: "POST",
1554
+ body: JSON.stringify({ name: prefix, production_branch: "main" })
1555
+ })), () => waitForLiveObservation(
1556
+ `Cloudflare Pages project ${prefix}`,
1557
+ () => cloudflareRequest(`/accounts/${accountId}/pages/projects/${prefix}`, env, fetchImpl),
1558
+ (value) => Boolean(value && typeof value === "object")
1559
+ ));
1560
+ await attempt("kv", "kv", () => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces`, env, fetchImpl, {
1561
+ method: "POST",
1562
+ body: JSON.stringify({ title: prefix })
1563
+ }), () => waitForLiveObservation(
1564
+ `Cloudflare KV namespace ${prefix}`,
1565
+ () => cloudflareRequest(`/accounts/${accountId}/storage/kv/namespaces?per_page=100`, env, fetchImpl),
1566
+ (value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
1567
+ ));
1568
+ await attempt("r2", "r2", () => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${prefix}`, env, fetchImpl, { method: "PUT" }), () => waitForLiveObservation(
1569
+ `Cloudflare R2 bucket ${prefix}`,
1570
+ () => cloudflareRequest(`/accounts/${accountId}/r2/buckets/${prefix}`, env, fetchImpl),
1571
+ (value) => Boolean(value && typeof value === "object")
1572
+ ));
1573
+ await attempt("d1", "d1", () => cloudflareRequest(`/accounts/${accountId}/d1/database`, env, fetchImpl, {
1574
+ method: "POST",
1575
+ body: JSON.stringify({ name: prefix })
1576
+ }), (createdResult) => {
1577
+ const id = cloudflareId(createdResult);
1578
+ return waitForLiveObservation(
1579
+ `Cloudflare D1 database ${prefix}`,
1580
+ () => id ? cloudflareRequest(`/accounts/${accountId}/d1/database/${id}`, env, fetchImpl) : cloudflareRequest(`/accounts/${accountId}/d1/database?per_page=100`, env, fetchImpl),
1581
+ (value) => Boolean(value && typeof value === "object") || Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
1582
+ );
1583
+ });
1584
+ await attempt("queue", "queue", () => cloudflareRequest(`/accounts/${accountId}/queues`, env, fetchImpl, {
1585
+ method: "POST",
1586
+ body: JSON.stringify({ queue_name: prefix })
1587
+ }), () => waitForLiveObservation(
1588
+ `Cloudflare Queue ${prefix}`,
1589
+ () => cloudflareRequest(`/accounts/${accountId}/queues?per_page=100`, env, fetchImpl),
1590
+ (value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
1591
+ ));
1592
+ await attempt("turnstile", "turnstile", () => cloudflareRequest(`/accounts/${accountId}/challenges/widgets`, env, fetchImpl, {
1593
+ method: "POST",
1594
+ body: JSON.stringify({ name: prefix, domains: [`${prefix}.${domain}`], mode: "managed" })
1595
+ }), () => waitForLiveObservation(
1596
+ `Cloudflare Turnstile widget ${prefix}`,
1597
+ () => cloudflareRequest(`/accounts/${accountId}/challenges/widgets?per_page=100`, env, fetchImpl),
1598
+ (value) => Array.isArray(value) && value.some((entry) => cloudflareName(entry) === prefix)
1599
+ ));
1600
+ await attempt("dns", "dns", () => cloudflareRequest(`/zones/${zoneId}/dns_records`, env, fetchImpl, {
1601
+ method: "POST",
1602
+ body: JSON.stringify({ type: "TXT", name: `${prefix}.${domain}`, content: "treeseed-live-test", ttl: 60 })
1603
+ }), (createdResult) => {
1604
+ const id = cloudflareId(createdResult);
1605
+ return waitForLiveObservation(
1606
+ `Cloudflare DNS record ${prefix}.${domain}`,
1607
+ () => id ? cloudflareRequest(`/zones/${zoneId}/dns_records/${id}`, env, fetchImpl) : cloudflareRequest(`/zones/${zoneId}/dns_records?name=${encodeURIComponent(`${prefix}.${domain}`)}`, env, fetchImpl),
1608
+ (value) => Boolean(value && typeof value === "object") || Array.isArray(value) && value.some((entry) => cloudflareName(entry) === `${prefix}.${domain}`)
1609
+ );
1610
+ });
1611
+ await attempt("secrets", "secrets", () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}/secrets`, env, fetchImpl, {
1612
+ method: "PUT",
1613
+ body: JSON.stringify({ name: "TREESEED_LIVE_TEST_SECRET", text: "redacted-test-value", type: "secret_text" })
1614
+ }), () => waitForLiveObservation(
1615
+ `Cloudflare Worker secret for ${prefix}`,
1616
+ () => cloudflareRequest(`/accounts/${accountId}/workers/scripts/${prefix}/settings`, env, fetchImpl),
1617
+ (value) => Boolean(value && typeof value === "object")
1618
+ ));
1619
+ await attempt("cache-rules", "cache-rules", () => cloudflareRequest(`/zones/${zoneId}/rulesets`, env, fetchImpl, {
1620
+ method: "GET"
1621
+ }), (createdResult) => Promise.resolve(createdResult));
1622
+ emitProgress(onProgress, { provider: "cloudflare", mode, environment, runId, resourcePrefix: prefix, phase: "destroy", message: `cloudflare: cleaning created resources for ${prefix}` });
1623
+ const cleanup = await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl);
1624
+ return { results, cleanupDrift: cleanup.cleanupDrift };
1625
+ }
1626
+ async function runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl) {
1627
+ const repository = resolveCurrentGitHubRepository(cwd, env);
1628
+ const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
1629
+ const cleanupDrift = [];
1630
+ const destroyed = [];
1631
+ const prefixRoot = mode === "cleanup" ? providerPrefixRoot(environment, "github") : prefix;
1632
+ if (!credential.token) {
1633
+ cleanupDrift.push(blocking("github", "repository-scoped-token", `Missing GitHub credential for ${repository}.`));
1634
+ } else {
1635
+ const [owner, repo] = credential.repository.split("/");
1636
+ const variables = await githubRequest(`/repos/${owner}/${repo}/actions/variables?per_page=100`, credential.token, fetchImpl).catch(() => ({ variables: [] }));
1637
+ for (const variable of variables.variables ?? []) {
1638
+ const name = variable.name ?? "";
1639
+ if (!name.startsWith(`TREESEED_LIVE_TEST_${prefixRoot.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`)) continue;
1640
+ try {
1641
+ await githubRequest(`/repos/${owner}/${repo}/actions/variables/${name}`, credential.token, fetchImpl, { method: "DELETE" });
1642
+ destroyed.push(node("github", environment, "variable", name, { deleted: true }));
1643
+ } catch (error) {
1644
+ const message = error instanceof Error ? error.message : String(error);
1645
+ if (!/404|Not Found/iu.test(message)) cleanupDrift.push(blocking("github", "variable", message));
1646
+ }
1647
+ }
1648
+ const environments = await githubRequest(`/repos/${owner}/${repo}/environments?per_page=100`, credential.token, fetchImpl).catch(() => ({ environments: [] }));
1649
+ for (const candidate of environments.environments ?? []) {
1650
+ const name = candidate.name ?? "";
1651
+ if (!name.startsWith(prefixRoot)) continue;
1652
+ try {
1653
+ await githubRequest(`/repos/${owner}/${repo}/environments/${name}`, credential.token, fetchImpl, { method: "DELETE" });
1654
+ destroyed.push(node("github", environment, "environment", name, { deleted: true }));
1655
+ } catch (error) {
1656
+ const message = error instanceof Error ? error.message : String(error);
1657
+ if (!/404|Not Found/iu.test(message)) cleanupDrift.push(blocking("github", "environment", message));
1658
+ }
1659
+ }
1660
+ }
1661
+ const results = PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider: "github", mode, prefix, capability, ok: cleanupDrift.length === 0, phase: "cleanup", action: destroyed.length ? "delete" : "noop", reason: cleanupDrift.length === 0 ? "GitHub cleanup completed." : "GitHub cleanup left blocking drift.", destroyedResources: destroyed }));
1662
+ return { results, cleanupDrift };
1663
+ }
1664
+ async function runGitHubAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress) {
1665
+ const mode = "acceptance";
1666
+ let repository = "";
1667
+ try {
1668
+ repository = resolveCurrentGitHubRepository(cwd, env);
1669
+ const credential = resolveGitHubCredentialForRepository(repository, { values: env, env });
1670
+ if (!credential.token) throw new Error(`Missing GitHub credential for ${repository}; expected ${credential.envName} or TREESEED_GITHUB_TOKEN fallback.`);
1671
+ const [owner, repo] = credential.repository.split("/");
1672
+ const environmentName = prefix;
1673
+ const variableName = `TREESEED_LIVE_TEST_${prefix.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`;
1674
+ await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl);
1675
+ const results = [];
1676
+ results.push(await measuredScenario({
1677
+ provider: "github",
1678
+ mode,
1679
+ environment,
1680
+ runId,
1681
+ prefix,
1682
+ capability: "environment",
1683
+ phase: "create",
1684
+ action: "create",
1685
+ startMessage: "github:environment: create/update started",
1686
+ successReason: "GitHub acceptance created a test environment and observed it live.",
1687
+ locators: { repository: credential.repository, environment: environmentName },
1688
+ onProgress
1689
+ }, async () => {
1690
+ await githubRequest(`/repos/${owner}/${repo}/environments/${environmentName}`, credential.token, fetchImpl, { method: "PUT", body: JSON.stringify({}) });
1691
+ return waitForLiveObservation(
1692
+ `GitHub environment ${environmentName}`,
1693
+ () => githubRequest(`/repos/${owner}/${repo}/environments?per_page=100`, credential.token ?? "", fetchImpl),
1694
+ (value) => Array.isArray(value.environments) && (value.environments ?? []).some((candidate) => candidate.name === environmentName)
1695
+ );
1696
+ }));
1697
+ results.push(await measuredScenario({
1698
+ provider: "github",
1699
+ mode,
1700
+ environment,
1701
+ runId,
1702
+ prefix,
1703
+ capability: "variable",
1704
+ phase: "update",
1705
+ action: "update",
1706
+ startMessage: "github:variable: create/update started",
1707
+ successReason: "GitHub acceptance created, updated, and observed a repository variable.",
1708
+ locators: { repository: credential.repository, variable: variableName },
1709
+ onProgress
1710
+ }, async () => {
1711
+ await githubRequest(`/repos/${owner}/${repo}/actions/variables`, credential.token, fetchImpl, { method: "POST", body: JSON.stringify({ name: variableName, value: "created" }) }).catch(async (error) => {
1712
+ if (/already_exists|already exists|409/iu.test(error instanceof Error ? error.message : String(error))) {
1713
+ await githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token ?? "", fetchImpl, { method: "PATCH", body: JSON.stringify({ name: variableName, value: "created" }) });
1714
+ return;
1715
+ }
1716
+ throw error;
1717
+ });
1718
+ await githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token, fetchImpl, { method: "PATCH", body: JSON.stringify({ name: variableName, value: "updated" }) });
1719
+ return waitForLiveObservation(
1720
+ `GitHub variable ${variableName}`,
1721
+ () => githubRequest(`/repos/${owner}/${repo}/actions/variables/${variableName}`, credential.token ?? "", fetchImpl),
1722
+ (value) => value.name === variableName && value.value === "updated"
1723
+ );
1724
+ }));
1725
+ results.push(await measuredScenario({
1726
+ provider: "github",
1727
+ mode,
1728
+ environment,
1729
+ runId,
1730
+ prefix,
1731
+ capability: "secret",
1732
+ phase: "verify",
1733
+ action: "noop",
1734
+ startMessage: "github:secret: verifying public-key secret API access",
1735
+ successReason: "GitHub acceptance observed repository public-key access for Actions secret encryption.",
1736
+ locators: { repository: credential.repository },
1737
+ onProgress
1738
+ }, async () => githubRequest(`/repos/${owner}/${repo}/actions/secrets/public-key`, credential.token, fetchImpl)));
1739
+ results.push(await measuredScenario({
1740
+ provider: "github",
1741
+ mode,
1742
+ environment,
1743
+ runId,
1744
+ prefix,
1745
+ capability: "workflow-dispatch",
1746
+ phase: "verify",
1747
+ action: "noop",
1748
+ startMessage: "github:workflow-dispatch: verifying dispatchable workflow metadata",
1749
+ successReason: "GitHub acceptance observed workflow metadata for dispatch routing.",
1750
+ locators: { repository: credential.repository },
1751
+ onProgress
1752
+ }, async () => {
1753
+ const workflows = await githubRequest(`/repos/${owner}/${repo}/actions/workflows?per_page=100`, credential.token, fetchImpl);
1754
+ const workflow = workflows.workflows?.find((candidate) => candidate.state === "active") ?? workflows.workflows?.[0] ?? null;
1755
+ if (!workflow) throw new Error("No workflow is available for dispatch observation.");
1756
+ return workflow;
1757
+ }));
1758
+ results.push(await measuredScenario({
1759
+ provider: "github",
1760
+ mode,
1761
+ environment,
1762
+ runId,
1763
+ prefix,
1764
+ capability: "workflow-observation",
1765
+ phase: "verify",
1766
+ action: "noop",
1767
+ startMessage: "github:workflow-observation: reading workflow runs",
1768
+ successReason: "GitHub acceptance observed workflow runs.",
1769
+ locators: { repository: credential.repository },
1770
+ onProgress
1771
+ }, async () => githubRequest(`/repos/${owner}/${repo}/actions/runs?per_page=1`, credential.token, fetchImpl)));
1772
+ results.push(await measuredScenario({
1773
+ provider: "github",
1774
+ mode,
1775
+ environment,
1776
+ runId,
1777
+ prefix,
1778
+ capability: "repository-scoped-token",
1779
+ phase: "verify",
1780
+ action: "noop",
1781
+ startMessage: "github:repository-scoped-token: resolving credential",
1782
+ successReason: credential.fallbackUsed ? "GitHub acceptance resolved fallback credential." : "GitHub acceptance resolved repository-scoped credential.",
1783
+ locators: { repository: credential.repository, credentialKey: credential.envName },
1784
+ onProgress
1785
+ }, async () => credential));
1786
+ const cleanup = await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl);
1787
+ return { results, cleanupDrift: cleanup.cleanupDrift };
1788
+ } catch (error) {
1789
+ const reason = error instanceof Error ? error.message : String(error);
1790
+ return {
1791
+ results: PROVIDER_CAPABILITIES.github.map((capability) => scenario({ provider: "github", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason, locators: { repository } })),
1792
+ cleanupDrift: []
1793
+ };
1794
+ }
1795
+ }
1796
+ async function listenOnEphemeralPort(server) {
1797
+ return new Promise((resolve2, reject) => {
1798
+ server.once("error", reject);
1799
+ server.listen(0, "127.0.0.1", () => {
1800
+ const address = server.address();
1801
+ if (address && typeof address === "object") resolve2(address.port);
1802
+ else reject(new Error("Local server did not expose an address."));
1803
+ });
1804
+ });
1805
+ }
1806
+ async function closeServer(server) {
1807
+ return new Promise((resolve2, reject) => {
1808
+ server.close((error) => error ? reject(error) : resolve2());
1809
+ });
1810
+ }
1811
+ async function runLocalAcceptance(environment, prefix, mode, runId, env, fetchImpl, onProgress) {
1812
+ const created = [];
1813
+ const destroyed = [];
1814
+ const tempBase = resolve(process.cwd(), ".treeseed", "tmp", "live-acceptance");
1815
+ await mkdir(tempBase, { recursive: true });
1816
+ const dir = await mkdtemp(join(tempBase, `${prefix}-`));
1817
+ const results = [];
1818
+ let server = null;
1819
+ try {
1820
+ results.push(await measuredScenario({
1821
+ provider: "local",
1822
+ mode,
1823
+ environment,
1824
+ runId,
1825
+ prefix,
1826
+ capability: "local-db",
1827
+ phase: "create",
1828
+ action: "create",
1829
+ startMessage: "local:local-db: creating isolated state",
1830
+ successReason: "Local acceptance created, wrote, read, and removed isolated local state.",
1831
+ createdResources: [node("local", environment, "local-db", dir, { path: dir })],
1832
+ onProgress
1833
+ }, async () => {
1834
+ const file = join(dir, "state.json");
1835
+ await writeFile(file, JSON.stringify({ ok: true, runId }), "utf8");
1836
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
1837
+ if (parsed.ok !== true || parsed.runId !== runId) throw new Error("Local state read-back did not match the written payload.");
1838
+ created.push(node("local", environment, "local-db", dir, { path: dir }));
1839
+ return parsed;
1840
+ }));
1841
+ server = createServer((socket) => {
1842
+ socket.end("treeseed-live-test-local\n");
1843
+ });
1844
+ results.push(await measuredScenario({
1845
+ provider: "local",
1846
+ mode,
1847
+ environment,
1848
+ runId,
1849
+ prefix,
1850
+ capability: "port",
1851
+ phase: "create",
1852
+ action: "create",
1853
+ startMessage: "local:port: binding ephemeral port",
1854
+ successReason: "Local acceptance bound and observed an ephemeral loopback port.",
1855
+ onProgress
1856
+ }, async () => {
1857
+ const port = await listenOnEphemeralPort(server);
1858
+ if (!port) throw new Error("No local port was allocated.");
1859
+ return { port };
1860
+ }));
1861
+ results.push(await measuredScenario({
1862
+ provider: "local",
1863
+ mode,
1864
+ environment,
1865
+ runId,
1866
+ prefix,
1867
+ capability: "process",
1868
+ phase: "verify",
1869
+ action: "noop",
1870
+ startMessage: "local:process: verifying current process",
1871
+ successReason: "Local acceptance observed the current Node process as a supervised-process stand-in.",
1872
+ locators: { pid: String(process.pid) },
1873
+ onProgress
1874
+ }, async () => {
1875
+ if (!process.pid) throw new Error("Current process id is unavailable.");
1876
+ return { pid: process.pid };
1877
+ }));
1878
+ results.push(await measuredScenario({
1879
+ provider: "local",
1880
+ mode,
1881
+ environment,
1882
+ runId,
1883
+ prefix,
1884
+ capability: "local-runner",
1885
+ phase: "verify",
1886
+ action: "noop",
1887
+ startMessage: "local:local-runner: verifying runner probe",
1888
+ successReason: "Local acceptance verified the local runner probe contract.",
1889
+ onProgress
1890
+ }, async () => ({ runnerProbe: true, runId })));
1891
+ results.push(await measuredScenario({
1892
+ provider: "local",
1893
+ mode,
1894
+ environment,
1895
+ runId,
1896
+ prefix,
1897
+ capability: "docker-compose-capacity-provider",
1898
+ phase: "verify",
1899
+ action: "noop",
1900
+ startMessage: "local:docker-compose-capacity-provider: checking Docker availability",
1901
+ successReason: (value) => value.available ? "Local acceptance observed Docker for the Docker Compose capacity-provider probe." : "Local acceptance checked Docker Compose capacity-provider probe availability; Docker is not installed or not reachable in this shell.",
1902
+ onProgress
1903
+ }, async () => {
1904
+ try {
1905
+ const docker = execFileSync("docker", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
1906
+ return { docker, available: true };
1907
+ } catch (error) {
1908
+ return { docker: error instanceof Error ? error.message : String(error), available: false };
1909
+ }
1910
+ }));
1911
+ results.push(await measuredScenario({
1912
+ provider: "local",
1913
+ mode,
1914
+ environment,
1915
+ runId,
1916
+ prefix,
1917
+ capability: "capacity-provider-assignment-proof",
1918
+ phase: "verify",
1919
+ action: "noop",
1920
+ startMessage: "local:capacity-provider-assignment-proof: running assignment lifecycle proof",
1921
+ successReason: "Local capacity acceptance created a diagnostic assignment, leased it through the provider protocol, emitted mode-run telemetry, and completed it.",
1922
+ retainedResources: (value) => {
1923
+ const proof = value;
1924
+ return [providerNode("local", environment, "capacity-runtime-proof", proof.assignmentId, {
1925
+ sessionId: proof.sessionId,
1926
+ modeRunId: proof.modeRunId,
1927
+ finalStatus: proof.finalStatus,
1928
+ mode: proof.mode,
1929
+ modeRunCount: proof.modeRunCount
1930
+ })];
1931
+ },
1932
+ onProgress
1933
+ }, async () => runCapacityProviderAssignmentProof({ provider: "local", environment, runId, prefix, env, fetchImpl })));
1934
+ } catch (error) {
1935
+ const reason = error instanceof Error ? error.message : String(error);
1936
+ for (const capability of PROVIDER_CAPABILITIES.local) {
1937
+ if (!results.some((result) => result.capability === capability)) {
1938
+ results.push(scenario({ provider: "local", mode, prefix, capability, ok: false, phase: "blocked", action: "blocked", reason }));
1939
+ }
1940
+ }
1941
+ } finally {
1942
+ if (server) await closeServer(server).catch(() => void 0);
1943
+ await rm(dir, { recursive: true, force: true });
1944
+ destroyed.push(node("local", environment, "local-db", dir, { deleted: true }));
1945
+ }
1946
+ return { results, cleanupDrift: [], destroyedResources: destroyed };
1947
+ }
1948
+ async function runProvider({
1949
+ provider,
1950
+ mode,
1951
+ environment,
1952
+ runId,
1953
+ cwd,
1954
+ env,
1955
+ fetchImpl,
1956
+ onProgress
1957
+ }) {
1958
+ const prefix = providerPrefix(environment, provider, runId);
1959
+ const started = Date.now();
1960
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: "start", message: `${provider}: ${mode} live reconciliation started` });
1961
+ if (mode === "smoke") {
1962
+ const results2 = await runSmokeProvider({ provider, environment, prefix, mode, cwd, env, fetchImpl });
1963
+ const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2 });
1964
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
1965
+ return report2;
1966
+ }
1967
+ if (provider === "railway") {
1968
+ const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runRailwayCleanup(environment, prefix, mode, env, fetchImpl) : await runRailwayAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
1969
+ const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
1970
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
1971
+ return report2;
1972
+ }
1973
+ if (provider === "cloudflare") {
1974
+ const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runCloudflareCleanup(cwd, environment, prefix, mode, env, fetchImpl) : await runCloudflareAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
1975
+ const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
1976
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
1977
+ return report2;
1978
+ }
1979
+ if (provider === "github") {
1980
+ const { results: results2, cleanupDrift: cleanupDrift2 } = mode === "cleanup" ? await runGitHubCleanup(cwd, environment, prefix, mode, env, fetchImpl) : await runGitHubAcceptance(cwd, environment, runId, prefix, env, fetchImpl, onProgress);
1981
+ const report2 = reportForProvider({ provider, mode, runId, prefix, environment, results: results2, cleanupDrift: cleanupDrift2 });
1982
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report2.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report2.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
1983
+ return report2;
1984
+ }
1985
+ const { results, cleanupDrift } = await runLocalAcceptance(environment, prefix, mode, runId, env, fetchImpl, onProgress);
1986
+ const report = reportForProvider({ provider, mode, runId, prefix, environment, results, cleanupDrift });
1987
+ emitProgress(onProgress, { provider, mode, environment, runId, resourcePrefix: prefix, phase: report.ok ? "complete" : "blocked", elapsedMs: Date.now() - started, message: `${provider}: ${report.ok ? "passed" : "blocked"} in ${Date.now() - started}ms` });
1988
+ return report;
1989
+ }
1990
+ function treeseedLiveReconcileProviderCapabilities(provider) {
1991
+ return [...PROVIDER_CAPABILITIES[provider]];
1992
+ }
1993
+ function treeseedLiveReconcileResourcePrefix(environment, provider, runId) {
1994
+ return providerPrefix(environment, provider, runId);
1995
+ }
1996
+ async function runTreeseedLiveReconcileTests(options) {
1997
+ const mode = options.mode ?? "smoke";
1998
+ const runId = options.runId ?? shortRunId(options.now);
1999
+ const env = options.env ?? process.env;
2000
+ const fetchImpl = options.fetchImpl ?? fetch;
2001
+ const providers = [...new Set(options.providers)];
2002
+ const reports = await Promise.all(providers.map((provider) => runProvider({
2003
+ provider,
2004
+ mode,
2005
+ environment: options.environment,
2006
+ runId,
2007
+ cwd: options.cwd,
2008
+ env,
2009
+ fetchImpl,
2010
+ onProgress: options.onProgress
2011
+ })));
2012
+ return {
2013
+ command: "reconcile test-live",
2014
+ mode,
2015
+ environment: options.environment,
2016
+ runId,
2017
+ resourcePrefix: `trsd-live-${options.environment}`,
2018
+ providers: reports,
2019
+ ok: reports.every((report) => report.ok)
2020
+ };
2021
+ }
2022
+ export {
2023
+ compileTreeseedLiveAcceptanceScenarios,
2024
+ runTreeseedLiveReconcileTests,
2025
+ treeseedLiveReconcileProviderCapabilities,
2026
+ treeseedLiveReconcileResourcePrefix
2027
+ };