@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
@@ -1,6 +1,12 @@
1
- import { existsSync } from "node:fs";
2
- import { relative, resolve } from "node:path";
3
- import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
1
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, relative, resolve } from "node:path";
7
+ import { resolveCapacityProviderLaunchPlan } from "../capacity-provider.js";
8
+ import { TreeDxClient } from "../treedx-client.js";
9
+ import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues, setTreeseedMachineEnvironmentValue } from "../operations/services/config-runtime.js";
4
10
  import {
5
11
  buildPublicVars,
6
12
  buildCloudflarePagesFunctionBindings,
@@ -26,6 +32,7 @@ import {
26
32
  queueName,
27
33
  reconcileCloudflareWebCacheRules,
28
34
  resolveConfiguredCloudflareAccountId,
35
+ resolveConfiguredSurfaceDomain,
29
36
  resolveCloudflareZoneIdForHost,
30
37
  runWrangler,
31
38
  scopeFromTarget,
@@ -34,10 +41,7 @@ import {
34
41
  } from "../operations/services/deploy.js";
35
42
  import {
36
43
  configuredRailwayServices,
37
- deriveRailwayMarketOperationsRunnerVolumeName,
38
- ensureRailwayProjectContext,
39
- ensureRailwayServiceVolumeWithCliFallback,
40
- runRailway,
44
+ findStaleTreeseedOperationsRunnerResources,
41
45
  validateRailwayDeployPrerequisites
42
46
  } from "../operations/services/railway-deploy.js";
43
47
  import { shouldExposeManagedHostRuntimeSecret } from "../operations/services/managed-host-security.js";
@@ -47,19 +51,56 @@ import {
47
51
  ensureRailwayProject,
48
52
  ensureRailwayService,
49
53
  ensureRailwayServiceInstanceConfiguration,
54
+ ensureRailwayServiceVolume,
55
+ ensureRailwayPostgresService,
56
+ deleteRailwayService,
57
+ deleteRailwayVolume,
58
+ deployRailwayServiceInstance,
59
+ listRailwayEnvironmentServices,
50
60
  getRailwayServiceInstance,
51
61
  getRailwayProject,
52
62
  listRailwayCustomDomains,
53
63
  listRailwayEnvironments,
54
64
  listRailwayProjects,
65
+ listRailwayServices,
55
66
  listRailwayVolumes,
56
67
  listRailwayVariables,
57
68
  resolveRailwayWorkspaceContext,
58
- updateRailwayServiceName,
59
- upsertRailwayVariables
69
+ updateRailwayServiceGitSource,
70
+ updateRailwayServiceImageSource
60
71
  } from "../operations/services/railway-api.js";
61
72
  import { loadTreeseedReconcileState } from "./state.js";
62
73
  import { createTreeseedReconcileUnitId } from "./units.js";
74
+ import { discoverTreeseedApplications } from "../hosting/apps.js";
75
+ import { findTreeseedPackageAdapter, syncTreeseedPackageWorkflows } from "../operations/services/package-adapters.js";
76
+ import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
77
+ import {
78
+ dispatchReconcileGitHubWorkflow,
79
+ ensureReconcileGitHubEnvironment,
80
+ observeGitHubEnvironment,
81
+ observeGitHubWorkflowRun,
82
+ upsertReconcileGitHubSecret,
83
+ upsertReconcileGitHubVariable
84
+ } from "./providers/github-private.js";
85
+ import {
86
+ buildDockerImage,
87
+ inspectDockerAvailability,
88
+ inspectDockerImage,
89
+ inspectDockerManifest,
90
+ runDockerCompose
91
+ } from "./providers/docker-private.js";
92
+ import { checkHttpHealth, runManagedDevAction } from "./providers/local-private.js";
93
+ import {
94
+ ensureTemplateReleaseTag,
95
+ runHostedReconcileGate,
96
+ runHostedVerifyGate,
97
+ runReleaseVerifyCommand,
98
+ runTemplateReleaseVerifyCommand,
99
+ writeReleaseRecord
100
+ } from "./providers/release-private.js";
101
+ import { checkedOutTemplateRepositories } from "../operations/services/managed-repositories.js";
102
+ import { runTreeseedGit } from "../operations/services/git-runner.js";
103
+ import { withDockerhubServiceCredentialEnv } from "../service-credentials.js";
63
104
  function toDeployTarget(target) {
64
105
  return target.kind === "persistent" ? createPersistentDeployTarget(target.scope) : createBranchPreviewDeployTarget(target.branchName);
65
106
  }
@@ -75,7 +116,7 @@ function isTransientCloudflareReconcileError(error) {
75
116
  }
76
117
  function isTransientRailwayReconcileError(error) {
77
118
  const message = error instanceof Error ? error.message : String(error ?? "");
78
- return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|connectivity issue|rate limit|too many requests|429|5\d\d/iu.test(message);
119
+ return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|connectivity issue|rate limit|too many requests|429|5\d\d|operation is already in progress|Problem processing request/iu.test(message);
79
120
  }
80
121
  function syntheticQueueLocator(name) {
81
122
  return `queue-name:${name}`;
@@ -103,81 +144,1793 @@ function noopDiff() {
103
144
  after: {}
104
145
  };
105
146
  }
106
- function parseRailwayJsonPayload(output) {
107
- const text = typeof output === "string" ? output.trim() : "";
108
- if (!text) {
109
- return null;
110
- }
111
- try {
112
- return JSON.parse(text);
113
- } catch {
114
- }
115
- const lines = text.split(/\r?\n/u);
116
- for (let index = lines.length - 1; index >= 0; index -= 1) {
117
- const candidate = lines.slice(index).join("\n").trim();
118
- if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
119
- continue;
120
- }
121
- try {
122
- return JSON.parse(candidate);
123
- } catch {
124
- }
125
- }
126
- return null;
147
+ function buildCompositeAdapter(unitType) {
148
+ return {
149
+ providerId: "treeseed",
150
+ unitTypes: [unitType],
151
+ supports(candidateUnitType, providerId) {
152
+ return candidateUnitType === unitType && providerId === "treeseed";
153
+ },
154
+ async refresh(input) {
155
+ return noopObservedState(input);
156
+ },
157
+ diff() {
158
+ return noopDiff();
159
+ },
160
+ requiredPostconditions({ unit }) {
161
+ return unit.dependencies.map((dependency) => ({
162
+ key: dependency,
163
+ description: `Dependency ${dependency} is verified`
164
+ }));
165
+ },
166
+ apply({ unit, observed, diff }) {
167
+ return {
168
+ unit,
169
+ observed,
170
+ diff,
171
+ action: diff.action,
172
+ warnings: [],
173
+ resourceLocators: {},
174
+ state: {
175
+ unitId: unit.unitId,
176
+ reconciledAt: nowIso()
177
+ },
178
+ verification: null
179
+ };
180
+ },
181
+ verify({ context, unit, postconditions }) {
182
+ const dependencyResults = context.session.get("treeseed:verification-results");
183
+ const checks = postconditions.map((condition) => {
184
+ const dependency = dependencyResults?.get(condition.key);
185
+ const verified = dependency?.verified === true;
186
+ return {
187
+ key: condition.key,
188
+ description: condition.description,
189
+ source: "derived",
190
+ exists: verified,
191
+ configured: verified,
192
+ ready: verified,
193
+ verified,
194
+ expected: true,
195
+ observed: dependency?.verified ?? false,
196
+ issues: verified ? [] : [`Dependency ${condition.key} is not verified.`]
197
+ };
198
+ });
199
+ return summarizeVerification(unit.unitId, checks);
200
+ }
201
+ };
202
+ }
203
+ function genericObservedState(input, exists = true, warnings = []) {
204
+ return {
205
+ exists,
206
+ status: exists ? "ready" : "pending",
207
+ live: {
208
+ unitId: input.unit.unitId,
209
+ unitType: input.unit.unitType,
210
+ provider: input.unit.provider,
211
+ spec: input.unit.spec
212
+ },
213
+ locators: { unitId: input.unit.unitId },
214
+ warnings
215
+ };
216
+ }
217
+ function genericResult(input, state = input.observed.live) {
218
+ return {
219
+ unit: input.unit,
220
+ observed: input.observed,
221
+ diff: input.diff,
222
+ action: input.diff.action,
223
+ warnings: input.observed.warnings,
224
+ resourceLocators: input.observed.locators,
225
+ state: {
226
+ ...state,
227
+ reconciledAt: nowIso()
228
+ },
229
+ verification: null
230
+ };
231
+ }
232
+ function genericVerification(input, observed, description = "Desired resource is represented in the canonical graph") {
233
+ return summarizeVerification(input.unit.unitId, [
234
+ verificationCheck("desired-resource", description, "derived", {
235
+ exists: observed.exists,
236
+ configured: true,
237
+ ready: observed.status !== "error",
238
+ verified: observed.exists && observed.status !== "error",
239
+ observed: observed.live
240
+ })
241
+ ], observed.warnings);
242
+ }
243
+ function buildManifestAdapter() {
244
+ return {
245
+ providerId: "treeseed",
246
+ unitTypes: ["package-manifest", "template-manifest"],
247
+ supports(unitType, providerId) {
248
+ return (unitType === "package-manifest" || unitType === "template-manifest") && providerId === "treeseed";
249
+ },
250
+ async refresh(input) {
251
+ const manifestPath = typeof input.unit.spec.manifestPath === "string" ? input.unit.spec.manifestPath : null;
252
+ const warningLabel = input.unit.unitType === "template-manifest" ? "template manifest path is not available" : "package manifest path is not available";
253
+ return genericObservedState(input, manifestPath ? existsSync(manifestPath) : true, manifestPath ? [] : [warningLabel]);
254
+ },
255
+ diff(input) {
256
+ const label = input.unit.unitType === "template-manifest" ? "template manifest" : "package manifest";
257
+ return input.observed.exists ? noopDiff() : { action: "blocked", reasons: [`${label} is missing`], before: input.observed.live, after: input.unit.spec };
258
+ },
259
+ apply(input) {
260
+ return genericResult(input);
261
+ },
262
+ verify(input) {
263
+ return genericVerification(input, input.observed, input.unit.unitType === "template-manifest" ? "Template manifest exists and is readable" : "Package manifest exists and is readable");
264
+ }
265
+ };
266
+ }
267
+ function buildPackageWorkflowAdapter() {
268
+ return {
269
+ providerId: "github",
270
+ unitTypes: ["package-workflow"],
271
+ supports(unitType, providerId) {
272
+ return unitType === "package-workflow" && providerId === "github";
273
+ },
274
+ refresh(input) {
275
+ const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : typeof input.unit.metadata.packageId === "string" ? input.unit.metadata.packageId : null;
276
+ const adapter = packageId ? findTreeseedPackageAdapter(input.context.tenantRoot, packageId) : null;
277
+ const sync = packageId ? syncTreeseedPackageWorkflows({ root: input.context.tenantRoot, packageId, execute: false }) : [];
278
+ return {
279
+ ...genericObservedState(input, Boolean(adapter), adapter ? [] : [`Package ${packageId ?? "(unknown)"} was not discovered.`]),
280
+ status: sync.some((entry) => entry.changed || !entry.exists) ? "drifted" : adapter ? "ready" : "error",
281
+ live: {
282
+ ...input.unit.spec,
283
+ packageId,
284
+ workflows: sync
285
+ }
286
+ };
287
+ },
288
+ diff(input) {
289
+ if (!input.observed.exists) {
290
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
291
+ }
292
+ const workflows = Array.isArray(input.observed.live.workflows) ? input.observed.live.workflows : [];
293
+ if (workflows.some((entry) => entry.changed || !entry.exists)) {
294
+ return { action: "update", reasons: ["package workflow files differ from rendered templates"], before: input.observed.live, after: input.unit.spec };
295
+ }
296
+ return noopDiff();
297
+ },
298
+ apply(input) {
299
+ const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : typeof input.unit.metadata.packageId === "string" ? input.unit.metadata.packageId : null;
300
+ if (packageId && input.context.dryRun !== true && input.diff.action !== "noop") {
301
+ const sync = syncTreeseedPackageWorkflows({
302
+ root: input.context.tenantRoot,
303
+ packageId,
304
+ execute: true
305
+ });
306
+ return genericResult(input, { ...input.observed.live, workflowSync: sync });
307
+ }
308
+ return genericResult(input);
309
+ },
310
+ verify(input) {
311
+ const workflows = Array.isArray(input.observed.live.workflows) ? input.observed.live.workflows : [];
312
+ const checks = workflows.map((workflow) => verificationCheck(
313
+ `workflow:${workflow.workflow ?? "unknown"}`,
314
+ `Workflow ${workflow.workflow ?? "unknown"} matches the rendered template`,
315
+ "sdk",
316
+ {
317
+ exists: workflow.exists === true,
318
+ configured: workflow.changed !== true,
319
+ ready: workflow.exists === true && workflow.changed !== true,
320
+ verified: workflow.exists === true && workflow.changed !== true,
321
+ expected: "rendered-template",
322
+ observed: workflow,
323
+ issues: [
324
+ ...workflow.exists ? [] : ["workflow file is missing"],
325
+ ...workflow.changed ? ["workflow file has template drift"] : []
326
+ ]
327
+ }
328
+ ));
329
+ return summarizeVerification(input.unit.unitId, checks.length > 0 ? checks : [
330
+ verificationCheck("package-workflow", "Package workflow resource is observable", "sdk", {
331
+ exists: input.observed.exists,
332
+ configured: input.observed.exists,
333
+ ready: input.observed.exists,
334
+ verified: input.observed.exists
335
+ })
336
+ ], input.observed.warnings);
337
+ }
338
+ };
339
+ }
340
+ function buildGraphOnlyAdapter(providerId, unitTypes, description) {
341
+ return {
342
+ providerId,
343
+ unitTypes,
344
+ supports(unitType, candidateProviderId) {
345
+ return candidateProviderId === providerId && unitTypes.includes(unitType);
346
+ },
347
+ refresh(input) {
348
+ return genericObservedState(input);
349
+ },
350
+ diff() {
351
+ return noopDiff();
352
+ },
353
+ apply(input) {
354
+ return genericResult(input);
355
+ },
356
+ verify(input) {
357
+ return genericVerification(input, input.observed, description);
358
+ },
359
+ destroy(input) {
360
+ return genericResult({
361
+ ...input,
362
+ diff: { action: "delete", reasons: ["selected for destroy"], before: input.observed.live, after: {} }
363
+ });
364
+ }
365
+ };
366
+ }
367
+ function buildGitHubEnv(input) {
368
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
369
+ const resolvedScope = scope === "local" ? "staging" : scope;
370
+ const configuredValues = resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, resolvedScope);
371
+ const values = {
372
+ ...normalizeEnvironmentValues(configuredValues),
373
+ ...resolveReconcileEnvironmentValues(input, resolvedScope)
374
+ };
375
+ const providerValues = withDockerhubServiceCredentialEnv(values);
376
+ const repository = typeof input.unit.spec.repository === "string" ? input.unit.spec.repository : null;
377
+ if (!repository) return providerValues;
378
+ const credential = resolveGitHubCredentialForRepository(repository, { values: providerValues, env: providerValues });
379
+ return credential.token ? { ...providerValues, TREESEED_GITHUB_TOKEN: credential.token, GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token } : providerValues;
380
+ }
381
+ function repositoryFromUnit(input) {
382
+ const repository = input.unit.spec.repository;
383
+ if (typeof repository !== "string" || !repository.trim()) {
384
+ throw new Error(`${input.unit.unitId} requires a GitHub repository.`);
385
+ }
386
+ return repository.trim();
387
+ }
388
+ function workflowName(value, fallback) {
389
+ return (typeof value === "string" && value.trim() ? value.trim() : fallback).replace(/^\.github\/workflows\//u, "");
390
+ }
391
+ function environmentFromUnit(input) {
392
+ const environment = input.unit.spec.environment;
393
+ if (typeof environment !== "string" || !environment.trim()) {
394
+ throw new Error(`${input.unit.unitId} requires a GitHub environment.`);
395
+ }
396
+ return environment.trim();
397
+ }
398
+ function buildGitHubEnvironmentAdapter() {
399
+ return {
400
+ providerId: "github",
401
+ unitTypes: ["github-environment"],
402
+ supports(unitType, providerId) {
403
+ return unitType === "github-environment" && providerId === "github";
404
+ },
405
+ async refresh(input) {
406
+ const repository = repositoryFromUnit(input);
407
+ const environment = environmentFromUnit(input);
408
+ const observed = await observeGitHubEnvironment(repository, environment, buildGitHubEnv(input));
409
+ const warnings = observed.exists ? [] : [String(observed.error ?? "GitHub environment is missing")];
410
+ return {
411
+ ...genericObservedState(input, observed.exists, warnings),
412
+ status: observed.exists ? "ready" : "pending",
413
+ live: observed
414
+ };
415
+ },
416
+ diff(input) {
417
+ if (input.observed.live?.authAvailable === false) {
418
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
419
+ }
420
+ return input.observed.exists ? noopDiff() : { action: "create", reasons: ["GitHub environment is missing"], before: input.observed.live, after: input.unit.spec };
421
+ },
422
+ async apply(input) {
423
+ if (input.diff.action !== "noop") {
424
+ const result = await ensureReconcileGitHubEnvironment(repositoryFromUnit(input), environmentFromUnit(input), typeof input.unit.spec.branch === "string" ? input.unit.spec.branch : null, buildGitHubEnv(input));
425
+ return genericResult(input, { ...input.observed.live, result });
426
+ }
427
+ return genericResult(input);
428
+ },
429
+ verify(input) {
430
+ return genericVerification(input, input.observed, "GitHub environment exists");
431
+ }
432
+ };
433
+ }
434
+ function buildGitHubBindingAdapter(unitType) {
435
+ return {
436
+ providerId: "github",
437
+ unitTypes: [unitType],
438
+ supports(candidateUnitType, providerId) {
439
+ return candidateUnitType === unitType && providerId === "github";
440
+ },
441
+ async refresh(input) {
442
+ const repository = repositoryFromUnit(input);
443
+ const environment = environmentFromUnit(input);
444
+ const nameKey = unitType === "github-secret-binding" ? "secretName" : "variableName";
445
+ const name = String(input.unit.spec[nameKey] ?? input.unit.spec.envName ?? "");
446
+ const observed = await observeGitHubEnvironment(repository, environment, buildGitHubEnv(input));
447
+ const names = unitType === "github-secret-binding" ? observed.secretNames : observed.variableNames;
448
+ const exists = observed.exists && names.includes(name);
449
+ const warnings = observed.authAvailable === false ? [String(observed.error ?? "GitHub authentication is unavailable")] : observed.exists ? [] : [`GitHub environment ${environment} is missing`];
450
+ return {
451
+ ...genericObservedState(input, exists, warnings),
452
+ status: exists ? "ready" : "pending",
453
+ live: { repository, environment, name, exists, observed }
454
+ };
455
+ },
456
+ diff(input) {
457
+ const name = String(input.observed.live.name ?? "");
458
+ if (input.observed.live?.observed?.authAvailable === false) {
459
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
460
+ }
461
+ if (input.observed.exists) {
462
+ return noopDiff();
463
+ }
464
+ const value = buildGitHubEnv(input)[name];
465
+ if (!value) {
466
+ return { action: "blocked", reasons: [`Missing local value for ${name}`], before: input.observed.live, after: input.unit.spec };
467
+ }
468
+ return { action: "update", reasons: [`GitHub ${unitType === "github-secret-binding" ? "secret" : "variable"} ${name} is missing`], before: input.observed.live, after: input.unit.spec };
469
+ },
470
+ async apply(input) {
471
+ if (input.diff.action === "noop" || input.diff.action === "blocked") return genericResult(input);
472
+ const name = String(input.observed.live.name ?? input.unit.spec.envName ?? "");
473
+ const value = buildGitHubEnv(input)[name];
474
+ if (!value) throw new Error(`Missing local value for ${name}`);
475
+ const result = unitType === "github-secret-binding" ? await upsertReconcileGitHubSecret(repositoryFromUnit(input), environmentFromUnit(input), name, value, buildGitHubEnv(input)) : await upsertReconcileGitHubVariable(repositoryFromUnit(input), environmentFromUnit(input), name, value, buildGitHubEnv(input));
476
+ return genericResult(input, { ...input.observed.live, result });
477
+ },
478
+ verify(input) {
479
+ return genericVerification(input, input.observed, `GitHub ${unitType === "github-secret-binding" ? "secret" : "variable"} binding exists`);
480
+ }
481
+ };
482
+ }
483
+ function buildGitHubWorkflowDispatchAdapter() {
484
+ return {
485
+ providerId: "github",
486
+ unitTypes: ["github-workflow-dispatch"],
487
+ supports(unitType, providerId) {
488
+ return unitType === "github-workflow-dispatch" && providerId === "github";
489
+ },
490
+ async refresh(input) {
491
+ const repository = repositoryFromUnit(input);
492
+ const workflow = workflowName(input.unit.spec.workflow, "publish.yml");
493
+ const branch = typeof input.unit.spec.branch === "string" ? input.unit.spec.branch : null;
494
+ const latest = await observeGitHubWorkflowRun({ repository, workflow, branch, env: buildGitHubEnv(input) });
495
+ const authBlocked = Boolean(latest && typeof latest === "object" && "authAvailable" in latest && latest.authAvailable === false);
496
+ return {
497
+ ...genericObservedState(input, Boolean(latest) && !authBlocked, authBlocked ? [String(latest.error ?? "GitHub authentication is unavailable")] : []),
498
+ status: latest && !authBlocked ? "ready" : "pending",
499
+ live: { repository, workflow, branch, latest }
500
+ };
501
+ },
502
+ diff(input) {
503
+ if (input.observed.live?.latest?.authAvailable === false) {
504
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
505
+ }
506
+ const latest = input.observed.live?.latest;
507
+ const conclusion = typeof latest?.conclusion === "string" ? latest.conclusion : null;
508
+ const status = typeof latest?.status === "string" ? latest.status : null;
509
+ const expectedHeadSha = typeof input.unit.spec.expectedHeadSha === "string" ? input.unit.spec.expectedHeadSha : null;
510
+ const observedHeadSha = typeof latest?.headSha === "string" ? latest.headSha : typeof latest?.head_sha === "string" ? latest.head_sha : null;
511
+ if (!input.observed.exists) {
512
+ return { action: "create", reasons: ["workflow dispatch has no observed run"], before: input.observed.live, after: input.unit.spec };
513
+ }
514
+ if (expectedHeadSha && observedHeadSha && observedHeadSha !== expectedHeadSha) {
515
+ return { action: "create", reasons: [`latest workflow run is for ${observedHeadSha}, expected ${expectedHeadSha}`], before: input.observed.live, after: input.unit.spec };
516
+ }
517
+ if (conclusion && conclusion !== "success") {
518
+ return { action: "create", reasons: [`latest workflow run concluded ${conclusion}`], before: input.observed.live, after: input.unit.spec };
519
+ }
520
+ if (status && status !== "completed" && input.unit.spec.wait === true) {
521
+ return { action: "create", reasons: [`latest workflow run is still ${status}`], before: input.observed.live, after: input.unit.spec };
522
+ }
523
+ return noopDiff();
524
+ },
525
+ async apply(input) {
526
+ if (input.diff.action === "noop") return genericResult(input);
527
+ const result = await dispatchReconcileGitHubWorkflow({
528
+ repository: repositoryFromUnit(input),
529
+ workflow: workflowName(input.unit.spec.workflow, "publish.yml"),
530
+ branch: String(input.unit.spec.branch ?? "staging"),
531
+ inputs: typeof input.unit.spec.inputs === "object" && input.unit.spec.inputs ? input.unit.spec.inputs : {},
532
+ wait: input.unit.spec.wait === true,
533
+ timeoutMs: typeof input.unit.spec.timeoutMs === "number" ? input.unit.spec.timeoutMs : void 0,
534
+ env: buildGitHubEnv(input)
535
+ });
536
+ return genericResult(input, { ...input.observed.live, result });
537
+ },
538
+ verify(input) {
539
+ const latest = input.observed.live?.latest;
540
+ const result = input.result?.state?.result && typeof input.result.state.result === "object" ? input.result.state.result : null;
541
+ const observedRun = result?.runId ? result : latest;
542
+ const expectedHeadSha = typeof input.unit.spec.expectedHeadSha === "string" ? input.unit.spec.expectedHeadSha : null;
543
+ const observedHeadSha = typeof observedRun?.headSha === "string" ? observedRun.headSha : typeof observedRun?.head_sha === "string" ? observedRun.head_sha : null;
544
+ const status = typeof observedRun?.status === "string" ? observedRun.status : null;
545
+ const conclusion = typeof observedRun?.conclusion === "string" ? observedRun.conclusion : null;
546
+ const wait = input.unit.spec.wait === true;
547
+ const issues = [
548
+ ...expectedHeadSha && observedHeadSha && observedHeadSha !== expectedHeadSha ? [`latest workflow run is for ${observedHeadSha}, expected ${expectedHeadSha}`] : [],
549
+ ...wait && status && status !== "completed" ? [`workflow run is still ${status}`] : [],
550
+ ...wait && status === "completed" && conclusion !== "success" ? [`workflow run concluded ${conclusion ?? "unknown"}`] : []
551
+ ];
552
+ return summarizeVerification(input.unit.unitId, [
553
+ verificationCheck("github.workflow-run", "GitHub workflow dispatch produced a successful observed run", "api", {
554
+ exists: Boolean(observedRun),
555
+ configured: !expectedHeadSha || !observedHeadSha || observedHeadSha === expectedHeadSha,
556
+ ready: !wait || status === "completed",
557
+ verified: Boolean(observedRun) && issues.length === 0,
558
+ expected: {
559
+ workflow: input.unit.spec.workflow,
560
+ branch: input.unit.spec.branch,
561
+ headSha: expectedHeadSha,
562
+ wait,
563
+ conclusion: wait ? "success" : void 0
564
+ },
565
+ observed: observedRun,
566
+ issues
567
+ })
568
+ ], input.observed.warnings);
569
+ }
570
+ };
571
+ }
572
+ function buildPackageImageAdapter() {
573
+ return {
574
+ providerId: "dockerhub",
575
+ unitTypes: ["package-image"],
576
+ supports(unitType, providerId) {
577
+ return unitType === "package-image" && providerId === "dockerhub";
578
+ },
579
+ async refresh(input) {
580
+ const env = buildGitHubEnv(input);
581
+ const repository = typeof input.unit.spec.repository === "string" ? input.unit.spec.repository : null;
582
+ const environment = typeof input.unit.spec.environment === "string" ? input.unit.spec.environment : "staging";
583
+ const requiredSecrets = Array.isArray(input.unit.spec.requiredSecrets) ? input.unit.spec.requiredSecrets.map((entry) => String(entry)).filter(Boolean) : ["TREESEED_DOCKERHUB_TOKEN"];
584
+ const requiredVariables = Array.isArray(input.unit.spec.requiredVariables) ? input.unit.spec.requiredVariables.map((entry) => String(entry)).filter(Boolean) : ["TREESEED_DOCKERHUB_USERNAME"];
585
+ const localMissingSecrets = requiredSecrets.filter((name) => !env[name]);
586
+ const localMissingVariables = requiredVariables.filter((name) => !env[name]);
587
+ let remote = null;
588
+ if ((localMissingSecrets.length > 0 || localMissingVariables.length > 0) && repository) {
589
+ remote = await observeGitHubEnvironment(repository, environment, env);
590
+ }
591
+ const remoteSecretNames = new Set(remote?.secretNames ?? []);
592
+ const remoteVariableNames = new Set(remote?.variableNames ?? []);
593
+ const missingSecrets = localMissingSecrets.filter((name) => !remoteSecretNames.has(name));
594
+ const missingVariables = localMissingVariables.filter((name) => !remoteVariableNames.has(name));
595
+ const usernameConfigured = missingVariables.length === 0;
596
+ const tokenConfigured = missingSecrets.length === 0;
597
+ return {
598
+ ...genericObservedState(input, usernameConfigured && tokenConfigured, [
599
+ ...missingVariables.map((name) => `${name} is not configured`),
600
+ ...missingSecrets.map((name) => `${name} is not configured`)
601
+ ]),
602
+ status: usernameConfigured && tokenConfigured ? "ready" : "pending",
603
+ live: {
604
+ ...input.unit.spec,
605
+ dockerHub: {
606
+ usernameConfigured,
607
+ tokenConfigured,
608
+ source: localMissingSecrets.length === 0 && localMissingVariables.length === 0 ? "local-env" : "github-environment",
609
+ environment,
610
+ repository
611
+ }
612
+ }
613
+ };
614
+ },
615
+ diff(input) {
616
+ return input.observed.exists ? noopDiff() : { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
617
+ },
618
+ apply(input) {
619
+ return genericResult(input);
620
+ },
621
+ verify(input) {
622
+ return genericVerification(input, input.observed, "Docker Hub credentials for package image publication are configured");
623
+ }
624
+ };
625
+ }
626
+ function buildDockerImageBuildAdapter() {
627
+ return {
628
+ providerId: "docker",
629
+ unitTypes: ["docker-image-build"],
630
+ supports(unitType, providerId) {
631
+ return unitType === "docker-image-build" && providerId === "docker";
632
+ },
633
+ refresh(input) {
634
+ const availability = inspectDockerAvailability();
635
+ const tags = Array.isArray(input.unit.spec.tags) ? input.unit.spec.tags.filter((entry) => typeof entry === "string") : [];
636
+ const inspectedImages = Object.fromEntries(tags.map((tag) => [tag, inspectDockerImage(tag)]));
637
+ const inspectedManifests = Object.fromEntries(tags.map((tag) => [tag, inspectDockerManifest(tag)]));
638
+ const exists = availability.available && availability.buildxAvailable;
639
+ return {
640
+ ...genericObservedState(input, exists, availability.warnings),
641
+ status: !exists ? "error" : tags.some((tag) => inspectedImages[tag] || inspectedManifests[tag]) ? "ready" : "pending",
642
+ live: {
643
+ ...input.unit.spec,
644
+ docker: availability,
645
+ tags,
646
+ inspectedImages,
647
+ inspectedManifests
648
+ }
649
+ };
650
+ },
651
+ diff(input) {
652
+ if (!input.observed.exists) {
653
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
654
+ }
655
+ const tags = Array.isArray(input.unit.spec.tags) ? input.unit.spec.tags.filter((entry) => typeof entry === "string") : [];
656
+ const inspectedImages = input.observed.live.inspectedImages;
657
+ const inspectedManifests = input.observed.live.inspectedManifests;
658
+ const missing = tags.filter((tag) => !inspectedImages?.[tag] && !inspectedManifests?.[tag]);
659
+ return missing.length > 0 ? { action: "create", reasons: [`missing docker image tags: ${missing.join(", ")}`], before: input.observed.live, after: input.unit.spec } : noopDiff();
660
+ },
661
+ apply(input) {
662
+ if (input.diff.action === "blocked" || input.diff.action === "noop") return genericResult(input);
663
+ const platforms = Array.isArray(input.unit.spec.platforms) ? input.unit.spec.platforms.filter((entry) => typeof entry === "string") : ["linux/amd64"];
664
+ const tags = Array.isArray(input.unit.spec.tags) ? input.unit.spec.tags.filter((entry) => typeof entry === "string") : [];
665
+ runDockerImagePrepareCommand(input);
666
+ const result = buildDockerImage({
667
+ tenantRoot: input.context.tenantRoot,
668
+ packageRoot: String(input.unit.spec.packageRoot ?? input.context.tenantRoot),
669
+ context: String(input.unit.spec.context ?? "."),
670
+ dockerfile: String(input.unit.spec.dockerfile ?? "Dockerfile"),
671
+ target: typeof input.unit.spec.target === "string" ? input.unit.spec.target : null,
672
+ platforms,
673
+ tags,
674
+ labels: typeof input.unit.spec.labels === "object" && input.unit.spec.labels ? input.unit.spec.labels : {},
675
+ buildArgs: typeof input.unit.spec.buildArgs === "object" && input.unit.spec.buildArgs ? input.unit.spec.buildArgs : {},
676
+ push: input.unit.spec.push === true,
677
+ load: input.unit.spec.load !== false,
678
+ provenance: input.unit.spec.provenance === false ? false : void 0,
679
+ env: input.context.launchEnv
680
+ });
681
+ return genericResult(input, { ...input.observed.live, build: result });
682
+ },
683
+ verify(input) {
684
+ const tags = Array.isArray(input.unit.spec.tags) ? input.unit.spec.tags.filter((entry) => typeof entry === "string") : [];
685
+ const checks = tags.map((tag) => {
686
+ const image = inspectDockerImage(tag);
687
+ const manifest = inspectDockerManifest(tag);
688
+ const ok = Boolean(image || manifest);
689
+ return verificationCheck(`docker-image:${tag}`, `Docker image ${tag} exists locally or as an inspectable manifest`, "cli", {
690
+ exists: ok,
691
+ configured: input.observed.exists,
692
+ ready: ok,
693
+ verified: ok,
694
+ observed: image ?? manifest,
695
+ issues: ok ? [] : [`Docker image ${tag} was not found after build.`]
696
+ });
697
+ });
698
+ return summarizeVerification(input.unit.unitId, checks.length > 0 ? checks : [
699
+ verificationCheck("docker", "Docker daemon and Buildx are available", "cli", {
700
+ exists: input.observed.exists,
701
+ configured: input.observed.exists,
702
+ ready: input.observed.exists,
703
+ verified: input.observed.exists
704
+ })
705
+ ], input.observed.warnings);
706
+ }
707
+ };
708
+ }
709
+ function runDockerImagePrepareCommand(input) {
710
+ const prepareCommand = input.unit.spec.prepareCommand;
711
+ if (!prepareCommand || typeof prepareCommand !== "object") return;
712
+ const command = typeof prepareCommand.command === "string" ? String(prepareCommand.command) : null;
713
+ const rawArgs = prepareCommand.args;
714
+ const args = Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string") : [];
715
+ if (!command) return;
716
+ const packageRoot = resolve(input.context.tenantRoot, String(input.unit.spec.packageRoot ?? "."));
717
+ const result = spawnSync(command, args, {
718
+ cwd: packageRoot,
719
+ env: { ...process.env, ...input.context.launchEnv ?? {} },
720
+ encoding: "utf8",
721
+ stdio: "pipe"
722
+ });
723
+ if (result.status !== 0) {
724
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `${command} ${args.join(" ")} failed`);
725
+ }
726
+ }
727
+ function buildLocalDockerComposeAdapter() {
728
+ return {
729
+ providerId: "local",
730
+ unitTypes: ["local-docker-compose"],
731
+ supports(unitType, providerId) {
732
+ return unitType === "local-docker-compose" && providerId === "local";
733
+ },
734
+ refresh(input) {
735
+ const composeFiles = resolveLocalComposeFiles(input);
736
+ const composeFilesExist = composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile));
737
+ const cwd = resolve(input.context.tenantRoot, String(input.unit.spec.cwd ?? "."));
738
+ const docker = inspectDockerAvailability();
739
+ const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
740
+ const env = buildLocalComposeLaunchEnv(input);
741
+ const profiles = localComposeProfiles(input);
742
+ const buildPolicy = localComposeBuildPolicy(input);
743
+ const ps = composeFilesExist && docker.available ? runDockerCompose({ composeFiles, projectName, cwd, env, profiles, buildPolicy, action: "ps" }) : null;
744
+ const hasContainers = Boolean(ps?.ok && ps.stdout.trim());
745
+ const config = composeFilesExist && docker.available ? runDockerCompose({ composeFiles, projectName, cwd, env, profiles, buildPolicy, action: "config" }) : null;
746
+ const missingComposeFiles = composeFiles.filter((composeFile) => !existsSync(composeFile));
747
+ return {
748
+ ...genericObservedState(input, Boolean(composeFilesExist && docker.available), [
749
+ ...composeFiles.length > 0 ? [] : ["Compose file is missing: (unset)"],
750
+ ...missingComposeFiles.map((composeFile) => `Compose file is missing: ${composeFile}`),
751
+ ...docker.warnings
752
+ ]),
753
+ status: hasContainers ? "ready" : composeFilesExist && docker.available ? "pending" : "error",
754
+ live: {
755
+ ...input.unit.spec,
756
+ composeFile: composeFiles[0] ?? null,
757
+ composeFiles,
758
+ projectName,
759
+ cwd,
760
+ profiles,
761
+ buildPolicy,
762
+ docker,
763
+ ps,
764
+ hasContainers,
765
+ configHash: config?.stdout?.trim() || null
766
+ }
767
+ };
768
+ },
769
+ diff(input) {
770
+ if (!input.observed.exists) return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
771
+ return input.observed.status === "ready" ? noopDiff() : { action: "create", reasons: ["compose services are not running"], before: input.observed.live, after: input.unit.spec };
772
+ },
773
+ apply(input) {
774
+ if (input.diff.action === "blocked" || input.diff.action === "noop") return genericResult(input);
775
+ runLocalComposePrepareCommand(input);
776
+ ensureLocalComposeDataDir(input);
777
+ const composeFiles = observedOrResolvedComposeFiles(input);
778
+ const cwd = String(input.observed.live.cwd ?? input.context.tenantRoot);
779
+ const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
780
+ const env = buildLocalComposeLaunchEnv(input);
781
+ const result = runDockerCompose({
782
+ composeFiles,
783
+ projectName,
784
+ cwd,
785
+ env,
786
+ profiles: localComposeProfiles(input),
787
+ buildPolicy: localComposeBuildPolicy(input),
788
+ action: "up"
789
+ });
790
+ if (!result.ok) {
791
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "docker compose up failed");
792
+ }
793
+ return genericResult(input, { ...input.observed.live, compose: result });
794
+ },
795
+ async verify(input) {
796
+ const healthChecks = Array.isArray(input.unit.spec.healthChecks) ? input.unit.spec.healthChecks : [];
797
+ const checks = [];
798
+ for (const healthCheck of healthChecks) {
799
+ if (healthCheck.kind === "http" && typeof healthCheck.url === "string") {
800
+ const health = await checkHttpHealthWithRetry(healthCheck.url);
801
+ checks.push(verificationCheck(String(healthCheck.id ?? healthCheck.url), `HTTP health ${healthCheck.url}`, "api", {
802
+ exists: health.ok,
803
+ configured: true,
804
+ ready: health.ok,
805
+ verified: health.ok,
806
+ observed: health,
807
+ issues: health.ok ? [] : [`Health endpoint ${healthCheck.url} did not respond successfully.`]
808
+ }));
809
+ }
810
+ }
811
+ if (checks.length === 0) {
812
+ checks.push(verificationCheck("compose", "Docker Compose project is observable", "cli", {
813
+ exists: input.observed.exists,
814
+ configured: input.observed.exists,
815
+ ready: input.observed.status === "ready",
816
+ verified: input.observed.exists && input.observed.status !== "error",
817
+ observed: input.observed.live
818
+ }));
819
+ }
820
+ return summarizeVerification(input.unit.unitId, checks, input.observed.warnings);
821
+ },
822
+ destroy(input) {
823
+ const composeFiles = observedOrResolvedComposeFiles(input);
824
+ const cwd = String(input.observed.live.cwd ?? input.context.tenantRoot);
825
+ const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
826
+ if (input.context.dryRun !== true && composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile))) {
827
+ const result = runDockerCompose({
828
+ composeFiles,
829
+ projectName,
830
+ cwd,
831
+ env: buildLocalComposeLaunchEnv(input),
832
+ profiles: localComposeProfiles(input),
833
+ buildPolicy: localComposeBuildPolicy(input),
834
+ action: "down"
835
+ });
836
+ if (!result.ok) {
837
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "docker compose down failed");
838
+ }
839
+ }
840
+ return genericResult({
841
+ ...input,
842
+ diff: { action: "delete", reasons: ["selected local compose project for destroy"], before: input.observed.live, after: {} }
843
+ });
844
+ }
845
+ };
846
+ }
847
+ function ensureLocalComposeDataDir(input) {
848
+ const dataDir = typeof input.unit.spec.dataDir === "string" ? input.unit.spec.dataDir.trim() : "";
849
+ if (!dataDir) return;
850
+ mkdirSync(resolve(input.context.tenantRoot, dataDir), { recursive: true });
851
+ }
852
+ function resolveLocalComposeFiles(input) {
853
+ const rawComposeFiles = Array.isArray(input.unit.spec.composeFiles) ? input.unit.spec.composeFiles : typeof input.unit.spec.composeFile === "string" ? [input.unit.spec.composeFile] : [];
854
+ return rawComposeFiles.filter((composeFile) => typeof composeFile === "string" && composeFile.trim().length > 0).map((composeFile) => resolve(input.context.tenantRoot, composeFile));
855
+ }
856
+ function observedOrResolvedComposeFiles(input) {
857
+ const observed = input.observed.live.composeFiles;
858
+ if (Array.isArray(observed)) {
859
+ return observed.filter((composeFile) => typeof composeFile === "string" && composeFile.trim().length > 0);
860
+ }
861
+ return resolveLocalComposeFiles(input);
862
+ }
863
+ function localComposeProfiles(input) {
864
+ return Array.isArray(input.unit.spec.profiles) ? input.unit.spec.profiles.filter((profile) => typeof profile === "string" && profile.trim().length > 0) : [];
865
+ }
866
+ function localComposeBuildPolicy(input) {
867
+ return input.unit.spec.buildPolicy === "never" || input.unit.spec.buildPolicy === "always" ? input.unit.spec.buildPolicy : "missing";
868
+ }
869
+ function runLocalComposePrepareCommand(input) {
870
+ const prepareCommand = input.unit.spec.prepareCommand;
871
+ if (!prepareCommand || typeof prepareCommand !== "object") return;
872
+ const command = typeof prepareCommand.command === "string" ? String(prepareCommand.command) : null;
873
+ const rawArgs = prepareCommand.args;
874
+ const args = Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string") : [];
875
+ if (!command) return;
876
+ const result = spawnSync(command, args, {
877
+ cwd: input.context.tenantRoot,
878
+ env: buildLocalComposeLaunchEnv(input),
879
+ encoding: "utf8",
880
+ stdio: "pipe"
881
+ });
882
+ if (result.status !== 0) {
883
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `${command} ${args.join(" ")} failed`);
884
+ }
885
+ }
886
+ async function checkHttpHealthWithRetry(url, attempts = 90, intervalMs = 2e3) {
887
+ let last = await checkHttpHealth(url);
888
+ for (let attempt = 1; attempt < attempts && !last.ok; attempt += 1) {
889
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
890
+ last = await checkHttpHealth(url);
891
+ }
892
+ return last;
893
+ }
894
+ function buildLocalComposeLaunchEnv(input) {
895
+ const fallback = resolveCapacityProviderLaunchPlan({
896
+ schemaVersion: 1,
897
+ provider: {
898
+ dataDir: ".treeseed/local-capacity-provider/data",
899
+ environment: "local"
900
+ },
901
+ runtime: {
902
+ images: {
903
+ roles: {
904
+ manager: { image: "treeseed/agent-manager", tag: "latest" },
905
+ runner: { image: "treeseed/agent-runner", tag: "latest" }
906
+ }
907
+ },
908
+ api: {
909
+ hostPort: 4783
910
+ }
911
+ }
912
+ }).composeEnv;
913
+ return {
914
+ ...input.context.launchEnv,
915
+ ...fallback,
916
+ ...typeof input.unit.spec.env === "object" && input.unit.spec.env ? input.unit.spec.env : {}
917
+ };
918
+ }
919
+ function localContentSpecString(input, key) {
920
+ const value = input.unit.spec[key];
921
+ return typeof value === "string" && value.trim() ? value.trim() : null;
922
+ }
923
+ function localContentSpecRecord(input, key) {
924
+ const value = input.unit.spec[key];
925
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
926
+ }
927
+ function localContentGitEnvironment(input) {
928
+ const repo = localContentSpecRecord(input, "contentRepository");
929
+ const owner = typeof repo.owner === "string" ? repo.owner : "";
930
+ const name = typeof repo.name === "string" ? repo.name : "";
931
+ if (!owner || !name) return { env: input.context.launchEnv, credential: null };
932
+ const credential = resolveGitHubCredentialForRepository(`${owner}/${name}`, { env: input.context.launchEnv });
933
+ const env = credential.token ? {
934
+ ...input.context.launchEnv,
935
+ TREESEED_GITHUB_TOKEN: credential.token,
936
+ GH_TOKEN: credential.token,
937
+ GITHUB_TOKEN: credential.token
938
+ } : input.context.launchEnv;
939
+ return { env, credential };
940
+ }
941
+ function localContentPathInsideTenant(input, targetPath) {
942
+ if (!targetPath) return true;
943
+ const resolvedTarget = resolve(targetPath);
944
+ const resolvedTenant = resolve(input.context.tenantRoot);
945
+ return resolvedTarget === resolvedTenant || resolvedTarget.startsWith(`${resolvedTenant}/`);
946
+ }
947
+ function inspectLocalContentGit(targetPath) {
948
+ const origin = runTreeseedGit(["remote", "get-url", "origin"], { cwd: targetPath, mode: "read", allowFailure: true }).stdout.trim();
949
+ const branch = runTreeseedGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: targetPath, mode: "read", allowFailure: true }).stdout.trim();
950
+ const status = runTreeseedGit(["status", "--porcelain"], { cwd: targetPath, mode: "read", allowFailure: true }).stdout.trim();
951
+ return {
952
+ isGit: Boolean(runTreeseedGit(["rev-parse", "--is-inside-work-tree"], { cwd: targetPath, mode: "read", allowFailure: true }).stdout.trim()),
953
+ origin: origin || null,
954
+ branch: branch || null,
955
+ dirty: status.length > 0
956
+ };
957
+ }
958
+ function localContentObservedState(input) {
959
+ const targetPath = localContentSpecString(input, "effectiveLocalPath");
960
+ const materialization = localContentSpecString(input, "localContentMaterialization") ?? "none";
961
+ const executeRequested = input.unit.spec.executeRequested === true;
962
+ const warnings = [];
963
+ if (!localContentPathInsideTenant(input, targetPath)) {
964
+ warnings.push("local content target path is outside the Treeseed workspace");
965
+ return {
966
+ ...genericObservedState(input, false, warnings),
967
+ status: "error",
968
+ live: {
969
+ ...input.unit.spec,
970
+ targetPath,
971
+ materializationStatus: "blocked"
972
+ }
973
+ };
974
+ }
975
+ if (materialization === "none" || !targetPath) {
976
+ return {
977
+ ...genericObservedState(input, true, warnings),
978
+ live: {
979
+ ...input.unit.spec,
980
+ materializationStatus: "not_requested"
981
+ }
982
+ };
983
+ }
984
+ const exists = existsSync(targetPath);
985
+ const stat = exists ? statSync(targetPath) : null;
986
+ if (exists && !stat?.isDirectory()) {
987
+ warnings.push("local content target path exists but is not a directory");
988
+ return {
989
+ ...genericObservedState(input, false, warnings),
990
+ status: "error",
991
+ live: {
992
+ ...input.unit.spec,
993
+ targetPath,
994
+ materializationStatus: "blocked"
995
+ }
996
+ };
997
+ }
998
+ const git = exists ? inspectLocalContentGit(targetPath) : null;
999
+ if (exists && materialization === "managed_clone" && !git?.isGit) {
1000
+ warnings.push("managed local content target exists but is not a Git worktree");
1001
+ return {
1002
+ ...genericObservedState(input, false, warnings),
1003
+ status: "error",
1004
+ live: {
1005
+ ...input.unit.spec,
1006
+ targetPath,
1007
+ materializationStatus: "blocked",
1008
+ git
1009
+ }
1010
+ };
1011
+ }
1012
+ const status = exists ? materialization === "managed_clone" ? "managed_clone_ready" : materialization === "submodule" ? "submodule_ready" : "existing_path_ready" : materialization === "managed_clone" ? "managed_clone_missing" : materialization === "submodule" ? "submodule_missing" : "existing_path_missing";
1013
+ if (git?.dirty) {
1014
+ warnings.push("local content Git worktree has uncommitted changes; Treeseed will not reset or overwrite it");
1015
+ }
1016
+ return {
1017
+ ...genericObservedState(input, exists || !executeRequested, warnings),
1018
+ status: exists || !executeRequested ? "ready" : "pending",
1019
+ live: {
1020
+ ...input.unit.spec,
1021
+ targetPath,
1022
+ materializationStatus: status,
1023
+ git
1024
+ },
1025
+ locators: {
1026
+ unitId: input.unit.unitId,
1027
+ targetPath,
1028
+ origin: git?.origin ?? null
1029
+ }
1030
+ };
1031
+ }
1032
+ function expectedLocalContentOrigin(input) {
1033
+ const repo = localContentSpecRecord(input, "contentRepository");
1034
+ return typeof repo.gitUrl === "string" ? repo.gitUrl : null;
1035
+ }
1036
+ function originMatches(observed, expected) {
1037
+ const liveGit = observed.live.git && typeof observed.live.git === "object" ? observed.live.git : null;
1038
+ const origin = typeof liveGit?.origin === "string" ? liveGit.origin : null;
1039
+ return !origin || !expected || origin === expected || origin.replace(/\.git$/u, "") === expected.replace(/\.git$/u, "");
1040
+ }
1041
+ function githubRepoSlugFromUrl(url) {
1042
+ if (!url) return null;
1043
+ if (!/^(https?:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)/iu.test(url)) return null;
1044
+ const raw = url.replace(/^https?:\/\/github\.com\//iu, "").replace(/^git@github\.com:/iu, "").replace(/^ssh:\/\/git@github\.com\//iu, "").replace(/\.git$/iu, "").replace(/^\/+|\/+$/gu, "");
1045
+ const [owner, name, ...extra] = raw.split("/").filter(Boolean);
1046
+ return owner && name && extra.length === 0 ? `${owner}/${name}` : null;
1047
+ }
1048
+ function runLocalContentClone(input, targetPath, gitUrl, branch) {
1049
+ mkdirSync(dirname(targetPath), { recursive: true });
1050
+ const { env, credential } = localContentGitEnvironment(input);
1051
+ const repoSlug = githubRepoSlugFromUrl(gitUrl);
1052
+ if (repoSlug && credential?.configured) {
1053
+ const args2 = ["repo", "clone", repoSlug, targetPath, "--"];
1054
+ if (branch) args2.push("--branch", branch);
1055
+ const result2 = spawnSync("gh", args2, {
1056
+ cwd: input.context.tenantRoot,
1057
+ env,
1058
+ encoding: "utf8",
1059
+ stdio: "pipe",
1060
+ timeout: 12e4,
1061
+ maxBuffer: 1024 * 1024 * 16
1062
+ });
1063
+ if (result2.status !== 0) {
1064
+ throw new Error((result2.stderr || result2.stdout || "GitHub CLI clone failed. Run `npx trsd install --json` and configure TREESEED_GITHUB_TOKEN if this repository is private.").trim());
1065
+ }
1066
+ return {
1067
+ status: result2.status,
1068
+ stdout: result2.stdout ?? "",
1069
+ stderr: result2.stderr ?? "",
1070
+ tool: "gh",
1071
+ credentialEnvName: credential.envName,
1072
+ fallbackUsed: credential.fallbackUsed
1073
+ };
1074
+ }
1075
+ const args = ["clone"];
1076
+ if (branch) args.push("--branch", branch);
1077
+ args.push(gitUrl, targetPath);
1078
+ const result = runTreeseedGit(args, {
1079
+ cwd: input.context.tenantRoot,
1080
+ mode: "mutate",
1081
+ env,
1082
+ timeoutMs: 12e4,
1083
+ maxBuffer: 1024 * 1024 * 16
1084
+ });
1085
+ return {
1086
+ ...result,
1087
+ tool: "git",
1088
+ credentialEnvName: credential?.envName ?? null,
1089
+ fallbackUsed: credential?.fallbackUsed ?? false
1090
+ };
1091
+ }
1092
+ function buildLocalContentMaterializationAdapter() {
1093
+ return {
1094
+ providerId: "local",
1095
+ unitTypes: ["local-content-materialization"],
1096
+ supports(unitType, providerId) {
1097
+ return unitType === "local-content-materialization" && providerId === "local";
1098
+ },
1099
+ refresh(input) {
1100
+ return localContentObservedState(input);
1101
+ },
1102
+ diff(input) {
1103
+ const materialization = localContentSpecString(input, "localContentMaterialization") ?? "none";
1104
+ const executeRequested = input.unit.spec.executeRequested === true;
1105
+ const expectedOrigin = expectedLocalContentOrigin(input);
1106
+ if (input.observed.status === "error") {
1107
+ return { action: "blocked", reasons: input.observed.warnings, before: input.observed.live, after: input.unit.spec };
1108
+ }
1109
+ if (materialization === "existing_path" && !input.observed.exists) {
1110
+ return { action: "blocked", reasons: ["existing local content path is missing; choose preview/edit with managed_clone or configure the path"], before: input.observed.live, after: input.unit.spec };
1111
+ }
1112
+ if (input.observed.exists && !originMatches(input.observed, expectedOrigin)) {
1113
+ return { action: "blocked", reasons: ["managed local content origin does not match the configured content repository"], before: input.observed.live, after: input.unit.spec };
1114
+ }
1115
+ if (!executeRequested) {
1116
+ return noopDiff();
1117
+ }
1118
+ if (!input.observed.exists && (materialization === "managed_clone" || materialization === "submodule")) {
1119
+ return { action: "create", reasons: [`${materialization} local content is requested but missing`], before: input.observed.live, after: input.unit.spec };
1120
+ }
1121
+ if (input.observed.exists && materialization === "managed_clone") {
1122
+ return { action: "update", reasons: ["managed local content exists; fetch without resetting local work"], before: input.observed.live, after: input.unit.spec };
1123
+ }
1124
+ return noopDiff();
1125
+ },
1126
+ apply(input) {
1127
+ if (input.diff.action === "noop" || input.diff.action === "blocked") return genericResult(input);
1128
+ const materialization = localContentSpecString(input, "localContentMaterialization") ?? "none";
1129
+ const targetPath = localContentSpecString(input, "effectiveLocalPath");
1130
+ const expectedOrigin = expectedLocalContentOrigin(input);
1131
+ const repo = localContentSpecRecord(input, "contentRepository");
1132
+ const branch = typeof repo.defaultBranch === "string" ? repo.defaultBranch : null;
1133
+ if (!targetPath) return genericResult(input);
1134
+ if (materialization === "submodule") {
1135
+ const submodulePath = typeof repo.submodulePath === "string" ? repo.submodulePath : localContentSpecString(input, "contentPath");
1136
+ if (!submodulePath) return genericResult(input);
1137
+ const { env } = localContentGitEnvironment(input);
1138
+ const submodule = runTreeseedGit(["submodule", "update", "--init", "--", submodulePath], {
1139
+ cwd: input.context.tenantRoot,
1140
+ mode: "mutate",
1141
+ env,
1142
+ timeoutMs: 12e4,
1143
+ maxBuffer: 1024 * 1024 * 16
1144
+ });
1145
+ return genericResult(input, { ...input.observed.live, submodule: { status: submodule.status } });
1146
+ }
1147
+ if (materialization === "managed_clone" && expectedOrigin) {
1148
+ const result = input.observed.exists ? runTreeseedGit(["fetch", "origin"], {
1149
+ cwd: targetPath,
1150
+ mode: "mutate",
1151
+ env: localContentGitEnvironment(input).env,
1152
+ timeoutMs: 12e4,
1153
+ maxBuffer: 1024 * 1024 * 16
1154
+ }) : runLocalContentClone(input, targetPath, expectedOrigin, branch);
1155
+ return genericResult(input, {
1156
+ ...input.observed.live,
1157
+ materializedAt: nowIso(),
1158
+ gitOperation: {
1159
+ status: result.status,
1160
+ tool: "tool" in result ? result.tool : "git",
1161
+ credentialEnvName: "credentialEnvName" in result ? result.credentialEnvName : null,
1162
+ fallbackUsed: "fallbackUsed" in result ? result.fallbackUsed : false
1163
+ }
1164
+ });
1165
+ }
1166
+ return genericResult(input);
1167
+ },
1168
+ verify(input) {
1169
+ const targetPath = localContentSpecString(input, "effectiveLocalPath");
1170
+ const materialization = localContentSpecString(input, "localContentMaterialization") ?? "none";
1171
+ const exists = !targetPath || existsSync(targetPath);
1172
+ const expectedOrigin = expectedLocalContentOrigin(input);
1173
+ const originOk = originMatches(input.observed, expectedOrigin);
1174
+ const checks = [
1175
+ verificationCheck("local-content.path", "Local content path policy is satisfied", "sdk", {
1176
+ exists: materialization === "none" ? true : exists,
1177
+ configured: true,
1178
+ ready: materialization === "none" || exists,
1179
+ verified: materialization === "none" || exists,
1180
+ observed: targetPath,
1181
+ issues: materialization !== "none" && !exists ? ["Local content path is missing."] : []
1182
+ }),
1183
+ verificationCheck("local-content.origin", "Local content origin matches the configured repository when observable", "sdk", {
1184
+ exists: true,
1185
+ configured: originOk,
1186
+ ready: originOk,
1187
+ verified: originOk,
1188
+ expected: expectedOrigin,
1189
+ observed: input.observed.live.git,
1190
+ issues: originOk ? [] : ["Origin does not match configured content repository."]
1191
+ })
1192
+ ];
1193
+ return summarizeVerification(input.unit.unitId, checks, input.observed.warnings);
1194
+ }
1195
+ };
1196
+ }
1197
+ function buildLocalProcessAdapter() {
1198
+ return {
1199
+ providerId: "local",
1200
+ unitTypes: ["local-process"],
1201
+ supports(unitType, providerId) {
1202
+ return unitType === "local-process" && providerId === "local";
1203
+ },
1204
+ async refresh(input) {
1205
+ const surfaces = Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [String(input.unit.spec.processId ?? input.unit.logicalName)];
1206
+ const status = await runManagedDevAction({
1207
+ tenantRoot: input.context.tenantRoot,
1208
+ action: "status",
1209
+ surfaces,
1210
+ options: typeof input.unit.spec.options === "object" && input.unit.spec.options ? input.unit.spec.options : {},
1211
+ env: input.context.launchEnv
1212
+ });
1213
+ const safeStatus = sanitizeManagedDevObservation(status);
1214
+ return {
1215
+ ...genericObservedState(input, true, safeStatus.ok ? [] : [safeStatus.output || "managed dev status failed"]),
1216
+ status: safeStatus.ok ? "ready" : "pending",
1217
+ live: {
1218
+ ...input.unit.spec,
1219
+ status: safeStatus
1220
+ }
1221
+ };
1222
+ },
1223
+ diff(input) {
1224
+ return input.observed.status === "ready" ? noopDiff() : { action: "create", reasons: ["local process is not reported ready"], before: input.observed.live, after: input.unit.spec };
1225
+ },
1226
+ async apply(input) {
1227
+ if (input.diff.action === "noop") return genericResult(input);
1228
+ const surfaces = Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [String(input.unit.spec.processId ?? input.unit.logicalName)];
1229
+ const result = await runManagedDevAction({
1230
+ tenantRoot: input.context.tenantRoot,
1231
+ action: input.unit.spec.action === "restart" ? "restart" : "start",
1232
+ surfaces,
1233
+ options: typeof input.unit.spec.options === "object" && input.unit.spec.options ? input.unit.spec.options : {},
1234
+ env: input.context.launchEnv
1235
+ });
1236
+ return genericResult(input, { ...input.observed.live, managedDev: result });
1237
+ },
1238
+ async verify(input) {
1239
+ const status = await runManagedDevAction({
1240
+ tenantRoot: input.context.tenantRoot,
1241
+ action: "status",
1242
+ surfaces: Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [],
1243
+ options: typeof input.unit.spec.options === "object" && input.unit.spec.options ? input.unit.spec.options : {},
1244
+ env: input.context.launchEnv
1245
+ });
1246
+ const safeStatus = sanitizeManagedDevObservation(status);
1247
+ return summarizeVerification(input.unit.unitId, [
1248
+ verificationCheck("managed-dev-status", "Managed dev status is observable", "sdk", {
1249
+ exists: safeStatus.ok,
1250
+ configured: true,
1251
+ ready: safeStatus.ok,
1252
+ verified: safeStatus.ok,
1253
+ observed: safeStatus.parsed ?? safeStatus.output,
1254
+ issues: safeStatus.ok ? [] : [safeStatus.output || "managed dev status failed"]
1255
+ })
1256
+ ], input.observed.warnings);
1257
+ },
1258
+ async destroy(input) {
1259
+ const surfaces = Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [String(input.unit.spec.processId ?? input.unit.logicalName)];
1260
+ if (input.context.dryRun !== true) {
1261
+ await runManagedDevAction({
1262
+ tenantRoot: input.context.tenantRoot,
1263
+ action: "stop",
1264
+ surfaces,
1265
+ options: typeof input.unit.spec.options === "object" && input.unit.spec.options ? input.unit.spec.options : {},
1266
+ env: input.context.launchEnv
1267
+ });
1268
+ }
1269
+ return genericResult({
1270
+ ...input,
1271
+ diff: { action: "delete", reasons: ["selected local process for stop"], before: input.observed.live, after: {} }
1272
+ });
1273
+ }
1274
+ };
1275
+ }
1276
+ function sanitizeManagedDevObservation(value) {
1277
+ if (Array.isArray(value)) {
1278
+ return value.map((entry) => sanitizeManagedDevObservation(entry));
1279
+ }
1280
+ if (!value || typeof value !== "object") {
1281
+ return value;
1282
+ }
1283
+ const result = {};
1284
+ for (const [key, entry] of Object.entries(value)) {
1285
+ if (key === "env") {
1286
+ result.redactedEnv = redactEnvironmentRecord(entry);
1287
+ continue;
1288
+ }
1289
+ result[key] = sanitizeManagedDevObservation(entry);
1290
+ }
1291
+ return result;
1292
+ }
1293
+ function redactEnvironmentRecord(value) {
1294
+ if (!value || typeof value !== "object") {
1295
+ return {};
1296
+ }
1297
+ return Object.fromEntries(
1298
+ Object.entries(value).map(([key, entry]) => [
1299
+ key,
1300
+ key === "PATH" || key === "NODE_ENV" ? String(entry ?? "") : "<redacted>"
1301
+ ])
1302
+ );
1303
+ }
1304
+ function buildCapacityProviderAdapter(providerId) {
1305
+ return {
1306
+ providerId,
1307
+ unitTypes: ["capacity-provider"],
1308
+ supports(unitType, candidateProviderId) {
1309
+ return unitType === "capacity-provider" && candidateProviderId === providerId;
1310
+ },
1311
+ refresh(input) {
1312
+ const dependencies = input.unit.dependencies;
1313
+ return {
1314
+ ...genericObservedState(input),
1315
+ live: {
1316
+ ...input.unit.spec,
1317
+ dependencies
1318
+ }
1319
+ };
1320
+ },
1321
+ diff() {
1322
+ return noopDiff();
1323
+ },
1324
+ apply(input) {
1325
+ return genericResult(input);
1326
+ },
1327
+ async verify(input) {
1328
+ const dependencyResults = input.context.session.get("treeseed:verification-results");
1329
+ const checks = input.unit.dependencies.map((dependency) => {
1330
+ const verification = dependencyResults?.get(dependency);
1331
+ const ok = verification ? verification.verified === true : true;
1332
+ return verificationCheck(`dependency:${dependency}`, `Capacity provider dependency ${dependency} is verified`, "derived", {
1333
+ exists: ok,
1334
+ configured: ok,
1335
+ ready: ok,
1336
+ verified: ok,
1337
+ observed: verification ?? null,
1338
+ issues: ok ? [] : [`Dependency ${dependency} is not verified.`]
1339
+ });
1340
+ });
1341
+ const healthEndpoint = typeof input.unit.spec.healthEndpoint === "string" ? input.unit.spec.healthEndpoint : null;
1342
+ if (healthEndpoint) {
1343
+ const health = await checkHttpHealth(healthEndpoint);
1344
+ checks.push(verificationCheck("capacity-provider-health", `Capacity provider health endpoint ${healthEndpoint} responds`, "api", {
1345
+ exists: health.ok,
1346
+ configured: true,
1347
+ ready: health.ok,
1348
+ verified: health.ok,
1349
+ observed: health,
1350
+ issues: health.ok ? [] : [`Capacity provider health endpoint ${healthEndpoint} did not respond successfully.`]
1351
+ }));
1352
+ }
1353
+ if (checks.length === 0) {
1354
+ checks.push(verificationCheck("capacity-provider", "Capacity provider desired topology is observable", "derived", {
1355
+ exists: input.observed.exists,
1356
+ configured: input.observed.exists,
1357
+ ready: input.observed.status !== "error",
1358
+ verified: input.observed.exists && input.observed.status !== "error",
1359
+ observed: input.observed.live
1360
+ }));
1361
+ }
1362
+ return summarizeVerification(input.unit.unitId, checks, input.observed.warnings);
1363
+ },
1364
+ destroy(input) {
1365
+ return genericResult({
1366
+ ...input,
1367
+ diff: { action: "delete", reasons: ["selected capacity provider for destroy"], before: input.observed.live, after: {} }
1368
+ });
1369
+ }
1370
+ };
1371
+ }
1372
+ function recordValue(value) {
1373
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1374
+ }
1375
+ function nonEmptyString(value) {
1376
+ return typeof value === "string" && value.trim() ? value.trim() : "";
1377
+ }
1378
+ function localTreeDxProjects(value) {
1379
+ return Array.isArray(value) ? value.flatMap((entry) => {
1380
+ const record = recordValue(entry);
1381
+ const slug = nonEmptyString(record.slug);
1382
+ const repositoryName = nonEmptyString(record.repositoryName);
1383
+ const repositoryId = nonEmptyString(record.repositoryId) || repositoryName;
1384
+ const localRoot = nonEmptyString(record.localRoot);
1385
+ const contentPath = nonEmptyString(record.contentPath);
1386
+ if (!slug || !repositoryName || !repositoryId || !localRoot || !contentPath) return [];
1387
+ return [{
1388
+ projectKey: nonEmptyString(record.projectKey) || void 0,
1389
+ slug,
1390
+ repositoryName,
1391
+ repositoryId,
1392
+ localRoot,
1393
+ contentPath,
1394
+ defaultRef: nonEmptyString(record.defaultRef) || "refs/heads/main",
1395
+ seedPaths: Array.isArray(record.seedPaths) ? record.seedPaths.filter((item) => typeof item === "string" && item.trim().length > 0) : []
1396
+ }];
1397
+ }) : [];
1398
+ }
1399
+ function base64urlJson(value) {
1400
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
1401
+ }
1402
+ function mintLocalTreeDxJwt(auth) {
1403
+ const secret = nonEmptyString(auth.TREESEED_TREEDX_JWT_HS256_SECRET);
1404
+ const issuer = nonEmptyString(auth.TREESEED_TREEDX_JWT_ISSUER);
1405
+ const audience = nonEmptyString(auth.TREESEED_TREEDX_JWT_AUDIENCE);
1406
+ if (!secret || !issuer || !audience) return "";
1407
+ const now = Math.floor(Date.now() / 1e3);
1408
+ const actorId = nonEmptyString(auth.TREESEED_TREEDX_PROXY_ACTOR_ID) || "treeseed-sdk-reconciler";
1409
+ const tenantId = nonEmptyString(auth.TREESEED_TREEDX_PROXY_TENANT_ID) || "treeseed-control-plane";
1410
+ const payload = {
1411
+ iss: issuer,
1412
+ aud: audience,
1413
+ sub: actorId,
1414
+ jti: randomUUID(),
1415
+ iat: now,
1416
+ nbf: now - 5,
1417
+ exp: now + 3600,
1418
+ treedx_actor_id: actorId,
1419
+ treedx_tenant_id: tenantId,
1420
+ treedx_repo_ids: ["*"],
1421
+ treedx_capabilities: ["*"],
1422
+ treedx_refs: ["*"],
1423
+ treedx_paths: ["**"]
1424
+ };
1425
+ const signingInput = `${base64urlJson({ alg: "HS256", typ: "JWT" })}.${base64urlJson(payload)}`;
1426
+ const signature = createHmac("sha256", secret).update(signingInput).digest("base64url");
1427
+ return `${signingInput}.${signature}`;
1428
+ }
1429
+ async function listLocalTreeDxSeedFiles(project) {
1430
+ const paths = project.seedPaths?.length ? project.seedPaths : [project.contentPath];
1431
+ const files = [];
1432
+ const visit = async (absoluteDir, root) => {
1433
+ if (!existsSync(absoluteDir)) return;
1434
+ for (const entry of await readdir(absoluteDir, { withFileTypes: true })) {
1435
+ const absolutePath = resolve(absoluteDir, entry.name);
1436
+ if (entry.isDirectory()) {
1437
+ await visit(absolutePath, root);
1438
+ } else if (entry.isFile() && /\.(md|mdx)$/iu.test(entry.name)) {
1439
+ files.push({
1440
+ path: relative(root, absolutePath).replace(/\\/gu, "/"),
1441
+ content: await readFile(absolutePath, "utf8")
1442
+ });
1443
+ }
1444
+ }
1445
+ };
1446
+ for (const seedPath of paths) {
1447
+ await visit(resolve(project.localRoot, seedPath), project.localRoot);
1448
+ }
1449
+ return files.sort((left, right) => left.path.localeCompare(right.path));
1450
+ }
1451
+ async function ensureLocalTreeDxProjectRepository(client, project) {
1452
+ const repositories = recordValue(await client.repositories.list()).repos;
1453
+ const existing = Array.isArray(repositories) ? repositories.find((entry) => recordValue(entry).repositoryName === project.repositoryName || recordValue(entry).name === project.repositoryName) : null;
1454
+ const existingRepoId = nonEmptyString(recordValue(existing).repoId);
1455
+ if (existingRepoId) return { repoId: existingRepoId, created: false };
1456
+ const registered = recordValue(await client.repositories.register({
1457
+ name: project.repositoryName,
1458
+ repositoryName: project.repositoryName,
1459
+ createIfMissing: true,
1460
+ defaultRef: project.defaultRef ?? "refs/heads/main"
1461
+ }));
1462
+ const repo = recordValue(registered.repo);
1463
+ const repoId = nonEmptyString(repo.repoId);
1464
+ if (!repoId) throw new Error(`TreeDX did not return a repository id for ${project.repositoryName}.`);
1465
+ return { repoId, created: true };
1466
+ }
1467
+ async function syncLocalTreeDxProjectContent(client, project) {
1468
+ const files = await listLocalTreeDxSeedFiles(project);
1469
+ const repository = await ensureLocalTreeDxProjectRepository(client, project);
1470
+ if (files.length === 0) {
1471
+ return { project: project.slug, repositoryId: repository.repoId, repositoryName: project.repositoryName, files: 0, committed: false };
1472
+ }
1473
+ const workspace = recordValue(await client.request({
1474
+ method: "POST",
1475
+ path: `/api/v1/repos/${encodeURIComponent(repository.repoId)}/workspaces`,
1476
+ body: {
1477
+ baseRef: project.defaultRef ?? "refs/heads/main",
1478
+ branchName: `refs/heads/treeseed-local-sync/${project.slug}/${Date.now().toString(36)}`,
1479
+ mode: "writable",
1480
+ allowedPaths: [`${project.contentPath.replace(/\/+$/u, "")}/**`],
1481
+ ttlSeconds: 900
1482
+ }
1483
+ }));
1484
+ const workspaceId = nonEmptyString(workspace.workspaceId);
1485
+ if (!workspaceId) throw new Error(`TreeDX did not return a workspace id for ${project.slug}.`);
1486
+ try {
1487
+ for (const file of files) {
1488
+ await client.request({
1489
+ method: "PUT",
1490
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/files`,
1491
+ query: { path: file.path },
1492
+ body: { content: file.content, encoding: "utf8" }
1493
+ });
1494
+ }
1495
+ const commit = recordValue(await client.request({
1496
+ method: "POST",
1497
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/commit`,
1498
+ body: {
1499
+ message: `Sync ${project.slug} knowledge hub seed content`,
1500
+ author: { name: "TreeSeed Reconciler", email: "reconciler@treeseed.local" }
1501
+ }
1502
+ }));
1503
+ const graphRefresh = recordValue(await client.request({
1504
+ method: "POST",
1505
+ path: `/api/v1/repos/${encodeURIComponent(repository.repoId)}/graph/refresh`,
1506
+ body: {
1507
+ paths: project.seedPaths?.length ? project.seedPaths.map((seedPath) => `${seedPath.replace(/\/+$/u, "")}/**`) : [`${project.contentPath.replace(/\/+$/u, "")}/**`]
1508
+ }
1509
+ }).catch((error) => ({
1510
+ error: error instanceof Error ? error.message : String(error)
1511
+ })));
1512
+ return {
1513
+ project: project.slug,
1514
+ repositoryId: repository.repoId,
1515
+ repositoryName: project.repositoryName,
1516
+ files: files.length,
1517
+ committed: true,
1518
+ commitSha: nonEmptyString(commit.commitSha) || null,
1519
+ graphRefresh
1520
+ };
1521
+ } finally {
1522
+ await client.request({ method: "POST", path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/close`, body: {} }).catch(() => null);
1523
+ }
1524
+ }
1525
+ async function ensureLocalTreeDxProjectRepositoryRef(client, project) {
1526
+ const repository = await ensureLocalTreeDxProjectRepository(client, project);
1527
+ return {
1528
+ project: project.slug,
1529
+ repositoryId: repository.repoId,
1530
+ repositoryName: project.repositoryName,
1531
+ files: 0,
1532
+ committed: false,
1533
+ skippedContentSync: true
1534
+ };
1535
+ }
1536
+ function buildLocalTreeDxAdapter() {
1537
+ return {
1538
+ providerId: "local",
1539
+ unitTypes: ["local-treedx"],
1540
+ supports(unitType, providerId) {
1541
+ return unitType === "local-treedx" && providerId === "local";
1542
+ },
1543
+ refresh(input) {
1544
+ return {
1545
+ ...genericObservedState(input),
1546
+ live: {
1547
+ ...input.unit.spec,
1548
+ dependencies: input.unit.dependencies
1549
+ }
1550
+ };
1551
+ },
1552
+ diff(input) {
1553
+ const projects = localTreeDxProjects(input.unit.spec.projects);
1554
+ if (projects.length === 0) return noopDiff();
1555
+ return {
1556
+ action: "update",
1557
+ reasons: ["local TreeDX project content repositories must be registered and seeded through TreeDX"],
1558
+ before: input.observed.live,
1559
+ after: input.unit.spec
1560
+ };
1561
+ },
1562
+ async apply(input) {
1563
+ if (input.diff.action === "noop" || input.diff.action === "blocked") return genericResult(input);
1564
+ const baseUrl = nonEmptyString(input.unit.spec.baseUrl);
1565
+ const token = nonEmptyString(input.unit.spec.token) || mintLocalTreeDxJwt(recordValue(input.unit.spec.auth));
1566
+ const projects = localTreeDxProjects(input.unit.spec.projects);
1567
+ if (!baseUrl || !token || projects.length === 0) return genericResult(input);
1568
+ const client = new TreeDxClient({ baseUrl, token });
1569
+ const syncedProjects = [];
1570
+ const syncSeedContent = input.unit.spec.syncSeedContent !== false;
1571
+ for (const project of projects) {
1572
+ syncedProjects.push(syncSeedContent ? await syncLocalTreeDxProjectContent(client, project) : await ensureLocalTreeDxProjectRepositoryRef(client, project));
1573
+ }
1574
+ return genericResult(input, { ...input.observed.live, syncedProjects });
1575
+ },
1576
+ async verify(input) {
1577
+ const dependencyResults = input.context.session.get("treeseed:verification-results");
1578
+ const dependencyChecks = input.unit.dependencies.map((dependency) => {
1579
+ const verification = dependencyResults?.get(dependency);
1580
+ const ok = verification ? verification.verified === true : true;
1581
+ return verificationCheck(`dependency:${dependency}`, `TreeDX dependency ${dependency} is verified`, "derived", {
1582
+ exists: ok,
1583
+ configured: ok,
1584
+ ready: ok,
1585
+ verified: ok,
1586
+ observed: verification ?? null,
1587
+ issues: ok ? [] : [`Dependency ${dependency} is not verified.`]
1588
+ });
1589
+ });
1590
+ const healthEndpoint = typeof input.unit.spec.healthEndpoint === "string" ? input.unit.spec.healthEndpoint : null;
1591
+ if (healthEndpoint) {
1592
+ const health = await checkHttpHealth(healthEndpoint);
1593
+ dependencyChecks.push(verificationCheck("treedx-health", `TreeDX health endpoint ${healthEndpoint} responds`, "api", {
1594
+ exists: health.ok,
1595
+ configured: true,
1596
+ ready: health.ok,
1597
+ verified: health.ok,
1598
+ observed: health,
1599
+ issues: health.ok ? [] : [`TreeDX health endpoint ${healthEndpoint} did not respond successfully.`]
1600
+ }));
1601
+ }
1602
+ const contentIsTreeDx = input.unit.spec.contentRepositoryAccessMode === "treedx";
1603
+ const siteIsFilesystem = input.unit.spec.siteRepositoryAccessMode === "filesystem";
1604
+ const projectIsFilesystem = input.unit.spec.projectRepositoryAccessMode === "filesystem";
1605
+ const topologyChecks = [
1606
+ verificationCheck("content-repository-plane", "Content repositories use TreeDX by default", "derived", {
1607
+ exists: true,
1608
+ configured: contentIsTreeDx,
1609
+ ready: contentIsTreeDx,
1610
+ verified: contentIsTreeDx,
1611
+ observed: input.unit.spec.contentRepositoryAccessMode,
1612
+ issues: contentIsTreeDx ? [] : ["Content repository access mode is not treedx."]
1613
+ }),
1614
+ verificationCheck("site-repository-plane", "Project site repositories use local filesystem/worktrees by default", "derived", {
1615
+ exists: true,
1616
+ configured: siteIsFilesystem,
1617
+ ready: siteIsFilesystem,
1618
+ verified: siteIsFilesystem,
1619
+ observed: input.unit.spec.siteRepositoryAccessMode,
1620
+ issues: siteIsFilesystem ? [] : ["Site repository access mode is not filesystem."]
1621
+ }),
1622
+ verificationCheck("project-repository-plane", "Super-project repositories use local filesystem/worktrees by default", "derived", {
1623
+ exists: true,
1624
+ configured: projectIsFilesystem,
1625
+ ready: projectIsFilesystem,
1626
+ verified: projectIsFilesystem,
1627
+ observed: input.unit.spec.projectRepositoryAccessMode,
1628
+ issues: projectIsFilesystem ? [] : ["Project repository access mode is not filesystem."]
1629
+ })
1630
+ ];
1631
+ const repositoryChecks = [];
1632
+ const baseUrl = nonEmptyString(input.unit.spec.baseUrl);
1633
+ const token = nonEmptyString(input.unit.spec.token) || mintLocalTreeDxJwt(recordValue(input.unit.spec.auth));
1634
+ if (baseUrl && token) {
1635
+ const client = new TreeDxClient({ baseUrl, token });
1636
+ let repositories = [];
1637
+ try {
1638
+ const list = recordValue(await client.repositories.list()).repos;
1639
+ repositories = Array.isArray(list) ? list : [];
1640
+ } catch {
1641
+ repositories = [];
1642
+ }
1643
+ for (const project of localTreeDxProjects(input.unit.spec.projects)) {
1644
+ const repo = repositories.find((entry) => {
1645
+ const record = recordValue(entry);
1646
+ return record.repositoryName === project.repositoryName || record.name === project.repositoryName;
1647
+ });
1648
+ if (repo) {
1649
+ repositoryChecks.push(verificationCheck(`treedx-repo:${project.slug}`, `TreeDX repository ${project.repositoryId} is registered`, "api", {
1650
+ exists: true,
1651
+ configured: true,
1652
+ ready: true,
1653
+ verified: true,
1654
+ observed: repo
1655
+ }));
1656
+ } else {
1657
+ repositoryChecks.push(verificationCheck(`treedx-repo:${project.slug}`, `TreeDX repository ${project.repositoryId} is registered`, "api", {
1658
+ exists: false,
1659
+ configured: true,
1660
+ ready: false,
1661
+ verified: false,
1662
+ observed: { repositoryName: project.repositoryName },
1663
+ issues: [`TreeDX repository ${project.repositoryId} is not registered.`]
1664
+ }));
1665
+ }
1666
+ }
1667
+ }
1668
+ return summarizeVerification(input.unit.unitId, [...dependencyChecks, ...topologyChecks, ...repositoryChecks], input.observed.warnings);
1669
+ },
1670
+ destroy(input) {
1671
+ return genericResult({
1672
+ ...input,
1673
+ diff: { action: "delete", reasons: ["selected local TreeDX topology for destroy"], before: input.observed.live, after: {} }
1674
+ });
1675
+ }
1676
+ };
1677
+ }
1678
+ function buildReleaseGateAdapter() {
1679
+ const unitTypes = [
1680
+ "release-gate:verify",
1681
+ "release-gate:template-verify",
1682
+ "release-gate:template-release-record",
1683
+ "release-gate:npm-publish",
1684
+ "release-gate:image-publish",
1685
+ "release-gate:hosted-reconcile",
1686
+ "release-gate:live-verify",
1687
+ "release-gate:candidate-record",
1688
+ "release-gate:production-record"
1689
+ ];
1690
+ return {
1691
+ providerId: "treeseed",
1692
+ unitTypes,
1693
+ supports(unitType, providerId) {
1694
+ return providerId === "treeseed" && unitTypes.includes(unitType);
1695
+ },
1696
+ refresh(input) {
1697
+ const fingerprint = typeof input.unit.spec.fingerprint === "string" ? input.unit.spec.fingerprint : null;
1698
+ const previousFingerprint = typeof input.persistedState?.lastReconciledState?.fingerprint === "string" ? input.persistedState.lastReconciledState.fingerprint : null;
1699
+ return {
1700
+ ...genericObservedState(input),
1701
+ status: fingerprint && previousFingerprint === fingerprint ? "ready" : "pending",
1702
+ live: {
1703
+ ...input.unit.spec,
1704
+ previousFingerprint,
1705
+ fingerprint
1706
+ }
1707
+ };
1708
+ },
1709
+ diff(input) {
1710
+ return input.observed.status === "ready" ? noopDiff() : { action: "update", reasons: ["release gate fingerprint has not been reconciled"], before: input.observed.live, after: input.unit.spec };
1711
+ },
1712
+ async apply(input) {
1713
+ if (input.diff.action === "noop") return genericResult(input);
1714
+ const gateKind = String(input.unit.spec.gateKind ?? input.unit.unitType);
1715
+ const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : null;
1716
+ const templateId = typeof input.unit.spec.templateId === "string" ? input.unit.spec.templateId : null;
1717
+ if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
1718
+ return genericResult(input, {
1719
+ ...input.observed.live,
1720
+ gateKind,
1721
+ packageId,
1722
+ templateId,
1723
+ skipped: true,
1724
+ reason: "disabled",
1725
+ fingerprint: input.unit.spec.fingerprint
1726
+ });
1727
+ }
1728
+ if (gateKind === "release-gate:verify" && packageId) {
1729
+ const verify = await runReleaseVerifyCommand({ tenantRoot: input.context.tenantRoot, packageId, env: input.context.launchEnv });
1730
+ if (verify.ok !== true) {
1731
+ throw new Error([verify.stderr, verify.stdout].filter(Boolean).join("\n").trim() || `${packageId} release verification failed`);
1732
+ }
1733
+ return genericResult(input, { ...input.observed.live, verify, fingerprint: input.unit.spec.fingerprint });
1734
+ }
1735
+ if (gateKind === "release-gate:template-verify" && templateId) {
1736
+ const verify = runTemplateReleaseVerifyCommand({ tenantRoot: input.context.tenantRoot, templateId, env: input.context.launchEnv });
1737
+ if (verify.ok !== true) {
1738
+ throw new Error([verify.stderr, verify.stdout].filter(Boolean).join("\n").trim() || `${templateId} template release verification failed`);
1739
+ }
1740
+ return genericResult(input, { ...input.observed.live, verify, fingerprint: input.unit.spec.fingerprint });
1741
+ }
1742
+ if (gateKind === "release-gate:template-release-record" && templateId) {
1743
+ const releaseTag = typeof input.unit.spec.releaseTag === "string" ? input.unit.spec.releaseTag : null;
1744
+ const tag = releaseTag && input.context.target.kind === "persistent" && input.context.target.scope === "prod" ? ensureTemplateReleaseTag({ tenantRoot: input.context.tenantRoot, templateId, tagName: releaseTag }) : null;
1745
+ const recordPath = typeof input.unit.spec.recordPath === "string" ? input.unit.spec.recordPath : `.treeseed/templates/${templateId}/latest-release.json`;
1746
+ const template = checkedOutTemplateRepositories(input.context.tenantRoot).find((candidate) => candidate.templateManifest?.id === templateId);
1747
+ const record = writeReleaseRecord({
1748
+ tenantRoot: input.context.tenantRoot,
1749
+ recordPath,
1750
+ record: {
1751
+ schemaVersion: 1,
1752
+ kind: gateKind,
1753
+ templateId,
1754
+ releaseTag,
1755
+ tag,
1756
+ environment: input.unit.spec.environment ?? null,
1757
+ version: template?.templateManifest?.version ?? null,
1758
+ recordedAt: nowIso()
1759
+ }
1760
+ });
1761
+ return genericResult(input, { ...input.observed.live, record, tag, fingerprint: input.unit.spec.fingerprint });
1762
+ }
1763
+ if ((gateKind === "release-gate:npm-publish" || gateKind === "release-gate:image-publish") && packageId) {
1764
+ const adapter = findTreeseedPackageAdapter(input.context.tenantRoot, packageId);
1765
+ const repository = typeof adapter?.metadata.repository === "string" ? adapter.metadata.repository : null;
1766
+ if (!repository) {
1767
+ throw new Error(`${packageId} does not declare a GitHub repository.`);
1768
+ }
1769
+ const workflow = gateKind === "release-gate:image-publish" ? workflowName(adapter?.metadata.developmentImageWorkflow, "publish.yml") : workflowName(adapter?.metadata.hostedVerifyWorkflow, "publish.yml");
1770
+ const dispatch = await dispatchReconcileGitHubWorkflow({
1771
+ repository,
1772
+ workflow,
1773
+ branch: input.context.target.kind === "persistent" && input.context.target.scope === "prod" ? "main" : "staging",
1774
+ inputs: {},
1775
+ wait: input.context.target.kind === "persistent" && input.context.target.scope === "prod",
1776
+ env: buildGitHubEnv(input)
1777
+ });
1778
+ return genericResult(input, { ...input.observed.live, dispatch, fingerprint: input.unit.spec.fingerprint });
1779
+ }
1780
+ if (gateKind === "release-gate:hosted-reconcile") {
1781
+ const environment = input.unit.spec.environment === "prod" ? "prod" : "staging";
1782
+ const selector = typeof input.unit.spec.hostedSelector === "object" && input.unit.spec.hostedSelector ? input.unit.spec.hostedSelector : { environment, provider: ["cloudflare", "railway", "github", "dockerhub"] };
1783
+ const nested = await runHostedReconcileGate({
1784
+ parentContext: input.context,
1785
+ selector,
1786
+ target: { kind: "persistent", scope: environment },
1787
+ dryRun: input.context.dryRun === true
1788
+ });
1789
+ return genericResult(input, { ...input.observed.live, nested, fingerprint: input.unit.spec.fingerprint });
1790
+ }
1791
+ if (gateKind === "release-gate:live-verify") {
1792
+ const environment = input.unit.spec.environment === "prod" ? "prod" : "staging";
1793
+ const selector = typeof input.unit.spec.hostedSelector === "object" && input.unit.spec.hostedSelector ? input.unit.spec.hostedSelector : { environment, provider: ["cloudflare", "railway", "github", "dockerhub"] };
1794
+ const status = await runHostedVerifyGate({
1795
+ parentContext: input.context,
1796
+ selector,
1797
+ target: { kind: "persistent", scope: environment }
1798
+ });
1799
+ if (!status.ready) {
1800
+ throw new Error(`Hosted live verification failed for ${environment}: ${status.blockers.join("\n")}`);
1801
+ }
1802
+ return genericResult(input, { ...input.observed.live, status, fingerprint: input.unit.spec.fingerprint });
1803
+ }
1804
+ if (gateKind === "release-gate:candidate-record" || gateKind === "release-gate:production-record") {
1805
+ const recordPath = typeof input.unit.spec.recordPath === "string" ? input.unit.spec.recordPath : gateKind === "release-gate:production-record" ? ".treeseed/workflow/releases/latest-production.json" : ".treeseed/workflow/release-candidates/latest-staging.json";
1806
+ const record = writeReleaseRecord({
1807
+ tenantRoot: input.context.tenantRoot,
1808
+ recordPath,
1809
+ record: {
1810
+ schemaVersion: 1,
1811
+ kind: gateKind,
1812
+ unitId: input.unit.unitId,
1813
+ fingerprint: input.unit.spec.fingerprint ?? null,
1814
+ environment: input.unit.spec.environment ?? null,
1815
+ recordedAt: nowIso()
1816
+ }
1817
+ });
1818
+ return genericResult(input, { ...input.observed.live, record, fingerprint: input.unit.spec.fingerprint });
1819
+ }
1820
+ return genericResult(input, { ...input.observed.live, fingerprint: input.unit.spec.fingerprint });
1821
+ },
1822
+ verify(input) {
1823
+ const dependencyResults = input.context.session.get("treeseed:verification-results");
1824
+ const dependencyChecks = input.unit.dependencies.map((dependency) => {
1825
+ const verification = dependencyResults?.get(dependency);
1826
+ const ok = verification?.verified === true;
1827
+ return verificationCheck(`dependency:${dependency}`, `Release gate dependency ${dependency} passed`, "derived", {
1828
+ exists: ok,
1829
+ configured: ok,
1830
+ ready: ok,
1831
+ verified: ok,
1832
+ observed: verification ?? null,
1833
+ issues: ok ? [] : [`Dependency ${dependency} is not verified.`]
1834
+ });
1835
+ });
1836
+ const ownCheck = verificationCheck("release-gate", "Release gate reconciled its fingerprint", "sdk", {
1837
+ exists: true,
1838
+ configured: true,
1839
+ ready: input.result?.state?.fingerprint === input.unit.spec.fingerprint || input.diff.action === "noop",
1840
+ verified: input.result?.state?.fingerprint === input.unit.spec.fingerprint || input.diff.action === "noop",
1841
+ expected: input.unit.spec.fingerprint ?? null,
1842
+ observed: input.result?.state?.fingerprint ?? input.observed.live.previousFingerprint ?? null
1843
+ });
1844
+ return summarizeVerification(input.unit.unitId, [...dependencyChecks, ownCheck], input.observed.warnings);
1845
+ }
1846
+ };
1847
+ }
1848
+ function workflowFingerprint(input) {
1849
+ return JSON.stringify({
1850
+ unitId: input.unit.unitId,
1851
+ unitType: input.unit.unitType,
1852
+ spec: input.unit.spec,
1853
+ dependencies: input.unit.dependencies
1854
+ });
127
1855
  }
128
- function buildCompositeAdapter(unitType) {
1856
+ function buildWorkflowMetaAdapter() {
1857
+ const unitTypes = [
1858
+ "branch-preview",
1859
+ "branch-preview-cleanup",
1860
+ "workflow-gate",
1861
+ "save-gate:local-verify",
1862
+ "save-gate:promotion-readiness",
1863
+ "save-gate:hosted-verify"
1864
+ ];
129
1865
  return {
130
1866
  providerId: "treeseed",
131
- unitTypes: [unitType],
132
- supports(candidateUnitType, providerId) {
133
- return candidateUnitType === unitType && providerId === "treeseed";
1867
+ unitTypes,
1868
+ supports(unitType, providerId) {
1869
+ return providerId === "treeseed" && unitTypes.includes(unitType);
134
1870
  },
135
- observe(input) {
136
- return noopObservedState(input);
1871
+ refresh(input) {
1872
+ const fingerprint = workflowFingerprint(input);
1873
+ const previousFingerprint = typeof input.persistedState?.lastReconciledState?.fingerprint === "string" ? input.persistedState.lastReconciledState.fingerprint : null;
1874
+ return {
1875
+ ...genericObservedState(input),
1876
+ status: previousFingerprint === fingerprint ? "ready" : "pending",
1877
+ live: {
1878
+ ...input.unit.spec,
1879
+ fingerprint,
1880
+ previousFingerprint
1881
+ }
1882
+ };
137
1883
  },
138
- plan() {
139
- return noopDiff();
1884
+ diff(input) {
1885
+ return input.observed.status === "ready" ? noopDiff() : { action: input.unit.unitType === "branch-preview-cleanup" ? "delete" : "update", reasons: [`${input.unit.unitType} fingerprint has not been reconciled`], before: input.observed.live, after: input.unit.spec };
140
1886
  },
141
- requiredPostconditions({ unit }) {
142
- return unit.dependencies.map((dependency) => ({
143
- key: dependency,
144
- description: `Dependency ${dependency} is verified`
145
- }));
1887
+ async apply(input) {
1888
+ if (input.diff.action === "noop") return genericResult(input);
1889
+ const fingerprint = workflowFingerprint(input);
1890
+ if (input.unit.unitType === "branch-preview") {
1891
+ const selector = typeof input.unit.spec.resources === "object" && input.unit.spec.resources ? input.unit.spec.resources : { environment: "staging", appId: [String(input.unit.spec.appId ?? "web")] };
1892
+ const nested = await runHostedReconcileGate({
1893
+ parentContext: input.context,
1894
+ selector,
1895
+ target: input.context.target,
1896
+ dryRun: input.context.dryRun === true
1897
+ });
1898
+ return genericResult(input, { ...input.observed.live, nested, fingerprint });
1899
+ }
1900
+ if (input.unit.unitType === "save-gate:hosted-verify") {
1901
+ const selector = typeof input.unit.spec.selector === "object" && input.unit.spec.selector ? input.unit.spec.selector : { environment: input.context.target.kind === "persistent" ? input.context.target.scope : "staging" };
1902
+ const status = await runHostedVerifyGate({
1903
+ parentContext: input.context,
1904
+ selector,
1905
+ target: input.context.target
1906
+ });
1907
+ if (!status.ready) {
1908
+ throw new Error(`Save hosted verification failed: ${status.blockers.join("\n")}`);
1909
+ }
1910
+ return genericResult(input, { ...input.observed.live, status, fingerprint });
1911
+ }
1912
+ return genericResult(input, { ...input.observed.live, fingerprint });
146
1913
  },
147
- reconcile({ unit, observed, diff }) {
148
- return {
149
- unit,
150
- observed,
151
- diff,
152
- action: diff.action,
153
- warnings: [],
154
- resourceLocators: {},
155
- state: {
156
- unitId: unit.unitId,
157
- reconciledAt: nowIso()
158
- },
159
- verification: null
160
- };
1914
+ verify(input) {
1915
+ const fingerprint = workflowFingerprint(input);
1916
+ const observedFingerprint = typeof input.observed.live.fingerprint === "string" ? input.observed.live.fingerprint : null;
1917
+ return summarizeVerification(input.unit.unitId, [
1918
+ verificationCheck("workflow-meta-fingerprint", `${input.unit.unitType} fingerprint is reconciled`, "derived", {
1919
+ exists: true,
1920
+ configured: observedFingerprint === fingerprint,
1921
+ ready: observedFingerprint === fingerprint,
1922
+ verified: observedFingerprint === fingerprint,
1923
+ expected: fingerprint,
1924
+ observed: observedFingerprint,
1925
+ issues: observedFingerprint === fingerprint ? [] : [`Expected fingerprint ${fingerprint}, observed ${observedFingerprint ?? "none"}.`]
1926
+ })
1927
+ ], input.observed.warnings);
161
1928
  },
162
- verify({ context, unit, postconditions }) {
163
- const dependencyResults = context.session.get("treeseed:verification-results");
164
- const checks = postconditions.map((condition) => {
165
- const dependency = dependencyResults?.get(condition.key);
166
- const verified = dependency?.verified === true;
167
- return {
168
- key: condition.key,
169
- description: condition.description,
170
- source: "derived",
171
- exists: verified,
172
- configured: verified,
173
- ready: verified,
174
- verified,
175
- expected: true,
176
- observed: dependency?.verified ?? false,
177
- issues: verified ? [] : [`Dependency ${condition.key} is not verified.`]
178
- };
179
- });
180
- return summarizeVerification(unit.unitId, checks);
1929
+ destroy(input) {
1930
+ return genericResult({
1931
+ ...input,
1932
+ diff: { action: "delete", reasons: [`selected ${input.unit.unitType} for destroy`], before: input.observed.live, after: {} }
1933
+ }, { ...input.observed.live, destroyedAt: nowIso(), fingerprint: workflowFingerprint(input) });
181
1934
  }
182
1935
  };
183
1936
  }
@@ -199,27 +1952,38 @@ function normalizeEnvironmentValues(env) {
199
1952
  }
200
1953
  function resolveReconcileEnvironmentValues(input, scope) {
201
1954
  if (scope === "local") {
202
- return resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
1955
+ return normalizeEnvironmentValues(resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope));
203
1956
  }
204
- const hostedRuntimeValues = {
1957
+ return {
205
1958
  ...normalizeEnvironmentValues(process.env),
206
1959
  ...normalizeEnvironmentValues(input.context.launchEnv)
207
1960
  };
208
- if (hostedRuntimeValues.CLOUDFLARE_API_TOKEN || hostedRuntimeValues.RAILWAY_API_TOKEN || hostedRuntimeValues.GH_TOKEN) {
209
- return hostedRuntimeValues;
210
- }
211
- const values = {
212
- ...resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope),
213
- ...hostedRuntimeValues
214
- };
215
- return values;
216
1961
  }
217
1962
  function buildCloudflareEnv(input) {
218
1963
  const scope = scopeFromTarget(toDeployTarget(input.context.target));
219
1964
  const values = resolveReconcileEnvironmentValues(input, scope);
1965
+ let accountId = [
1966
+ values.TREESEED_CLOUDFLARE_ACCOUNT_ID,
1967
+ input.context.launchEnv.TREESEED_CLOUDFLARE_ACCOUNT_ID,
1968
+ process.env.TREESEED_CLOUDFLARE_ACCOUNT_ID,
1969
+ resolveConfiguredCloudflareAccountId(input.context.deployConfig)
1970
+ ].find((value) => typeof value === "string" && value.trim().length > 0 && value.trim() !== "replace-with-cloudflare-account-id")?.trim() ?? "";
1971
+ let token = [
1972
+ values.TREESEED_CLOUDFLARE_API_TOKEN,
1973
+ input.context.launchEnv.TREESEED_CLOUDFLARE_API_TOKEN,
1974
+ process.env.TREESEED_CLOUDFLARE_API_TOKEN
1975
+ ].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
1976
+ if (scope !== "local" && (!accountId || !token)) {
1977
+ const configuredValues = normalizeEnvironmentValues(resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope));
1978
+ accountId = accountId || ([
1979
+ configuredValues.TREESEED_CLOUDFLARE_ACCOUNT_ID,
1980
+ resolveConfiguredCloudflareAccountId(input.context.deployConfig)
1981
+ ].find((value) => typeof value === "string" && value.trim().length > 0 && value.trim() !== "replace-with-cloudflare-account-id")?.trim() ?? "");
1982
+ token = token || (configuredValues.TREESEED_CLOUDFLARE_API_TOKEN ?? "");
1983
+ }
220
1984
  return {
221
- CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID ?? input.context.launchEnv.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? resolveConfiguredCloudflareAccountId(input.context.deployConfig),
222
- CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN ?? input.context.launchEnv.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1985
+ CLOUDFLARE_ACCOUNT_ID: accountId,
1986
+ CLOUDFLARE_API_TOKEN: token
223
1987
  };
224
1988
  }
225
1989
  function hasLiveResourceId(value) {
@@ -227,15 +1991,24 @@ function hasLiveResourceId(value) {
227
1991
  }
228
1992
  function buildRailwayEnv(input, scope) {
229
1993
  const values = resolveReconcileEnvironmentValues(input, scope);
230
- const token = [
231
- values.RAILWAY_API_TOKEN,
232
- input.context.launchEnv.RAILWAY_API_TOKEN,
233
- process.env.RAILWAY_API_TOKEN
1994
+ let token = [
1995
+ values.TREESEED_RAILWAY_API_TOKEN,
1996
+ input.context.launchEnv.TREESEED_RAILWAY_API_TOKEN,
1997
+ process.env.TREESEED_RAILWAY_API_TOKEN
234
1998
  ].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
1999
+ let railwayApiUrl = values.TREESEED_RAILWAY_API_URL ?? input.context.launchEnv.TREESEED_RAILWAY_API_URL ?? process.env.TREESEED_RAILWAY_API_URL ?? "";
2000
+ let railwayWorkspace = values.TREESEED_RAILWAY_WORKSPACE ?? input.context.launchEnv.TREESEED_RAILWAY_WORKSPACE ?? process.env.TREESEED_RAILWAY_WORKSPACE ?? "";
2001
+ if (scope !== "local" && !token) {
2002
+ const configuredValues = normalizeEnvironmentValues(resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope));
2003
+ token = configuredValues.TREESEED_RAILWAY_API_TOKEN ?? "";
2004
+ railwayApiUrl = railwayApiUrl || (configuredValues.TREESEED_RAILWAY_API_URL ?? "");
2005
+ railwayWorkspace = railwayWorkspace || (configuredValues.TREESEED_RAILWAY_WORKSPACE ?? "");
2006
+ }
235
2007
  return {
2008
+ TREESEED_RAILWAY_API_TOKEN: token,
236
2009
  RAILWAY_API_TOKEN: token,
237
- TREESEED_RAILWAY_API_URL: values.TREESEED_RAILWAY_API_URL ?? input.context.launchEnv.TREESEED_RAILWAY_API_URL ?? process.env.TREESEED_RAILWAY_API_URL ?? "",
238
- TREESEED_RAILWAY_WORKSPACE: values.TREESEED_RAILWAY_WORKSPACE ?? input.context.launchEnv.TREESEED_RAILWAY_WORKSPACE ?? process.env.TREESEED_RAILWAY_WORKSPACE ?? ""
2010
+ TREESEED_RAILWAY_API_URL: railwayApiUrl,
2011
+ TREESEED_RAILWAY_WORKSPACE: railwayWorkspace
239
2012
  };
240
2013
  }
241
2014
  function findCloudflareQueueByName(input, env, expectedName, { attempts = 6, delayMs = 350 } = {}) {
@@ -329,7 +2102,7 @@ function listCloudflareQueuesViaApi(env) {
329
2102
  return Array.isArray(payload?.result) ? payload.result : [];
330
2103
  }
331
2104
  function cloudflareObservationSnapshot(input, forceRefresh = false) {
332
- const cacheKey = `cloudflare:observe:${input.unit.target.kind === "persistent" ? input.unit.target.scope : input.unit.target.branchName}`;
2105
+ const cacheKey = `cloudflare:refresh:${input.unit.target.kind === "persistent" ? input.unit.target.scope : input.unit.target.branchName}`;
333
2106
  return providerCache(input, cacheKey, () => {
334
2107
  const target = toDeployTarget(input.context.target);
335
2108
  const env = buildCloudflareEnv(input);
@@ -512,6 +2285,8 @@ function normalizeRailwayDomainPayload(value) {
512
2285
  }
513
2286
  const record = value;
514
2287
  const status = record.status && typeof record.status === "object" ? record.status : {};
2288
+ const certificate = record.certificate && typeof record.certificate === "object" ? record.certificate : status.certificate && typeof status.certificate === "object" ? status.certificate : {};
2289
+ const verification = record.verification && typeof record.verification === "object" ? record.verification : status.verification && typeof status.verification === "object" ? status.verification : {};
515
2290
  const domain = typeof record.domain === "string" ? record.domain.trim() : typeof record.name === "string" ? record.name.trim() : "";
516
2291
  const serviceDomain = firstRailwayDomainString(
517
2292
  record.serviceDomain,
@@ -550,65 +2325,42 @@ function normalizeRailwayDomainPayload(value) {
550
2325
  id: typeof record.id === "string" ? record.id.trim() : null,
551
2326
  domain,
552
2327
  serviceDomain,
553
- certificateStatus: typeof status.certificateStatus === "string" ? String(status.certificateStatus).trim().toUpperCase() : null,
554
- verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof status.verificationDnsHost === "string" ? String(status.verificationDnsHost).trim() : null,
555
- verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof status.verificationToken === "string" ? String(status.verificationToken).trim() : null,
2328
+ verified: record.verified === true || status.verified === true || verification.verified === true,
2329
+ certificateStatus: firstRailwayDomainString(
2330
+ record.certificateStatus,
2331
+ status.certificateStatus,
2332
+ certificate.status
2333
+ )?.toUpperCase() ?? null,
2334
+ verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof status.verificationDnsHost === "string" ? String(status.verificationDnsHost).trim() : typeof verification.dnsHost === "string" ? String(verification.dnsHost).trim() : null,
2335
+ verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof status.verificationToken === "string" ? String(status.verificationToken).trim() : typeof verification.token === "string" ? String(verification.token).trim() : null,
556
2336
  dnsRecords: effectiveDnsRecords
557
2337
  };
558
2338
  }
559
2339
  async function ensureRailwayCustomDomain(input, service, domain, env, identifiers) {
560
- if (identifiers?.projectId && identifiers?.environmentId && identifiers?.serviceId) {
561
- const ensured = await ensureRailwayCustomDomainViaApi({
562
- projectId: identifiers.projectId,
563
- environmentId: identifiers.environmentId,
564
- serviceId: identifiers.serviceId,
565
- domain,
566
- env
567
- });
568
- if (ensured.domain) {
569
- return ensured.domain;
570
- }
2340
+ if (!identifiers?.projectId || !identifiers.environmentId || !identifiers.serviceId) {
2341
+ throw new Error(`Railway custom domain ${domain} cannot be reconciled without project, environment, and service ids.`);
571
2342
  }
572
- ensureRailwayProjectContext(service, { env, capture: true });
573
- const result = runRailway(["domain", domain, "--service", service.serviceName ?? service.serviceId, "--json"], {
574
- cwd: service.rootDir,
575
- capture: true,
576
- allowFailure: true,
2343
+ const existing = await listRailwayCustomDomains({
2344
+ projectId: identifiers.projectId,
2345
+ environmentId: identifiers.environmentId,
2346
+ serviceId: identifiers.serviceId,
577
2347
  env
578
2348
  });
579
- const output = `${result.stderr ?? ""}
580
- ${result.stdout ?? ""}`;
581
- if (identifiers?.projectId && identifiers?.environmentId && identifiers?.serviceId) {
582
- const refreshed = await listRailwayCustomDomains({
583
- projectId: identifiers.projectId,
584
- environmentId: identifiers.environmentId,
585
- serviceId: identifiers.serviceId,
586
- env
587
- });
588
- const matched = refreshed.find((entry) => entry.domain === domain) ?? null;
589
- if (matched) {
590
- return matched;
591
- }
2349
+ const matched = existing.find((entry) => entry.domain === domain) ?? null;
2350
+ if (matched) {
2351
+ return matched;
592
2352
  }
593
- if (result.status !== 0 && !/already exists|already assigned|taken|has already been taken|not available/iu.test(output)) {
594
- throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway domain ${domain} failed`);
595
- }
596
- let parsedJson = {};
597
- if (result.stdout?.trim()) {
598
- try {
599
- parsedJson = JSON.parse(result.stdout);
600
- } catch {
601
- parsedJson = {};
602
- }
603
- }
604
- const parsed = normalizeRailwayDomainPayload(parsedJson);
605
- return parsed ?? {
606
- id: null,
2353
+ const ensured = await ensureRailwayCustomDomainViaApi({
2354
+ projectId: identifiers.projectId,
2355
+ environmentId: identifiers.environmentId,
2356
+ serviceId: identifiers.serviceId,
607
2357
  domain,
608
- serviceDomain: null,
609
- certificateStatus: null,
610
- dnsRecords: []
611
- };
2358
+ env
2359
+ });
2360
+ if (!ensured.domain) {
2361
+ throw new Error(`Railway API custom domain reconciliation did not return ${domain}.`);
2362
+ }
2363
+ return ensured.domain;
612
2364
  }
613
2365
  function collectCloudflareEnvironmentSync(input) {
614
2366
  const target = toDeployTarget(input.context.target);
@@ -637,7 +2389,7 @@ function collectCloudflareEnvironmentSync(input) {
637
2389
  secretNames.add(entry.id);
638
2390
  }
639
2391
  }
640
- if (entry.targets.includes("cloudflare-var") && value) {
2392
+ if (entry.targets.includes("cloudflare-var") && value && entry.id !== "TREESEED_PROJECT_DOMAINS") {
641
2393
  vars[entry.id] = value;
642
2394
  varNames.add(entry.id);
643
2395
  }
@@ -858,61 +2610,6 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
858
2610
  current.databaseId = created.uuid;
859
2611
  current.previewDatabaseId = created.previewDatabaseUuid ?? created.uuid;
860
2612
  };
861
- const ensureQueue = () => {
862
- const current = state.queues?.agentWork;
863
- if (!current?.name) {
864
- return;
865
- }
866
- const liveQueue = findCloudflareQueueByName(input, env, current.name, { attempts: 1, delayMs: 0 });
867
- const liveDlq = current.dlqName ? findCloudflareQueueByName(input, env, current.dlqName, { attempts: 1, delayMs: 0 }) : null;
868
- if (liveQueue) {
869
- current.queueId = queueId(liveQueue);
870
- current.dlqId = current.dlqName ? queueId(liveDlq) : null;
871
- return;
872
- }
873
- let refreshedQueues = queues;
874
- const existing = refreshedQueues.find((entry) => queueName(entry) === current.name);
875
- if (existing) {
876
- current.queueId = queueId(existing);
877
- const existingDlq = current.dlqName ? refreshedQueues.find((entry) => queueName(entry) === current.dlqName) : null;
878
- current.dlqId = queueId(existingDlq);
879
- return;
880
- }
881
- if (dryRun) {
882
- current.queueId = `dryrun-${current.name}`;
883
- current.dlqId = current.dlqName ? `dryrun-${current.dlqName}` : null;
884
- return;
885
- }
886
- try {
887
- runWrangler(["queues", "create", current.name], {
888
- cwd: input.context.tenantRoot,
889
- capture: true,
890
- env
891
- });
892
- } catch (error) {
893
- if (!isWranglerAlreadyExistsError(error, [/Queue name .* is already taken/i, /\[code:\s*11009\]/i])) {
894
- throw error;
895
- }
896
- }
897
- refreshedQueues = listQueues(input.context.tenantRoot, env);
898
- if (current.dlqName && !refreshedQueues.find((entry) => queueName(entry) === current.dlqName)) {
899
- try {
900
- runWrangler(["queues", "create", current.dlqName], {
901
- cwd: input.context.tenantRoot,
902
- capture: true,
903
- env
904
- });
905
- } catch (error) {
906
- if (!isWranglerAlreadyExistsError(error, [/Queue name .* is already taken/i, /\[code:\s*11009\]/i])) {
907
- throw error;
908
- }
909
- }
910
- }
911
- const created = findCloudflareQueueByName(input, env, current.name);
912
- current.queueId = created ? queueId(created) : syntheticQueueLocator(current.name);
913
- const createdDlq = current.dlqName ? findCloudflareQueueByName(input, env, current.dlqName) : null;
914
- current.dlqId = current.dlqName ? createdDlq ? queueId(createdDlq) : syntheticQueueLocator(current.dlqName) : null;
915
- };
916
2613
  const ensureR2Bucket = () => {
917
2614
  const bucketName = state.content?.bucketName;
918
2615
  if (!bucketName) {
@@ -1031,7 +2728,6 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
1031
2728
  };
1032
2729
  runStep("kv-form-guard", () => ensureKv("FORM_GUARD_KV"));
1033
2730
  runStep("d1", ensureD1);
1034
- runStep("queue", ensureQueue);
1035
2731
  runStep("r2", ensureR2Bucket);
1036
2732
  runStep("pages", ensurePagesProject);
1037
2733
  runStep("turnstile-widget", ensureTurnstileWidget);
@@ -1077,20 +2773,19 @@ function observeCloudflareUnit(input) {
1077
2773
  const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets } = snapshot;
1078
2774
  switch (input.unit.unitType) {
1079
2775
  case "queue": {
1080
- const liveQueue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
1081
- const liveDlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2776
+ const name = input.unit.spec.name;
2777
+ const dlqName = input.unit.spec.dlqName;
2778
+ const liveQueue = queues.find((entry) => queueName(entry) === name);
2779
+ const liveDlq = dlqName ? queues.find((entry) => queueName(entry) === dlqName) : null;
1082
2780
  return {
1083
- exists: Boolean(liveQueue || state.queues?.agentWork?.queueId),
2781
+ exists: Boolean(liveQueue),
1084
2782
  status: liveQueue ? "ready" : "pending",
1085
- live: { ...state.queues?.agentWork ?? {} },
2783
+ live: { name, dlqName },
1086
2784
  locators: {
1087
- queueId: queueId(liveQueue) ?? state.queues?.agentWork?.queueId ?? null,
1088
- dlqId: queueId(liveDlq) ?? state.queues?.agentWork?.dlqId ?? null
2785
+ queueId: queueId(liveQueue) ?? null,
2786
+ dlqId: queueId(liveDlq) ?? null
1089
2787
  },
1090
- warnings: [
1091
- ...isSyntheticQueueLocator(state.queues?.agentWork?.queueId) ? ["Cloudflare queue id is pending propagation; using queue-name fallback."] : [],
1092
- ...isSyntheticQueueLocator(state.queues?.agentWork?.dlqId) ? ["Cloudflare dead-letter queue id is pending propagation; using queue-name fallback."] : []
1093
- ]
2788
+ warnings: []
1094
2789
  };
1095
2790
  }
1096
2791
  case "database": {
@@ -1180,7 +2875,7 @@ function verifyCloudflareUnitOnce(input, postconditions) {
1180
2875
  const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets, env } = snapshot;
1181
2876
  switch (input.unit.unitType) {
1182
2877
  case "queue": {
1183
- const queue = state.queues?.agentWork;
2878
+ const queue = input.unit.spec;
1184
2879
  const liveQueue = findCloudflareQueueByName(input, env, queue?.name, { attempts: 12, delayMs: 500 });
1185
2880
  const liveDlq = queue?.dlqName ? findCloudflareQueueByName(input, env, queue.dlqName, { attempts: 12, delayMs: 500 }) : null;
1186
2881
  return summarizeVerification(input.unit.unitId, [
@@ -1417,16 +3112,27 @@ function buildCloudflareDiff(input, observed) {
1417
3112
  after: input.unit.spec
1418
3113
  };
1419
3114
  }
3115
+ if (input.unit.unitType === "pages-project") {
3116
+ const verification = verifyCloudflareUnit(input, []);
3117
+ if (verification.supported && !verification.verified) {
3118
+ return {
3119
+ action: "update",
3120
+ reasons: [...verification.missing, ...verification.drifted],
3121
+ before: observed.live,
3122
+ after: input.unit.spec
3123
+ };
3124
+ }
3125
+ }
1420
3126
  const locatorValues = Object.values(observed.locators).filter(Boolean);
1421
3127
  return {
1422
- action: locatorValues.length > 0 ? "reuse" : "update",
3128
+ action: locatorValues.length > 0 ? "noop" : "update",
1423
3129
  reasons: locatorValues.length > 0 ? ["resource already present"] : ["resource partially configured"],
1424
3130
  before: observed.live,
1425
3131
  after: input.unit.spec
1426
3132
  };
1427
3133
  }
1428
3134
  function reconcileCloudflareUnit(input, diff) {
1429
- const cacheKey = `cloudflare:reconcile:${input.unit.target.kind === "persistent" ? input.unit.target.scope : input.unit.target.branchName}`;
3135
+ const cacheKey = `cloudflare:apply:${input.unit.target.kind === "persistent" ? input.unit.target.scope : input.unit.target.branchName}`;
1430
3136
  const { state } = providerCache(input, cacheKey, () => {
1431
3137
  let attempt = 0;
1432
3138
  for (; ; ) {
@@ -1449,7 +3155,7 @@ function reconcileCloudflareUnit(input, diff) {
1449
3155
  unit: input.unit,
1450
3156
  observed: refreshed,
1451
3157
  diff,
1452
- action: diff.action === "create" || diff.action === "update" ? "drift_correct" : diff.action,
3158
+ action: diff.action === "create" || diff.action === "update" ? "update" : diff.action,
1453
3159
  warnings: refreshed.warnings,
1454
3160
  resourceLocators: refreshed.locators,
1455
3161
  state: input.unit.unitType === "edge-worker" ? { workerName: state.workerName, lastDeployedUrl: state.lastDeployedUrl ?? null } : refreshed.live
@@ -1462,7 +3168,7 @@ function buildCloudflareAdapter(unitType) {
1462
3168
  supports(candidateUnitType, providerId) {
1463
3169
  return providerId === "cloudflare" && candidateUnitType === unitType;
1464
3170
  },
1465
- observe(input) {
3171
+ refresh(input) {
1466
3172
  return observeCloudflareUnit(input);
1467
3173
  },
1468
3174
  requiredPostconditions(input) {
@@ -1507,10 +3213,10 @@ function buildCloudflareAdapter(unitType) {
1507
3213
  return [];
1508
3214
  }
1509
3215
  },
1510
- plan(input) {
3216
+ diff(input) {
1511
3217
  return buildCloudflareDiff(input, input.observed);
1512
3218
  },
1513
- reconcile(input) {
3219
+ apply(input) {
1514
3220
  return reconcileCloudflareUnit(input, input.diff);
1515
3221
  },
1516
3222
  verify(input) {
@@ -1523,12 +3229,12 @@ function buildCloudflareAdapter(unitType) {
1523
3229
  unit: input.unit,
1524
3230
  observed: input.observed,
1525
3231
  diff: {
1526
- action: "destroy",
3232
+ action: "delete",
1527
3233
  reasons: ["target destroyed"],
1528
3234
  before: input.observed.live,
1529
3235
  after: {}
1530
3236
  },
1531
- action: "destroy",
3237
+ action: "delete",
1532
3238
  warnings: [],
1533
3239
  resourceLocators: {},
1534
3240
  state: {},
@@ -1541,6 +3247,31 @@ function relativeRailwayRootDir(tenantRoot, serviceRoot) {
1541
3247
  const resolved = relative(tenantRoot, serviceRoot).replace(/\\/gu, "/");
1542
3248
  return !resolved || resolved === "" ? "." : resolved;
1543
3249
  }
3250
+ function railwayServiceRootDirectory(tenantRoot, service) {
3251
+ const sourceRootDirectory = String(service.sourceRootDirectory ?? "").trim();
3252
+ if (sourceRootDirectory) {
3253
+ return sourceRootDirectory;
3254
+ }
3255
+ return relativeRailwayRootDir(service.application?.root ?? tenantRoot, service.rootDir);
3256
+ }
3257
+ function configuredMarketDatabaseService(tenantRoot, deployConfig) {
3258
+ if (deployConfig.services?.treeseedDatabase) {
3259
+ return treeseedDatabaseDescriptor(deployConfig.services.treeseedDatabase, deployConfig.slug);
3260
+ }
3261
+ for (const application of discoverTreeseedApplications(tenantRoot)) {
3262
+ const service = application.config.services?.treeseedDatabase;
3263
+ if (service) {
3264
+ return treeseedDatabaseDescriptor(service, application.config.slug);
3265
+ }
3266
+ }
3267
+ return null;
3268
+ }
3269
+ function treeseedDatabaseDescriptor(service, slug) {
3270
+ return {
3271
+ service,
3272
+ serviceName: typeof service.railway?.serviceName === "string" && service.railway.serviceName.trim() ? service.railway.serviceName.trim() : `${slug ?? "treeseed-api"}-postgres`
3273
+ };
3274
+ }
1544
3275
  async function ensureRailwayEnvironmentForService({
1545
3276
  service,
1546
3277
  project,
@@ -1559,16 +3290,6 @@ async function ensureRailwayEnvironmentForService({
1559
3290
  throw error;
1560
3291
  }
1561
3292
  }
1562
- ensureRailwayProjectContext({
1563
- ...service,
1564
- projectId: project.id,
1565
- projectName: project.name ?? service.projectName,
1566
- railwayEnvironment: environmentName
1567
- }, {
1568
- env,
1569
- allowFailure: true,
1570
- capture: true
1571
- });
1572
3293
  for (let attempt = 0; attempt < 12; attempt += 1) {
1573
3294
  const environments = await listRailwayEnvironments({ projectId: project.id, env });
1574
3295
  const existing = environments.find((environment) => environment.name === environmentName || environment.id === environmentName) ?? null;
@@ -1577,10 +3298,11 @@ async function ensureRailwayEnvironmentForService({
1577
3298
  }
1578
3299
  await new Promise((resolve2) => setTimeout(resolve2, 2500));
1579
3300
  }
1580
- throw new Error(`Railway environment ${environmentName} was created through the CLI fallback but was not visible through the Railway API.`);
3301
+ throw new Error(`Railway API environment provisioning failed for ${project.name ?? service.projectName ?? project.id}/${environmentName}.`);
1581
3302
  }
1582
3303
  async function resolveRailwayTopologyForScope(input, scope, {
1583
3304
  ensure = false,
3305
+ ensureServices = ensure,
1584
3306
  refresh = false,
1585
3307
  serviceKeys,
1586
3308
  includeInstances = ensure,
@@ -1592,21 +3314,28 @@ async function resolveRailwayTopologyForScope(input, scope, {
1592
3314
  const env = buildRailwayEnv(input, scope);
1593
3315
  const deployState = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target: toDeployTarget(input.context.target) });
1594
3316
  const services = configuredRailwayServices(input.context.tenantRoot, scope).filter((service) => normalizedServiceKeys.includes("__all__") || normalizedServiceKeys.includes(service.key));
3317
+ traceRailwayReconcile(env, "topology:start", `scope=${scope} ensure=${ensure ? "yes" : "no"} services=${services.map((service) => service.key).join(",")}`);
1595
3318
  let workspace = null;
1596
3319
  const knownProjects = [];
1597
3320
  const knownProjectIds = [...new Set(services.map((service) => service.projectId || deployState.services?.[service.key]?.projectId || "").filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim()))];
1598
3321
  for (const projectId of knownProjectIds) {
3322
+ traceRailwayReconcile(env, "topology:known-project:get", projectId);
1599
3323
  const project = await getRailwayProject({ projectId, env });
3324
+ traceRailwayReconcile(env, "topology:known-project:done", `${projectId}:${project?.name ?? "(missing)"}`);
1600
3325
  if (project) {
1601
3326
  knownProjects.push(project);
1602
3327
  }
1603
3328
  }
1604
3329
  if (knownProjects.length === 0 || services.some((service) => !(service.projectId || deployState.services?.[service.key]?.projectId))) {
3330
+ traceRailwayReconcile(env, "topology:workspace:get", "start");
1605
3331
  workspace = await resolveRailwayWorkspaceContext({ env });
3332
+ traceRailwayReconcile(env, "topology:workspace:done", workspace.name);
3333
+ traceRailwayReconcile(env, "topology:projects:list", workspace.name);
1606
3334
  const listedProjects = await listRailwayProjects({
1607
3335
  env,
1608
3336
  workspaceId: workspace.id
1609
3337
  });
3338
+ traceRailwayReconcile(env, "topology:projects:list:done", String(listedProjects.length));
1610
3339
  for (const project of listedProjects) {
1611
3340
  if (!knownProjects.find((entry) => entry?.id === project.id)) {
1612
3341
  knownProjects.push(project);
@@ -1626,16 +3355,30 @@ async function resolveRailwayTopologyForScope(input, scope, {
1626
3355
  }
1627
3356
  const resolvedServices = /* @__PURE__ */ new Map();
1628
3357
  for (const service of services) {
3358
+ traceRailwayReconcile(env, "topology:service:start", service.key);
1629
3359
  const persistedService = deployState.services?.[service.key] ?? {};
1630
3360
  const resolvedProjectId = service.projectId ?? persistedService.projectId ?? "";
1631
3361
  const resolvedProjectName = service.projectName ?? persistedService.projectName ?? "";
1632
3362
  const resolvedServiceId = service.serviceId ?? persistedService.serviceId ?? "";
1633
3363
  const resolvedServiceName = service.serviceName ?? persistedService.serviceName ?? "";
1634
3364
  let project = projectsByKey.get(resolvedProjectId) ?? projectsByKey.get(resolvedProjectName) ?? null;
3365
+ if (project && (!Array.isArray(project.services) || project.services.length === 0 || !Array.isArray(project.environments) || project.environments.length === 0)) {
3366
+ const hydratedProject = await getRailwayProject({ projectId: project.id, env });
3367
+ if (hydratedProject) {
3368
+ project = hydratedProject;
3369
+ projectsByKey.set(project.id, project);
3370
+ projectsByKey.set(project.name, project);
3371
+ }
3372
+ }
3373
+ if (project) {
3374
+ traceRailwayReconcile(env, "topology:project:resolved", `${service.key}:${project.name}:services=${project.services.map((entry) => entry.name).join(",") || "(none)"}:envs=${project.environments.map((entry) => entry.name).join(",") || "(none)"}`);
3375
+ }
1635
3376
  if (!project && ensure) {
1636
3377
  if (!workspace) {
3378
+ traceRailwayReconcile(env, "topology:workspace", "resolving workspace");
1637
3379
  workspace = await resolveRailwayWorkspaceContext({ env });
1638
3380
  }
3381
+ traceRailwayReconcile(env, "topology:project:ensure", `${service.key}:${resolvedProjectName || resolvedProjectId}`);
1639
3382
  const ensuredProject = await ensureRailwayProject({
1640
3383
  projectId: resolvedProjectId,
1641
3384
  projectName: resolvedProjectName,
@@ -1648,7 +3391,11 @@ async function resolveRailwayTopologyForScope(input, scope, {
1648
3391
  projectsByKey.set(project.name, project);
1649
3392
  }
1650
3393
  let environment = project?.environments.find((entry) => entry.name === service.railwayEnvironment || entry.id === service.railwayEnvironment) ?? null;
3394
+ if (project) {
3395
+ traceRailwayReconcile(env, "topology:environment:resolved", `${service.key}:${environment?.name ?? "(none)"}:${service.railwayEnvironment}`);
3396
+ }
1651
3397
  if (project && !environment && ensure) {
3398
+ traceRailwayReconcile(env, "topology:environment:ensure", `${service.key}:${service.railwayEnvironment}`);
1652
3399
  environment = await ensureRailwayEnvironmentForService({
1653
3400
  service,
1654
3401
  project,
@@ -1662,12 +3409,30 @@ async function resolveRailwayTopologyForScope(input, scope, {
1662
3409
  projectsByKey.set(project.id, project);
1663
3410
  projectsByKey.set(project.name, project);
1664
3411
  }
3412
+ if (project && environment && (!Array.isArray(project.services) || project.services.length === 0 || !project.services.some((entry) => entry.id === resolvedServiceId || entry.name === resolvedServiceName))) {
3413
+ const environmentServices = await listRailwayEnvironmentServices({ environmentId: environment.id, env }).catch(() => []);
3414
+ traceRailwayReconcile(env, "topology:environment-services:lookup", `${service.key}:${environment.name}:${environmentServices.map((entry) => entry.name).join(",") || "(none)"}`);
3415
+ if (environmentServices.length > 0) {
3416
+ project = {
3417
+ ...project,
3418
+ services: [...new Map([...project.services, ...environmentServices].map((entry) => [entry.id, entry])).values()]
3419
+ };
3420
+ projectsByKey.set(project.id, project);
3421
+ projectsByKey.set(project.name, project);
3422
+ traceRailwayReconcile(env, "topology:environment-services:resolved", `${service.key}:${environmentServices.map((entry) => entry.name).join(",")}`);
3423
+ }
3424
+ }
1665
3425
  let resolvedService = project?.services.find((entry) => entry.id === resolvedServiceId || entry.name === resolvedServiceName) ?? null;
1666
- if (project && !resolvedService && ensure) {
3426
+ if (project && !resolvedService && ensureServices) {
3427
+ traceRailwayReconcile(env, "topology:railway-service:ensure", `${service.key}:${resolvedServiceName || resolvedServiceId}`);
1667
3428
  resolvedService = (await ensureRailwayService({
1668
3429
  projectId: project.id,
1669
3430
  serviceId: resolvedServiceId,
1670
3431
  serviceName: resolvedServiceName,
3432
+ environmentId: environment?.id,
3433
+ imageRef: service.imageRef,
3434
+ sourceRepo: service.sourceRepo,
3435
+ sourceBranch: service.sourceBranch,
1671
3436
  env
1672
3437
  })).service;
1673
3438
  project = {
@@ -1676,16 +3441,37 @@ async function resolveRailwayTopologyForScope(input, scope, {
1676
3441
  };
1677
3442
  projectsByKey.set(project.id, project);
1678
3443
  projectsByKey.set(project.name, project);
3444
+ } else if (project && resolvedService && ensureServices && (service.imageRef || service.sourceRepo)) {
3445
+ traceRailwayReconcile(env, "topology:railway-service:repair-source", `${service.key}:${resolvedService.name}`);
3446
+ try {
3447
+ resolvedService = (await ensureRailwayService({
3448
+ projectId: project.id,
3449
+ serviceId: resolvedService.id,
3450
+ serviceName: resolvedServiceName || resolvedService.name,
3451
+ environmentId: environment?.id,
3452
+ imageRef: service.imageRef,
3453
+ sourceRepo: service.sourceRepo,
3454
+ sourceBranch: service.sourceBranch,
3455
+ env
3456
+ })).service;
3457
+ } catch (error) {
3458
+ if (!looksLikeRailwayExistingSourceRepairUnsupported(error)) {
3459
+ throw error;
3460
+ }
3461
+ traceRailwayReconcile(env, "topology:railway-service:repair-source-skipped", `${service.key}:${resolvedService.name}`);
3462
+ }
1679
3463
  }
1680
3464
  let instance = null;
1681
3465
  if (includeInstances && resolvedService && environment) {
1682
- if (ensure) {
3466
+ if (ensureServices) {
3467
+ traceRailwayReconcile(env, "topology:instance:ensure", `${service.key}:${resolvedService.name}`);
1683
3468
  instance = (await ensureRailwayServiceInstanceConfiguration({
1684
3469
  serviceId: resolvedService.id,
1685
3470
  environmentId: environment.id,
1686
3471
  buildCommand: service.buildCommand,
3472
+ dockerfilePath: !service.imageRef ? service.dockerfilePath ?? (existsSync(resolve(service.rootDir, "Dockerfile")) ? "/Dockerfile" : null) : null,
1687
3473
  startCommand: service.startCommand,
1688
- rootDirectory: relativeRailwayRootDir(input.context.tenantRoot, service.rootDir),
3474
+ rootDirectory: service.imageRef || service.sourceMode === "image" ? null : railwayServiceRootDirectory(input.context.tenantRoot, service),
1689
3475
  healthcheckPath: service.healthcheckPath,
1690
3476
  healthcheckTimeoutSeconds: service.healthcheckTimeoutSeconds,
1691
3477
  healthcheckIntervalSeconds: service.healthcheckIntervalSeconds,
@@ -1701,154 +3487,714 @@ async function resolveRailwayTopologyForScope(input, scope, {
1701
3487
  });
1702
3488
  }
1703
3489
  }
1704
- const currentVariables = includeVariables && project && environment && resolvedService ? await listRailwayVariables({
1705
- projectId: project.id,
1706
- environmentId: environment.id,
1707
- serviceId: resolvedService.id,
1708
- env
1709
- }) : {};
1710
- resolvedServices.set(service.key, {
1711
- configuredService: service,
1712
- project,
1713
- environment,
1714
- service: resolvedService,
1715
- instance,
1716
- currentVariables
1717
- });
3490
+ const currentVariables = includeVariables && project && environment && resolvedService ? await listRailwayVariables({
3491
+ projectId: project.id,
3492
+ environmentId: environment.id,
3493
+ serviceId: resolvedService.id,
3494
+ env
3495
+ }) : {};
3496
+ resolvedServices.set(service.key, {
3497
+ configuredService: service,
3498
+ project,
3499
+ environment,
3500
+ service: resolvedService,
3501
+ instance,
3502
+ currentVariables
3503
+ });
3504
+ traceRailwayReconcile(env, "topology:service:done", service.key);
3505
+ }
3506
+ traceRailwayReconcile(env, "topology:done", `scope=${scope}`);
3507
+ return {
3508
+ scope,
3509
+ env,
3510
+ workspace: workspace ?? {
3511
+ id: "",
3512
+ name: String(env.TREESEED_RAILWAY_WORKSPACE ?? "").trim()
3513
+ },
3514
+ services: resolvedServices
3515
+ };
3516
+ }, refresh);
3517
+ }
3518
+ function traceRailwayReconcile(env, stage, message) {
3519
+ if (env?.TREESEED_RECONCILE_TRACE === "1" || process.env.TREESEED_RECONCILE_TRACE === "1") {
3520
+ console.error(`[trsd][railway][${stage}] ${message}`);
3521
+ }
3522
+ }
3523
+ function railwayDriftSessionKey(scope) {
3524
+ return `railway:provider-drift:${scope}`;
3525
+ }
3526
+ function recordRailwayProviderDrift(input, scope, drift) {
3527
+ const key = railwayDriftSessionKey(scope);
3528
+ const current = input.context.session.get(key);
3529
+ const entries = Array.isArray(current) ? current : [];
3530
+ entries.push(drift);
3531
+ input.context.session.set(key, entries);
3532
+ }
3533
+ function railwayProviderDrift(input, scope) {
3534
+ const current = input.context.session.get(railwayDriftSessionKey(scope));
3535
+ return Array.isArray(current) ? current : [];
3536
+ }
3537
+ function activeRailwayVolumeInstances(volume) {
3538
+ const instances = Array.isArray(volume.instances) ? volume.instances : [];
3539
+ return instances.filter((instance) => {
3540
+ const state = String(instance.state ?? "READY").toUpperCase();
3541
+ const pendingDeletion = "isPendingDeletion" in instance && instance.isPendingDeletion === true;
3542
+ const deletedAt = "deletedAt" in instance && typeof instance.deletedAt === "string" && instance.deletedAt.trim();
3543
+ return !pendingDeletion && !deletedAt && state !== "DELETING" && state !== "DELETED";
3544
+ });
3545
+ }
3546
+ function isRailwayCapacityProviderService(service) {
3547
+ return String(service.key ?? "").startsWith("capacityProvider");
3548
+ }
3549
+ function railwayServiceMatchesKey(service, key) {
3550
+ return service.key === key || service.instanceKey === key || service.serviceName === key || service.serviceId === key;
3551
+ }
3552
+ function configuredRailwayProjectSyncGroups(input, scope, serviceKeys) {
3553
+ const allServices = configuredRailwayServices(input.context.tenantRoot, scope).filter((service) => service.enabled !== false).filter((service) => !isRailwayCapacityProviderService(service));
3554
+ const selected = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? allServices.filter((service) => serviceKeys.some((key) => railwayServiceMatchesKey(service, key))) : allServices;
3555
+ const selectedProjectKeys = new Set(selected.map((service) => `${service.projectName}:::${service.railwayEnvironment}`));
3556
+ const grouped = /* @__PURE__ */ new Map();
3557
+ for (const service of allServices) {
3558
+ const key = `${service.projectName}:::${service.railwayEnvironment}`;
3559
+ if (!selectedProjectKeys.has(key)) {
3560
+ continue;
3561
+ }
3562
+ const services = grouped.get(key) ?? [];
3563
+ services.push(service);
3564
+ grouped.set(key, services);
3565
+ }
3566
+ return [...grouped.values()];
3567
+ }
3568
+ function railwayIacServiceInput(input, sync, service, scope) {
3569
+ if (scope === "staging" && service.imageRef) {
3570
+ throw new Error(`Railway staging service ${service.serviceName} must build from Git source, not Docker image ${service.imageRef}.`);
3571
+ }
3572
+ if (scope === "prod" && service.sourceMode === "git") {
3573
+ throw new Error(`Railway production service ${service.serviceName} must deploy an immutable image, not Git source.`);
3574
+ }
3575
+ const serviceSync = sync.forService(service.key, service);
3576
+ const deployVariablePrefix = service.serviceName.includes("treedx") || service.key.includes("treedx") ? "TREEDX" : "TREESEED";
3577
+ const sourceVariables = service.sourceMode === "git" ? {
3578
+ ...service.sourceRepo ? { [`${deployVariablePrefix}_DEPLOY_SOURCE_REPOSITORY`]: service.sourceRepo } : {},
3579
+ ...service.sourceBranch ? { [`${deployVariablePrefix}_DEPLOY_SOURCE_BRANCH`]: service.sourceBranch } : {},
3580
+ ...service.sourceCommit ? { [`${deployVariablePrefix}_DEPLOY_SOURCE_COMMIT`]: service.sourceCommit } : {}
3581
+ } : {};
3582
+ return {
3583
+ key: service.instanceKey ?? service.key,
3584
+ serviceName: service.serviceName,
3585
+ sourceMode: service.sourceMode,
3586
+ sourceRepo: service.sourceRepo,
3587
+ sourceBranch: service.sourceBranch,
3588
+ sourceCommit: service.sourceCommit,
3589
+ sourceRootDirectory: service.sourceRootDirectory ?? railwayServiceRootDirectory(input.context.tenantRoot, service),
3590
+ imageRef: service.imageRef,
3591
+ dockerfilePath: service.dockerfilePath,
3592
+ buildCommand: service.buildCommand,
3593
+ startCommand: service.startCommand,
3594
+ healthcheckPath: service.healthcheckPath,
3595
+ healthcheckTimeoutSeconds: service.healthcheckTimeoutSeconds,
3596
+ runtimeMode: service.runtimeMode,
3597
+ volumeMountPath: service.volumeMountPath,
3598
+ variables: {
3599
+ ...serviceSync.variables,
3600
+ ...sourceVariables
3601
+ },
3602
+ secrets: serviceSync.secrets
3603
+ };
3604
+ }
3605
+ function configuredRailwayIacDatabase(input, projectServices, detachVolumeIds = []) {
3606
+ if (!projectServices.some((service) => service.key === "api" || service.key === "operationsRunner")) {
3607
+ return null;
3608
+ }
3609
+ const treeseedDatabase = configuredMarketDatabaseService(input.context.tenantRoot, input.context.deployConfig);
3610
+ const treeseedDatabaseService = treeseedDatabase?.service;
3611
+ if (!treeseedDatabaseService || treeseedDatabaseService.enabled === false || treeseedDatabaseService.provider !== "railway" || treeseedDatabaseService.railway?.resourceType !== "postgres") {
3612
+ return null;
3613
+ }
3614
+ return {
3615
+ serviceName: treeseedDatabase.serviceName,
3616
+ environmentVariable: "TREESEED_DATABASE_URL",
3617
+ mountPath: "/var/lib/postgresql/data",
3618
+ detachVolumeIds
3619
+ };
3620
+ }
3621
+ function activeAttachedRailwayVolumeIds(volumes, serviceId, environmentId, desiredVolumeName) {
3622
+ if (!serviceId) {
3623
+ return [];
3624
+ }
3625
+ return volumes.filter((volume) => volume.instances.some(
3626
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && (volume.name !== desiredVolumeName || activeRailwayVolumeInstances({ instances: [instance] }).length === 0)
3627
+ )).map((volume) => volume.id);
3628
+ }
3629
+ function blockedPendingRailwayVolumeAttachments(volumes, serviceIdByName, environmentId) {
3630
+ const serviceNameById = new Map([...serviceIdByName.entries()].filter(([, serviceId]) => serviceId).map(([serviceName, serviceId]) => [serviceId, serviceName]));
3631
+ const blocked = [];
3632
+ for (const volume of volumes) {
3633
+ for (const instance of volume.instances) {
3634
+ if (!instance.serviceId || instance.environmentId !== environmentId) {
3635
+ continue;
3636
+ }
3637
+ const serviceName = serviceNameById.get(instance.serviceId);
3638
+ if (!serviceName) {
3639
+ continue;
3640
+ }
3641
+ if (activeRailwayVolumeInstances({ instances: [instance] }).length > 0) {
3642
+ continue;
3643
+ }
3644
+ blocked.push(`${serviceName}: volume ${volume.name ?? volume.id} (${volume.id}) is pending/deleted while still attached to service ${instance.serviceId} at ${instance.mountPath ?? "(unknown mount)"}; deletedAt=${instance.deletedAt ?? "(none)"} state=${instance.state ?? "(unknown)"}`);
3645
+ }
3646
+ }
3647
+ return blocked;
3648
+ }
3649
+ async function reconcileStaleOperationsRunnerResourcesForScope(input, topology) {
3650
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
3651
+ const desiredRunners = configuredRailwayServices(input.context.tenantRoot, scope).filter((service) => service.key === "operationsRunner").filter((service) => service.enabled !== false);
3652
+ const desiredServiceNames = new Set(desiredRunners.map((service) => service.serviceName).filter(Boolean));
3653
+ const desiredVolumeNames = new Set([...desiredServiceNames].map((serviceName) => `${serviceName}-volume`));
3654
+ if (desiredServiceNames.size === 0) {
3655
+ traceRailwayReconcile(topology.env, "sync:runner:cleanup-skip", "no desired operations runner service names");
3656
+ return;
3657
+ }
3658
+ const projectEntries = [...topology.services.values()].filter((entry) => entry.project?.id && entry.environment?.id).map((entry) => ({ project: entry.project, environment: entry.environment }));
3659
+ traceRailwayReconcile(
3660
+ topology.env,
3661
+ "sync:runner:cleanup-projects",
3662
+ projectEntries.map((entry) => `${entry.project.name}:${entry.environment.name}`).join(",") || "(none)"
3663
+ );
3664
+ const projects = new Map(projectEntries.map((entry) => [entry.project.id, entry]));
3665
+ for (const { project, environment } of projects.values()) {
3666
+ await reconcileStaleOperationsRunnerResourcesForProject(input, {
3667
+ scope,
3668
+ env: topology.env,
3669
+ project,
3670
+ environment,
3671
+ desiredServiceNames,
3672
+ desiredVolumeNames
3673
+ });
3674
+ }
3675
+ }
3676
+ async function reconcileStaleOperationsRunnerResourcesForProject(input, {
3677
+ scope,
3678
+ env,
3679
+ project,
3680
+ environment,
3681
+ desiredServiceNames,
3682
+ desiredVolumeNames
3683
+ }) {
3684
+ if (desiredServiceNames.size === 0 || desiredVolumeNames.size === 0) {
3685
+ traceRailwayReconcile(env, "sync:runner:delete-skip", `${project.name}: no desired runner names; refusing to prune operations runner resources`);
3686
+ return;
3687
+ }
3688
+ const services = await listRailwayServices({ projectId: project.id, env });
3689
+ const desiredServiceIds = new Set(services.filter((service) => desiredServiceNames.has(service.name)).map((service) => service.id));
3690
+ const staleServices = findStaleTreeseedOperationsRunnerResources(services, desiredServiceNames);
3691
+ traceRailwayReconcile(
3692
+ env,
3693
+ "sync:runner:observed-services",
3694
+ `${project.name}: desired=${[...desiredServiceNames].join(",") || "(none)"} stale=${staleServices.map((service) => service.name).join(",") || "(none)"}`
3695
+ );
3696
+ for (const service of staleServices) {
3697
+ traceRailwayReconcile(env, "sync:runner:block-stale-service", `${service.name}:${service.id}`);
3698
+ recordRailwayProviderDrift(input, scope, {
3699
+ kind: "railway.stale-operations-runner-service",
3700
+ action: "manual-repair-required",
3701
+ status: "blocked",
3702
+ projectId: project.id,
3703
+ environmentId: environment.id,
3704
+ serviceId: service.id,
3705
+ serviceName: service.name,
3706
+ reason: "Railway reports an undeclared operations runner service. Reconciliation refuses to delete existing services; adopt, rename, retain, or remove it through an explicit destroy workflow."
3707
+ });
3708
+ }
3709
+ const volumes = await listRailwayVolumes({ projectId: project.id, env }).catch(() => []);
3710
+ const staleVolumes = findStaleTreeseedOperationsRunnerResources(volumes, desiredVolumeNames).filter((volume) => {
3711
+ const activeInstances = activeRailwayVolumeInstances(volume);
3712
+ return volume.instances.length === 0 || activeInstances.length > 0;
3713
+ }).filter((volume) => activeRailwayVolumeInstances(volume).every((instance) => !desiredServiceIds.has(instance.serviceId ?? "")));
3714
+ traceRailwayReconcile(
3715
+ env,
3716
+ "sync:runner:observed-volumes",
3717
+ `${project.name}: desired=${[...desiredVolumeNames].join(",") || "(none)"} stale=${staleVolumes.map((volume) => volume.name ?? volume.id).join(",") || "(none)"}`
3718
+ );
3719
+ for (const volume of staleVolumes) {
3720
+ traceRailwayReconcile(env, "sync:runner:block-stale-volume", `${volume.name ?? volume.id}:${volume.id}`);
3721
+ recordRailwayProviderDrift(input, scope, {
3722
+ kind: "railway.stale-operations-runner-volume",
3723
+ action: "manual-repair-required",
3724
+ status: "blocked",
3725
+ projectId: project.id,
3726
+ environmentId: environment.id,
3727
+ volumeId: volume.id,
3728
+ volumeName: volume.name ?? null,
3729
+ reason: "Railway reports an undeclared operations runner volume. Reconciliation refuses destructive cleanup during repair; adopt, retain, or remove it through an explicit destroy workflow."
3730
+ });
3731
+ }
3732
+ }
3733
+ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKeys } = {}) {
3734
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
3735
+ const sync = collectRailwayEnvironmentSync(input);
3736
+ const selectedServiceKeys = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? [...new Set(serviceKeys.map((value) => String(value).trim()).filter(Boolean))] : void 0;
3737
+ traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope} dryRun=${dryRun ? "yes" : "no"}`);
3738
+ const projectGroups = configuredRailwayProjectSyncGroups(input, scope, selectedServiceKeys);
3739
+ if (projectGroups.length === 0) {
3740
+ return {
3741
+ scope,
3742
+ services: [],
3743
+ secrets: Object.keys(sync.secrets),
3744
+ variables: Object.keys(sync.variables),
3745
+ dryRun,
3746
+ workspace: ""
3747
+ };
3748
+ }
3749
+ let workspace = "";
3750
+ const syncedServices = [];
3751
+ for (const projectServices of projectGroups) {
3752
+ const serviceKeySet = [...new Set(projectServices.map((service) => service.key))];
3753
+ const topology = await resolveRailwayTopologyForScope(input, scope, {
3754
+ ensure: !dryRun,
3755
+ ensureServices: false,
3756
+ serviceKeys: serviceKeySet,
3757
+ includeInstances: false,
3758
+ includeVariables: false,
3759
+ refresh: true
3760
+ });
3761
+ workspace = topology.workspace.name;
3762
+ const resolvedEntry = [...topology.services.values()].find((entry) => entry.project && entry.environment) ?? null;
3763
+ const project = resolvedEntry?.project;
3764
+ const environment = resolvedEntry?.environment;
3765
+ traceRailwayReconcile(topology.env, "sync:topology", `project-services=${projectServices.map((service) => service.serviceName).join(",")}`);
3766
+ if (dryRun) {
3767
+ syncedServices.push(...projectServices);
3768
+ continue;
3769
+ }
3770
+ if (!project || !environment) {
3771
+ throw new Error(`Railway IaC reconciliation could not resolve project/environment for ${projectServices[0]?.projectName ?? "(unknown project)"}/${projectServices[0]?.railwayEnvironment ?? "(unknown environment)"}.`);
3772
+ }
3773
+ const liveServices = await listRailwayEnvironmentServices({ environmentId: environment.id, env: topology.env }).catch(() => project.services);
3774
+ const liveServiceByName = new Map(liveServices.map((service) => [service.name, service]));
3775
+ const liveVolumes = await listRailwayVolumes({ projectId: project.id, env: topology.env }).catch(() => []);
3776
+ const databaseDescriptor = configuredRailwayIacDatabase(input, projectServices);
3777
+ const serviceIdByName = new Map(projectServices.map((service) => {
3778
+ const liveService = liveServiceByName.get(service.serviceName) ?? project.services.find((candidate) => candidate.name === service.serviceName || candidate.id === service.serviceId) ?? null;
3779
+ return [service.serviceName, liveService?.id ?? null];
3780
+ }));
3781
+ if (databaseDescriptor) {
3782
+ const databaseLiveService2 = liveServiceByName.get(databaseDescriptor.serviceName) ?? project.services.find((candidate) => candidate.name === databaseDescriptor.serviceName) ?? null;
3783
+ serviceIdByName.set(databaseDescriptor.serviceName, databaseLiveService2?.id ?? null);
3784
+ }
3785
+ const pendingVolumeBlocks = blockedPendingRailwayVolumeAttachments(liveVolumes, serviceIdByName, environment.id);
3786
+ if (pendingVolumeBlocks.length > 0) {
3787
+ for (const reason of pendingVolumeBlocks) {
3788
+ recordRailwayProviderDrift(input, scope, {
3789
+ kind: "railway.pending-volume-attachment",
3790
+ action: "blocked",
3791
+ status: "blocked",
3792
+ projectId: project.id,
3793
+ environmentId: environment.id,
3794
+ reason
3795
+ });
3796
+ }
3797
+ throw new Error(`Railway has pending-deletion volume attachments that block official IaC reconciliation. ${pendingVolumeBlocks.join("; ")}`);
1718
3798
  }
1719
- return {
3799
+ const detachVolumeIdsByServiceName = new Map(projectServices.map((service) => {
3800
+ const liveService = liveServiceByName.get(service.serviceName) ?? project.services.find((candidate) => candidate.name === service.serviceName || candidate.id === service.serviceId) ?? null;
3801
+ return [
3802
+ service.serviceName,
3803
+ activeAttachedRailwayVolumeIds(liveVolumes, liveService?.id, environment.id, `${service.serviceName}-volume`)
3804
+ ];
3805
+ }));
3806
+ const databaseLiveService = databaseDescriptor ? liveServiceByName.get(databaseDescriptor.serviceName) ?? project.services.find((candidate) => candidate.name === databaseDescriptor.serviceName) ?? null : null;
3807
+ const databaseDetachVolumeIds = databaseDescriptor ? activeAttachedRailwayVolumeIds(liveVolumes, databaseLiveService?.id, environment.id, `${databaseDescriptor.serviceName}-volume`) : [];
3808
+ const databaseForIac = databaseDescriptor ? { ...databaseDescriptor, detachVolumeIds: databaseDetachVolumeIds, useNativePostgres: Boolean(databaseLiveService) } : null;
3809
+ const token = String(topology.env.TREESEED_RAILWAY_API_TOKEN ?? topology.env.RAILWAY_API_TOKEN ?? "").trim();
3810
+ if (!token) {
3811
+ throw new Error(`Railway IaC reconciliation requires TREESEED_RAILWAY_API_TOKEN for ${project.name}/${environment.name}.`);
3812
+ }
3813
+ const iacInput = {
3814
+ tenantRoot: input.context.tenantRoot,
1720
3815
  scope,
1721
- env,
1722
- workspace: workspace ?? {
1723
- id: "",
1724
- name: String(env.TREESEED_RAILWAY_WORKSPACE ?? "").trim()
1725
- },
1726
- services: resolvedServices
3816
+ projectName: project.name,
3817
+ projectId: project.id,
3818
+ environmentName: environment.name,
3819
+ environmentId: environment.id,
3820
+ railwayApiToken: token,
3821
+ railwayApiUrl: String(topology.env.TREESEED_RAILWAY_API_URL ?? topology.env.RAILWAY_API_URL ?? "").trim() || null,
3822
+ services: projectServices.map((service) => ({
3823
+ ...railwayIacServiceInput(input, sync, service, scope),
3824
+ detachVolumeIds: detachVolumeIdsByServiceName.get(service.serviceName) ?? []
3825
+ })),
3826
+ database: databaseForIac
1727
3827
  };
1728
- }, refresh);
1729
- }
1730
- async function syncRailwayEnvironmentForScope(input, { dryRun = false } = {}) {
1731
- const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
1732
- const sync = collectRailwayEnvironmentSync(input);
1733
- const topology = await resolveRailwayTopologyForScope(input, scope, {
1734
- ensure: !dryRun,
1735
- includeInstances: !dryRun,
1736
- includeVariables: false
1737
- });
1738
- if (!dryRun) {
1739
- await ensureRailwayMarketDatabaseForScope(input, topology);
1740
- for (const entry of topology.services.values()) {
1741
- if (!entry.project || !entry.environment || !entry.service) {
1742
- continue;
3828
+ const {
3829
+ applyRailwayIacProject,
3830
+ cleanupRailwayIacRender,
3831
+ planRailwayIacProject,
3832
+ renderRailwayIacProject,
3833
+ validateRailwayIacChangeSet
3834
+ } = await import("./providers/railway-iac.js");
3835
+ const rendered = renderRailwayIacProject(iacInput);
3836
+ try {
3837
+ traceRailwayReconcile(topology.env, "sync:iac-plan", `${project.name}/${environment.name}:${rendered.serviceNames.join(",")}:volumes=${rendered.volumeNames.join(",")}`);
3838
+ const plan = await planRailwayIacProject(iacInput, rendered);
3839
+ if (!plan.ok) {
3840
+ const diagnostics = plan.diagnostics.map((entry) => entry.message).filter(Boolean).join("; ");
3841
+ throw new Error(`Railway IaC plan failed for ${project.name}/${environment.name}${diagnostics ? `: ${diagnostics}` : "."}`);
1743
3842
  }
1744
- const serviceSync = sync.forService(entry.configuredService.key);
1745
- await upsertRailwayVariables({
1746
- projectId: entry.project.id,
1747
- environmentId: entry.environment.id,
1748
- serviceId: entry.service.id,
1749
- variables: {
1750
- ...serviceSync.variables,
1751
- ...serviceSync.secrets
1752
- },
1753
- env: topology.env
3843
+ const validation = validateRailwayIacChangeSet(plan.changeSet, {
3844
+ services: rendered.serviceNames,
3845
+ volumes: rendered.volumeNames,
3846
+ database: rendered.databaseName,
3847
+ scope
1754
3848
  });
1755
- if (entry.configuredService.volumeMountPath) {
1756
- const volumeName = entry.configuredService.key === "marketOperationsRunner" ? deriveRailwayMarketOperationsRunnerVolumeName(entry.service.name, entry.environment.name) : `${entry.service.name}-volume`;
1757
- const volume = await ensureRailwayServiceVolumeWithCliFallback({
1758
- tenantRoot: input.context.tenantRoot,
1759
- projectId: entry.project.id,
1760
- environmentId: entry.environment.id,
1761
- environmentName: entry.environment.name,
1762
- serviceId: entry.service.id,
1763
- serviceName: entry.service.name,
1764
- name: volumeName,
1765
- mountPath: entry.configuredService.volumeMountPath,
1766
- preferCli: entry.configuredService.key === "marketOperationsRunner",
1767
- env: topology.env
1768
- });
1769
- if (volume.instance?.serviceId !== entry.service.id || volume.instance?.environmentId !== entry.environment.id || volume.instance?.mountPath !== entry.configuredService.volumeMountPath) {
1770
- ensureRailwayProjectContext(entry.configuredService, { env: topology.env, capture: true });
1771
- let attachMessage = "";
1772
- let attached = false;
1773
- for (let attempt = 0; attempt < 5; attempt += 1) {
1774
- const attachResult = runRailway([
1775
- "volume",
1776
- "--service",
1777
- entry.service.id,
1778
- "attach",
1779
- "--volume",
1780
- volume.volume.id,
1781
- "--yes",
1782
- "--json"
1783
- ], {
1784
- cwd: entry.configuredService.rootDir,
1785
- capture: true,
1786
- allowFailure: true,
1787
- env: topology.env
1788
- });
1789
- if ((attachResult.status ?? 1) === 0) {
1790
- attached = true;
1791
- break;
1792
- }
1793
- attachMessage = attachResult.stderr?.trim() || attachResult.stdout?.trim() || "";
1794
- if (/already mounted/iu.test(attachMessage)) {
1795
- attached = true;
1796
- break;
1797
- }
1798
- if (!/volume .*not found|not found|does not exist/iu.test(attachMessage)) {
1799
- break;
1800
- }
1801
- sleepMs(2e3 * (attempt + 1));
1802
- }
1803
- if (!attached) {
1804
- throw new Error(attachMessage || `Railway volume attach failed for ${entry.service.name}.`);
1805
- }
1806
- }
3849
+ if (!validation.ok) {
3850
+ throw new Error(`Railway IaC plan rejected for ${project.name}/${environment.name}: ${validation.blockedReasons.join("; ")}`);
1807
3851
  }
3852
+ const apply = await applyRailwayIacProject(iacInput, rendered);
3853
+ if (!apply.ok) {
3854
+ const diagnostics = apply.diagnostics.map((entry) => entry.message).filter(Boolean).join("; ");
3855
+ throw new Error(`Railway IaC apply failed for ${project.name}/${environment.name}${diagnostics ? `: ${diagnostics}` : "."}`);
3856
+ }
3857
+ syncedServices.push(...projectServices);
3858
+ } finally {
3859
+ cleanupRailwayIacRender(rendered);
1808
3860
  }
1809
3861
  }
3862
+ traceRailwayReconcile(input.context.env, "sync:done", `scope=${scope}`);
1810
3863
  return {
1811
3864
  scope,
1812
- services: [...topology.services.values()].map((entry) => entry.configuredService),
3865
+ services: syncedServices,
1813
3866
  secrets: Object.keys(sync.secrets),
1814
3867
  variables: Object.keys(sync.variables),
1815
3868
  dryRun,
1816
- workspace: topology.workspace.name
3869
+ workspace
3870
+ };
3871
+ }
3872
+ function shouldDeployRailwayServiceBySourceUpload(scope, service) {
3873
+ return scope === "staging" && service.sourceMode === "git" && Boolean(service.sourceRepo) && !service.dockerfilePath && service.key !== "capacityProviderManager" && service.key !== "capacityProviderRunner" && !service.imageRef;
3874
+ }
3875
+ function deployRailwayServiceBySourceUpload(input, entry, env, scope) {
3876
+ if (!entry.project?.id || !entry.environment?.name || !entry.service?.name) {
3877
+ throw new Error(`Railway source upload deploy requires resolved project, environment, and service for ${entry.configuredService.key}.`);
3878
+ }
3879
+ const rootDir = String(entry.configuredService.rootDir ?? "").trim();
3880
+ if (!rootDir || !existsSync(rootDir)) {
3881
+ throw new Error(`Railway source upload deploy root does not exist for ${entry.configuredService.key}: ${rootDir || "(unset)"}.`);
3882
+ }
3883
+ const uploadContext = prepareRailwaySourceUploadContext(input, entry.configuredService, rootDir);
3884
+ const uploadRoot = uploadContext.rootDir;
3885
+ const args = [
3886
+ "trsd",
3887
+ "railway",
3888
+ "--environment",
3889
+ scope,
3890
+ "--",
3891
+ "up",
3892
+ "--service",
3893
+ entry.service.name,
3894
+ "--project",
3895
+ entry.project.id,
3896
+ "--environment",
3897
+ entry.environment.name,
3898
+ "--ci",
3899
+ "--detach",
3900
+ "--path-as-root",
3901
+ ...uploadContext.noGitignore ? ["--no-gitignore"] : [],
3902
+ "--verbose",
3903
+ "--message",
3904
+ `trsd staging source upload ${entry.configuredService.key}`,
3905
+ uploadRoot
3906
+ ];
3907
+ const commandEnv = {
3908
+ ...process.env,
3909
+ ...env,
3910
+ TREESEED_RECONCILE_TRACE: env.TREESEED_RECONCILE_TRACE ?? process.env.TREESEED_RECONCILE_TRACE
1817
3911
  };
3912
+ try {
3913
+ const result = spawnSync("npx", args, {
3914
+ cwd: input.context.tenantRoot,
3915
+ env: commandEnv,
3916
+ encoding: "utf8",
3917
+ stdio: ["ignore", "pipe", "pipe"],
3918
+ maxBuffer: 1024 * 1024 * 20
3919
+ });
3920
+ if ((result.status ?? 1) !== 0) {
3921
+ const stderr = String(result.stderr ?? "").split("\n").slice(-120).join("\n");
3922
+ const stdout = String(result.stdout ?? "").split("\n").slice(-120).join("\n");
3923
+ throw new Error(`Railway source upload deploy failed for ${entry.configuredService.key} with status ${result.status ?? 1}.
3924
+ ${stderr || stdout}`);
3925
+ }
3926
+ } finally {
3927
+ if (uploadContext.cleanupDir) {
3928
+ rmSync(uploadContext.cleanupDir, { recursive: true, force: true });
3929
+ }
3930
+ }
3931
+ }
3932
+ function prepareRailwaySourceUploadContext(input, service, rootDir) {
3933
+ const role = service.key === "capacityProviderManager" ? "manager" : service.key === "capacityProviderRunner" ? "runner" : null;
3934
+ if (!role) {
3935
+ return { rootDir, cleanupDir: null, noGitignore: false };
3936
+ }
3937
+ const cleanupDir = mkdtempSync(resolve(tmpdir(), `trsd-railway-agent-${role}-`));
3938
+ const contextRoot = resolve(cleanupDir, "context");
3939
+ mkdirSync(contextRoot, { recursive: true });
3940
+ for (const entry of ["src", "scripts", "templates", "docs"]) {
3941
+ const source = resolve(rootDir, entry);
3942
+ if (existsSync(source)) {
3943
+ cpSync(source, resolve(contextRoot, entry), { recursive: true });
3944
+ }
3945
+ }
3946
+ for (const entry of ["docker-entrypoint.sh", "package.json", "package-lock.json", "tsconfig.json", "tsconfig.dist.json"]) {
3947
+ const source = resolve(rootDir, entry);
3948
+ if (existsSync(source)) {
3949
+ cpSync(source, resolve(contextRoot, entry));
3950
+ }
3951
+ }
3952
+ const sdkRoot = resolve(rootDir, "..", "sdk");
3953
+ let sdkTarballPrepared = false;
3954
+ if (existsSync(resolve(sdkRoot, "package.json"))) {
3955
+ const pack = spawnSync("npm", ["pack", "--ignore-scripts", "--pack-destination", contextRoot], {
3956
+ cwd: sdkRoot,
3957
+ env: process.env,
3958
+ encoding: "utf8",
3959
+ stdio: ["ignore", "pipe", "pipe"],
3960
+ maxBuffer: 1024 * 1024 * 20
3961
+ });
3962
+ if ((pack.status ?? 1) !== 0) {
3963
+ const stderr = String(pack.stderr ?? "").split("\n").slice(-80).join("\n");
3964
+ const stdout = String(pack.stdout ?? "").split("\n").slice(-80).join("\n");
3965
+ throw new Error(`Failed to pack local SDK for Agent ${role} Railway source upload context.
3966
+ ${stderr || stdout}`);
3967
+ }
3968
+ const filename = String(pack.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).at(-1);
3969
+ if (!filename || !existsSync(resolve(contextRoot, filename))) {
3970
+ throw new Error(`Local SDK pack did not produce a tarball for Agent ${role} Railway source upload context.`);
3971
+ }
3972
+ cpSync(resolve(contextRoot, filename), resolve(contextRoot, "treeseed-sdk.tgz"));
3973
+ if (filename !== "treeseed-sdk.tgz") {
3974
+ rmSync(resolve(contextRoot, filename), { force: true });
3975
+ }
3976
+ sdkTarballPrepared = true;
3977
+ }
3978
+ const dockerfile = [
3979
+ "FROM node:22-alpine AS builder",
3980
+ "WORKDIR /app",
3981
+ "RUN apk add --no-cache ca-certificates git openssh-client",
3982
+ sdkTarballPrepared ? "COPY package.json package-lock.json treeseed-sdk.tgz ./" : "COPY package.json package-lock.json ./",
3983
+ sdkTarballPrepared ? "RUN npm ci --include=dev --ignore-scripts \\\n && npm install --include=dev --ignore-scripts ./treeseed-sdk.tgz" : "RUN npm ci --include=dev --ignore-scripts",
3984
+ "COPY . .",
3985
+ "RUN npm run build:dist \\",
3986
+ " && npm prune --omit=dev --ignore-scripts \\",
3987
+ " && rm -rf node_modules/@github node_modules/@openai node_modules/@cloudflare node_modules/@railway \\",
3988
+ " node_modules/wrangler node_modules/miniflare node_modules/workerd \\",
3989
+ " node_modules/playwright node_modules/playwright-core node_modules/gpt-tokenizer \\",
3990
+ " node_modules/@remotion node_modules/remotion node_modules/@rspack \\",
3991
+ " node_modules/@repomix node_modules/repomix node_modules/@esbuild node_modules/esbuild \\",
3992
+ " node_modules/webpack node_modules/typescript node_modules/@mediabunny node_modules/mediabunny \\",
3993
+ " node_modules/web-tree-sitter node_modules/caniuse-lite node_modules/@img node_modules/@secretlint \\",
3994
+ " node_modules/lightningcss* node_modules/vite \\",
3995
+ " && npm cache clean --force \\",
3996
+ " && rm -rf src scripts test .git .github",
3997
+ "FROM node:22-alpine AS runtime",
3998
+ "WORKDIR /app",
3999
+ "ENV NODE_ENV=production TREESEED_PROVIDER_DATA_DIR=/data",
4000
+ "RUN apk add --no-cache ca-certificates tini util-linux git openssh-client \\",
4001
+ " && mkdir -p /data",
4002
+ "COPY --from=builder --chown=65532:65532 /app/package.json /app/package-lock.json ./",
4003
+ "COPY --from=builder --chown=65532:65532 /app/node_modules ./node_modules",
4004
+ "COPY --from=builder --chown=65532:65532 /app/dist ./dist",
4005
+ "COPY --from=builder --chown=65532:65532 /app/templates ./templates",
4006
+ "COPY --from=builder --chown=65532:65532 /app/docs ./docs",
4007
+ "COPY --from=builder --chown=65532:65532 /app/docker-entrypoint.sh ./docker-entrypoint.sh",
4008
+ "RUN chmod 0755 /app/docker-entrypoint.sh \\",
4009
+ " && chown -R 65532:65532 /data",
4010
+ "EXPOSE 3100",
4011
+ 'ENTRYPOINT ["tini", "--", "/app/docker-entrypoint.sh"]',
4012
+ `ENV TREESEED_PROVIDER_ROLE=${role}`,
4013
+ `CMD ["${role}"]`,
4014
+ ""
4015
+ ].join("\n");
4016
+ writeFileSync(
4017
+ resolve(contextRoot, "Dockerfile"),
4018
+ dockerfile
4019
+ );
4020
+ return { rootDir: contextRoot, cleanupDir, noGitignore: true };
4021
+ }
4022
+ async function deployRailwayServiceInstanceWithSourceRepair(entry, env) {
4023
+ if (!entry.environment || !entry.service) {
4024
+ return;
4025
+ }
4026
+ const deploy = () => deployRailwayServiceInstance({
4027
+ serviceId: entry.service.id,
4028
+ environmentId: entry.environment.id,
4029
+ env
4030
+ });
4031
+ if (entry.configuredService.sourceMode === "git" && entry.configuredService.sourceRepo && !entry.configuredService.imageRef) {
4032
+ await repairRailwayServiceSource(entry, env);
4033
+ }
4034
+ try {
4035
+ await deploy();
4036
+ return;
4037
+ } catch (error) {
4038
+ const message = error instanceof Error ? error.message : String(error ?? "");
4039
+ if (/Problem processing request/iu.test(message)) {
4040
+ traceRailwayReconcile(env, "sync:deploy-skipped", `${entry.configuredService.key}:${entry.service.name}:problem-processing-request`);
4041
+ return;
4042
+ }
4043
+ if (!/Deployment not found/iu.test(message)) {
4044
+ throw error;
4045
+ }
4046
+ }
4047
+ const repaired = await repairRailwayServiceSource(entry, env);
4048
+ if (repaired) {
4049
+ try {
4050
+ await deploy();
4051
+ return;
4052
+ } catch (error) {
4053
+ const message = error instanceof Error ? error.message : String(error ?? "");
4054
+ if (/Problem processing request/iu.test(message)) {
4055
+ traceRailwayReconcile(env, "sync:deploy-skipped", `${entry.configuredService.key}:${entry.service.name}:problem-processing-request`);
4056
+ return;
4057
+ }
4058
+ if (!/Deployment not found/iu.test(message)) {
4059
+ throw error;
4060
+ }
4061
+ }
4062
+ }
4063
+ throw new Error(
4064
+ `Railway deployment for existing service ${entry.service.name} (${entry.service.id}) was not found; refusing to delete and recreate an existing service. Repair the service in place or restore a deployable service source before rerunning reconciliation.`
4065
+ );
4066
+ }
4067
+ async function repairRailwayServiceSource(entry, env) {
4068
+ if (!entry.service) {
4069
+ return false;
4070
+ }
4071
+ const sourceRepo = String(entry.configuredService.sourceRepo ?? "").trim();
4072
+ if (sourceRepo) {
4073
+ await updateRailwayServiceGitSource({
4074
+ serviceId: entry.service.id,
4075
+ sourceRepo,
4076
+ sourceBranch: entry.configuredService.sourceBranch,
4077
+ env
4078
+ });
4079
+ return true;
4080
+ }
4081
+ const imageRef = String(entry.configuredService.imageRef ?? "").trim();
4082
+ if (imageRef) {
4083
+ await updateRailwayServiceImageSource({
4084
+ serviceId: entry.service.id,
4085
+ imageRef,
4086
+ env
4087
+ });
4088
+ return true;
4089
+ }
4090
+ return false;
4091
+ }
4092
+ function looksLikeRailwayExistingSourceRepairUnsupported(error) {
4093
+ const message = error instanceof Error ? error.message : String(error ?? "");
4094
+ return /source update .*existing service|existing service .*source update|provider-supported .*source update|ServiceUpdateInput|Problem processing request|User does not have access to the repo/iu.test(message);
1818
4095
  }
1819
4096
  async function ensureRailwayMarketDatabaseForScope(input, topology) {
1820
- const marketDatabaseService = input.context.deployConfig.services?.marketDatabase;
1821
- if (!marketDatabaseService || marketDatabaseService.enabled === false || marketDatabaseService.provider !== "railway" || marketDatabaseService.railway?.resourceType !== "postgres") {
4097
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
4098
+ const treeseedDatabase = configuredMarketDatabaseService(input.context.tenantRoot, input.context.deployConfig);
4099
+ const treeseedDatabaseService = treeseedDatabase?.service;
4100
+ if (!treeseedDatabaseService || treeseedDatabaseService.enabled === false || treeseedDatabaseService.provider !== "railway" || treeseedDatabaseService.railway?.resourceType !== "postgres") {
1822
4101
  return;
1823
4102
  }
1824
- const firstService = [...topology.services.values()].find((entry) => entry.project && entry.environment);
4103
+ const serviceName = treeseedDatabase.serviceName;
4104
+ const firstService = [...topology.services.values()].find(
4105
+ (entry) => entry.project && entry.environment && ["api", "operationsRunner"].includes(entry.configuredService.key)
4106
+ );
1825
4107
  if (!firstService?.project || !firstService.environment) {
4108
+ await reconcileAccidentalMarketDatabaseServices(input, topology, serviceName, null);
1826
4109
  return;
1827
4110
  }
1828
- const baseName = typeof marketDatabaseService.railway?.serviceName === "string" && marketDatabaseService.railway.serviceName.trim() ? marketDatabaseService.railway.serviceName.trim() : `${input.context.deployConfig.slug ?? "treeseed-market"}-postgres`;
1829
- const serviceName = `${baseName.replace(/-(staging|prod|production)$/u, "")}-${topology.scope === "prod" ? "prod" : topology.scope}`;
1830
- let postgresService = firstService.project.services.find((entry) => entry.name === serviceName || entry.id === serviceName) ?? null;
4111
+ await reconcileAccidentalMarketDatabaseServices(input, topology, serviceName, firstService.project.id);
4112
+ let postgresService;
4113
+ try {
4114
+ postgresService = (await ensureRailwayPostgresService({
4115
+ projectId: firstService.project.id,
4116
+ environmentId: firstService.environment.id,
4117
+ serviceName,
4118
+ env: topology.env
4119
+ })).service;
4120
+ } catch (error) {
4121
+ const detail = error instanceof Error ? error.message : String(error ?? "");
4122
+ throw new Error(`Railway API Postgres provisioning unsupported for ${serviceName}. ${detail}`);
4123
+ }
1831
4124
  if (!postgresService) {
1832
- ensureRailwayProjectContext(firstService.configuredService, { env: topology.env, capture: true });
1833
- const addResult = runRailway(["add", "--database", "postgres", "--json"], {
1834
- cwd: firstService.configuredService.rootDir,
1835
- capture: true,
1836
- allowFailure: true,
4125
+ throw new Error(`Railway API Postgres provisioning unsupported for ${serviceName}. No service was returned.`);
4126
+ }
4127
+ try {
4128
+ const postgresVolume = await ensureRailwayServiceVolume({
4129
+ projectId: firstService.project.id,
4130
+ environmentId: firstService.environment.id,
4131
+ serviceId: postgresService.id,
4132
+ name: `${serviceName}-volume`,
4133
+ mountPath: "/var/lib/postgresql/data",
1837
4134
  env: topology.env
1838
4135
  });
1839
- if ((addResult.status ?? 1) !== 0) {
1840
- throw new Error(addResult.stderr?.trim() || addResult.stdout?.trim() || `Railway database provisioning failed for ${serviceName}.`);
4136
+ if (postgresVolume.instance?.serviceId !== postgresService.id || postgresVolume.instance?.environmentId !== firstService.environment.id || postgresVolume.instance?.mountPath !== "/var/lib/postgresql/data") {
4137
+ throw new Error(`observed volume ${postgresVolume.volume.name ?? postgresVolume.volume.id ?? "(unknown)"} is not attached to ${serviceName} at /var/lib/postgresql/data`);
1841
4138
  }
1842
- const payload = parseRailwayJsonPayload(addResult.stdout);
1843
- const createdServiceId = typeof payload?.serviceId === "string" && payload.serviceId.trim() ? payload.serviceId.trim() : "";
1844
- if (!createdServiceId) {
1845
- throw new Error(`Railway database provisioning did not return a service id for ${serviceName}.`);
4139
+ } catch (error) {
4140
+ const detail = error instanceof Error ? error.message : String(error ?? "");
4141
+ throw new Error(`Railway provider limitation while reconciling PostgreSQL volume name for ${serviceName}: ${detail}`);
4142
+ }
4143
+ const services = await listRailwayServices({ projectId: firstService.project.id, env: topology.env });
4144
+ for (const service of services) {
4145
+ if (service.id === postgresService.id || service.name === serviceName) {
4146
+ continue;
1846
4147
  }
1847
- postgresService = await updateRailwayServiceName({
1848
- serviceId: createdServiceId,
1849
- name: serviceName,
4148
+ const variables = await listRailwayVariables({
4149
+ projectId: firstService.project.id,
4150
+ environmentId: firstService.environment.id,
4151
+ serviceId: service.id,
1850
4152
  env: topology.env
1851
- });
4153
+ }).catch(() => ({}));
4154
+ const looksLikePostgres = typeof variables.DATABASE_URL === "string" && typeof variables.PGHOST === "string" && typeof variables.PGUSER === "string" && typeof variables.PGPASSWORD === "string";
4155
+ if (looksLikePostgres) {
4156
+ traceRailwayReconcile(topology.env, "sync:postgres:block-duplicate", `${service.name}:${service.id}`);
4157
+ recordRailwayProviderDrift(input, scope, {
4158
+ kind: "railway.duplicate-postgres-service",
4159
+ action: "manual-repair-required",
4160
+ status: "blocked",
4161
+ projectId: firstService.project.id,
4162
+ environmentId: firstService.environment.id,
4163
+ serviceId: service.id,
4164
+ serviceName: service.name,
4165
+ reason: `Railway reports an extra PostgreSQL service in ${firstService.project.name}. Reconciliation refuses to delete existing services; adopt, retain, or remove it through an explicit destroy workflow.`
4166
+ });
4167
+ }
4168
+ }
4169
+ const detachedPostgresVolumes = await listRailwayVolumes({
4170
+ projectId: firstService.project.id,
4171
+ env: topology.env
4172
+ });
4173
+ for (const volume of detachedPostgresVolumes) {
4174
+ if (isRetainedRailwayDetachedPostgresVolume(volume.name)) {
4175
+ traceRailwayReconcile(topology.env, "sync:postgres:retain-detached-volume", `${volume.name ?? volume.id}:${volume.id}`);
4176
+ continue;
4177
+ }
4178
+ const postgresInstances = volume.instances.filter(
4179
+ (instance) => instance.environmentId === firstService.environment?.id && instance.mountPath === "/var/lib/postgresql/data"
4180
+ );
4181
+ const activePostgresInstances = postgresInstances.filter((instance) => Boolean(instance.serviceId));
4182
+ const attachedToCanonical = postgresInstances.some((instance) => instance.serviceId === postgresService.id);
4183
+ const detached = postgresInstances.length > 0 && postgresInstances.every((instance) => !instance.serviceId);
4184
+ const detachedCanonicalVolume = volume.name === `${serviceName}-volume` && activePostgresInstances.length === 0;
4185
+ if ((detached || detachedCanonicalVolume) && !attachedToCanonical) {
4186
+ traceRailwayReconcile(topology.env, "sync:postgres:block-detached-volume", `${volume.name ?? volume.id}:${volume.id}`);
4187
+ recordRailwayProviderDrift(input, scope, {
4188
+ kind: "railway.detached-postgres-volume",
4189
+ action: "manual-repair-required",
4190
+ status: "blocked",
4191
+ projectId: firstService.project.id,
4192
+ environmentId: firstService.environment.id,
4193
+ volumeId: volume.id,
4194
+ volumeName: volume.name ?? null,
4195
+ reason: "Railway reports a detached PostgreSQL volume. Reconciliation refuses destructive cleanup during repair; adopt, retain, or remove it through an explicit destroy workflow."
4196
+ });
4197
+ }
1852
4198
  }
1853
4199
  for (let attempt = 0; attempt < 20; attempt += 1) {
1854
4200
  const existingVolumes = await listRailwayVolumes({
@@ -1864,6 +4210,61 @@ async function ensureRailwayMarketDatabaseForScope(input, topology) {
1864
4210
  await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1865
4211
  }
1866
4212
  }
4213
+ async function reconcileAccidentalMarketDatabaseServices(input, topology, serviceName, ownerProjectId) {
4214
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
4215
+ const projectEntries = [...topology.services.values()].filter((entry) => entry.project?.id && entry.environment?.id).map((entry) => ({ project: entry.project, environment: entry.environment }));
4216
+ const projects = new Map(projectEntries.map((entry) => [entry.project.id, entry]));
4217
+ for (const { project, environment } of projects.values()) {
4218
+ if (ownerProjectId && project.id === ownerProjectId) {
4219
+ continue;
4220
+ }
4221
+ const services = await listRailwayServices({ projectId: project.id, env: topology.env }).catch(() => []);
4222
+ for (const service of services.filter((entry) => entry.name === serviceName)) {
4223
+ traceRailwayReconcile(topology.env, "sync:postgres:block-accidental-service", `${project.name}:${service.name}:${service.id}`);
4224
+ recordRailwayProviderDrift(input, scope, {
4225
+ kind: "railway.accidental-market-database-service",
4226
+ action: "manual-repair-required",
4227
+ status: "blocked",
4228
+ projectId: project.id,
4229
+ environmentId: environment.id,
4230
+ serviceId: service.id,
4231
+ serviceName: service.name,
4232
+ reason: `Railway reports undeclared Market database service ${service.name} in non-owner project ${project.name}. Reconciliation refuses to delete existing services; adopt, retain, or remove it through an explicit destroy workflow.`
4233
+ });
4234
+ }
4235
+ const volumes = await listRailwayVolumes({ projectId: project.id, env: topology.env }).catch(() => []);
4236
+ for (const volume of volumes) {
4237
+ if (isRetainedRailwayDetachedPostgresVolume(volume.name)) {
4238
+ continue;
4239
+ }
4240
+ const postgresInstances = volume.instances.filter(
4241
+ (instance) => instance.environmentId === environment.id && instance.mountPath === "/var/lib/postgresql/data"
4242
+ );
4243
+ if (postgresInstances.length === 0) {
4244
+ continue;
4245
+ }
4246
+ const activeServiceIds = new Set(services.map((service) => service.id));
4247
+ const stillAttached = postgresInstances.some((instance) => instance.serviceId && activeServiceIds.has(instance.serviceId));
4248
+ if (stillAttached) {
4249
+ continue;
4250
+ }
4251
+ traceRailwayReconcile(topology.env, "sync:postgres:block-accidental-volume", `${project.name}:${volume.name ?? volume.id}:${volume.id}`);
4252
+ recordRailwayProviderDrift(input, scope, {
4253
+ kind: "railway.accidental-market-database-volume",
4254
+ action: "manual-repair-required",
4255
+ status: "blocked",
4256
+ projectId: project.id,
4257
+ environmentId: environment.id,
4258
+ volumeId: volume.id,
4259
+ volumeName: volume.name ?? null,
4260
+ reason: `Railway reports undeclared Market database volume ${volume.name ?? volume.id} in non-owner project ${project.name}. Reconciliation refuses destructive cleanup during repair; adopt, retain, or remove it through an explicit destroy workflow.`
4261
+ });
4262
+ }
4263
+ }
4264
+ }
4265
+ function isRetainedRailwayDetachedPostgresVolume(value) {
4266
+ return String(value ?? "").trim().startsWith("retained-");
4267
+ }
1867
4268
  async function observeRailwayUnit(input, { refresh = false } = {}) {
1868
4269
  let attempt = 0;
1869
4270
  for (; ; ) {
@@ -1879,15 +4280,19 @@ async function observeRailwayUnit(input, { refresh = false } = {}) {
1879
4280
  includeVariables: false
1880
4281
  });
1881
4282
  const entry = topology.services.get(serviceKey) ?? null;
4283
+ const configuredService = entry?.configuredService ?? null;
4284
+ const usesPortableSource = Boolean(
4285
+ configuredService?.imageRef || configuredService?.sourceMode === "git" && configuredService?.sourceRepo
4286
+ );
1882
4287
  const configured = Boolean(
1883
- entry?.configuredService && (entry.configuredService.serviceName || entry.configuredService.serviceId) && (entry.configuredService.projectName || entry.configuredService.projectId) && existsSync(resolve(entry.configuredService.rootDir))
4288
+ configuredService && (configuredService.serviceName || configuredService.serviceId) && (configuredService.projectName || configuredService.projectId) && (usesPortableSource || existsSync(resolve(configuredService.rootDir)))
1884
4289
  );
1885
4290
  return {
1886
- exists: Boolean(entry?.configuredService),
4291
+ exists: Boolean(entry?.project && entry?.environment && entry?.service),
1887
4292
  status: entry?.project && entry?.environment && entry?.service && configured ? "ready" : "pending",
1888
4293
  live: {
1889
4294
  ...persisted ?? {},
1890
- ...entry?.configuredService ?? {},
4295
+ ...configuredService ?? {},
1891
4296
  project: entry?.project ?? null,
1892
4297
  environment: entry?.environment ?? null,
1893
4298
  service: entry?.service ?? null,
@@ -1911,32 +4316,48 @@ async function observeRailwayUnit(input, { refresh = false } = {}) {
1911
4316
  }
1912
4317
  }
1913
4318
  }
1914
- function collectRailwayEnvironmentSync(input) {
4319
+ function collectRailwayEnvironmentSync(input, valuesOverlay = {}) {
1915
4320
  const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
1916
- const values = resolveReconcileEnvironmentValues(input, scope);
1917
4321
  const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
4322
+ const configuredServices = configuredRailwayServices(input.context.tenantRoot, scope);
4323
+ const hasPublicTreeDxService = configuredServices.some((service) => service.key === "public-treedx-node-01" && service.enabled !== false);
4324
+ const machineValues = hasPublicTreeDxService ? normalizeEnvironmentValues(resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope)) : {};
4325
+ const baseValues = {
4326
+ ...resolveReconcileEnvironmentValues(input, scope),
4327
+ ...machineValues,
4328
+ ...valuesOverlay
4329
+ };
4330
+ const values = {
4331
+ ...baseValues,
4332
+ ...ensurePublicTreeDxSecretsForRailwaySync(input, scope, baseValues, registry)
4333
+ };
4334
+ if (scope !== "local") {
4335
+ sanitizeRemoteRailwayServiceUrls(input, scope, values);
4336
+ }
1918
4337
  const state = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target: toDeployTarget(input.context.target) });
1919
4338
  const serviceTargetsEntry = (entry, serviceKey) => {
1920
4339
  const targets = Array.isArray(entry.serviceTargets) ? entry.serviceTargets.map((value) => String(value).trim()).filter(Boolean) : [];
1921
4340
  return targets.length === 0 || targets.includes(serviceKey);
1922
4341
  };
1923
- const entriesForService = (target, serviceKey) => registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes(target) && serviceTargetsEntry(entry, serviceKey) && (target !== "railway-secret" || shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, entry.id))).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0);
1924
- const aggregateEntries = (target) => registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes(target) && (target !== "railway-secret" || shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, entry.id))).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0);
4342
+ const entriesForService = (target, serviceKey) => registry.entries.filter(
4343
+ (entry) => entry.scopes.includes(scope) && entry.targets.includes(target) && serviceTargetsEntry(entry, serviceKey)
4344
+ ).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0);
4345
+ const aggregateEntries = (target) => registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes(target)).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0);
1925
4346
  const secrets = Object.fromEntries(aggregateEntries("railway-secret"));
1926
4347
  const variables = Object.fromEntries(aggregateEntries("railway-var"));
1927
4348
  const apiOnlySecrets = {};
1928
4349
  const apiOnlyVariables = {};
1929
- const marketDatabaseService = input.context.deployConfig.services?.marketDatabase;
1930
- const marketDatabaseBaseName = typeof marketDatabaseService?.railway?.serviceName === "string" && marketDatabaseService.railway.serviceName.trim() ? marketDatabaseService.railway.serviceName.trim() : `${input.context.deployConfig.slug ?? "treeseed-market"}-postgres`;
1931
- const marketDatabaseServiceName = `${marketDatabaseBaseName.replace(/-(staging|prod|production)$/u, "")}-${scope === "prod" ? "prod" : scope}`;
1932
- const marketDatabaseUrl = typeof values.TREESEED_MARKET_DATABASE_URL === "string" && values.TREESEED_MARKET_DATABASE_URL.length > 0 ? values.TREESEED_MARKET_DATABASE_URL : marketDatabaseService?.enabled !== false && marketDatabaseService?.provider === "railway" ? `\${{${marketDatabaseServiceName}.DATABASE_URL}}` : "";
1933
- if (typeof values.CLOUDFLARE_API_TOKEN === "string" && values.CLOUDFLARE_API_TOKEN.length > 0 && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, "CLOUDFLARE_API_TOKEN")) {
1934
- secrets.CLOUDFLARE_API_TOKEN = values.CLOUDFLARE_API_TOKEN;
1935
- apiOnlySecrets.CLOUDFLARE_API_TOKEN = values.CLOUDFLARE_API_TOKEN;
1936
- }
1937
- if (typeof values.CLOUDFLARE_ACCOUNT_ID === "string" && values.CLOUDFLARE_ACCOUNT_ID.length > 0) {
1938
- variables.CLOUDFLARE_ACCOUNT_ID = values.CLOUDFLARE_ACCOUNT_ID;
1939
- apiOnlyVariables.CLOUDFLARE_ACCOUNT_ID = values.CLOUDFLARE_ACCOUNT_ID;
4350
+ const treeseedDatabase = configuredMarketDatabaseService(input.context.tenantRoot, input.context.deployConfig);
4351
+ const treeseedDatabaseService = treeseedDatabase?.service;
4352
+ const treeseedDatabaseServiceName = treeseedDatabase?.serviceName ?? "";
4353
+ const treeseedDatabaseUrl = typeof values.TREESEED_DATABASE_URL === "string" && values.TREESEED_DATABASE_URL.length > 0 ? values.TREESEED_DATABASE_URL : treeseedDatabaseService?.enabled !== false && treeseedDatabaseService?.provider === "railway" ? `\${{${treeseedDatabaseServiceName}.DATABASE_URL}}` : "";
4354
+ if (typeof values.TREESEED_CLOUDFLARE_API_TOKEN === "string" && values.TREESEED_CLOUDFLARE_API_TOKEN.length > 0 && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, "TREESEED_CLOUDFLARE_API_TOKEN")) {
4355
+ secrets.TREESEED_CLOUDFLARE_API_TOKEN = values.TREESEED_CLOUDFLARE_API_TOKEN;
4356
+ apiOnlySecrets.TREESEED_CLOUDFLARE_API_TOKEN = values.TREESEED_CLOUDFLARE_API_TOKEN;
4357
+ }
4358
+ if (typeof values.TREESEED_CLOUDFLARE_ACCOUNT_ID === "string" && values.TREESEED_CLOUDFLARE_ACCOUNT_ID.length > 0) {
4359
+ variables.TREESEED_CLOUDFLARE_ACCOUNT_ID = values.TREESEED_CLOUDFLARE_ACCOUNT_ID;
4360
+ apiOnlyVariables.TREESEED_CLOUDFLARE_ACCOUNT_ID = values.TREESEED_CLOUDFLARE_ACCOUNT_ID;
1940
4361
  }
1941
4362
  const siteDataDb = state.d1Databases?.SITE_DATA_DB;
1942
4363
  let apiD1DatabaseId = siteDataDb?.databaseId;
@@ -1948,28 +4369,183 @@ function collectRailwayEnvironmentSync(input) {
1948
4369
  variables.TREESEED_API_D1_DATABASE_ID = apiD1DatabaseId;
1949
4370
  apiOnlyVariables.TREESEED_API_D1_DATABASE_ID = apiD1DatabaseId;
1950
4371
  }
1951
- if (marketDatabaseUrl) {
1952
- secrets.TREESEED_MARKET_DATABASE_URL = marketDatabaseUrl;
4372
+ if (treeseedDatabaseUrl) {
4373
+ secrets.TREESEED_DATABASE_URL = treeseedDatabaseUrl;
1953
4374
  }
1954
4375
  return {
1955
4376
  scope,
1956
4377
  secrets,
1957
4378
  variables,
1958
- forService(serviceKey) {
4379
+ forService(serviceKey, configuredService) {
4380
+ const capacityVariables = capacityProviderVariablesForService(input, scope, values, serviceKey, configuredService);
4381
+ const capacitySecrets = capacityProviderSecretsForService(values, serviceKey);
4382
+ const serviceVariables = configuredService?.environmentVariables && typeof configuredService.environmentVariables === "object" ? Object.fromEntries(Object.entries(configuredService.environmentVariables).filter(([, value]) => typeof value === "string" && value.length > 0)) : {};
4383
+ const serviceSecretRefs = Array.isArray(configuredService?.secretRefs) ? configuredService.secretRefs.map((value) => String(value).trim()).filter(Boolean) : [];
4384
+ const serviceVariableRefs = Array.isArray(configuredService?.variableRefs) ? configuredService.variableRefs.map((value) => String(value).trim()).filter(Boolean) : [];
4385
+ const serviceValue = (key) => {
4386
+ const direct = values[key];
4387
+ if (typeof direct === "string" && direct.length > 0) return direct;
4388
+ if (key.startsWith("TREEDX_")) {
4389
+ const legacy = values[`TREESEED_${key}`];
4390
+ if (typeof legacy === "string" && legacy.length > 0) return legacy;
4391
+ }
4392
+ return void 0;
4393
+ };
4394
+ const serviceRefSecrets = Object.fromEntries(serviceSecretRefs.map((key) => [key, serviceValue(key)]).filter(([, value]) => typeof value === "string" && value.length > 0));
4395
+ const serviceRefVariables = Object.fromEntries(serviceVariableRefs.map((key) => [key, serviceValue(key)]).filter(([, value]) => typeof value === "string" && value.length > 0));
1959
4396
  return {
1960
4397
  secrets: {
1961
4398
  ...Object.fromEntries(entriesForService("railway-secret", serviceKey)),
1962
- ...marketDatabaseUrl && ["api", "marketOperationsRunner"].includes(serviceKey) ? { TREESEED_MARKET_DATABASE_URL: marketDatabaseUrl } : {},
1963
- ...serviceKey === "api" ? apiOnlySecrets : {}
4399
+ ...serviceRefSecrets,
4400
+ ...treeseedDatabaseUrl && ["api", "operationsRunner"].includes(serviceKey) ? { TREESEED_DATABASE_URL: treeseedDatabaseUrl } : {},
4401
+ ...serviceKey === "api" ? apiOnlySecrets : {},
4402
+ ...capacitySecrets
1964
4403
  },
1965
4404
  variables: {
4405
+ ...serviceVariables,
4406
+ ...serviceRefVariables,
1966
4407
  ...Object.fromEntries(entriesForService("railway-var", serviceKey)),
1967
- ...serviceKey === "api" ? apiOnlyVariables : {}
4408
+ ...serviceKey === "operationsRunner" ? { TREESEED_MANAGER_ID: scope } : {},
4409
+ ...serviceKey === "api" ? apiOnlyVariables : {},
4410
+ ...capacityVariables
1968
4411
  }
1969
4412
  };
1970
4413
  }
1971
4414
  };
1972
4415
  }
4416
+ function isLoopbackServiceUrl(value) {
4417
+ if (typeof value !== "string" || !value.trim()) return false;
4418
+ try {
4419
+ const url = new URL(value);
4420
+ return url.hostname === "127.0.0.1" || url.hostname === "localhost";
4421
+ } catch {
4422
+ return false;
4423
+ }
4424
+ }
4425
+ function sanitizeRemoteRailwayServiceUrls(input, scope, values) {
4426
+ const configuredApiDomain = resolveConfiguredSurfaceDomain(input.context.deployConfig, { kind: "persistent", scope }, "api");
4427
+ const configuredApiBaseUrl = configuredApiDomain ? `https://${configuredApiDomain}` : "";
4428
+ const apiBaseUrl = [
4429
+ values.TREESEED_MARKET_API_BASE_URL,
4430
+ values.TREESEED_STAGING_MARKET_API_BASE_URL,
4431
+ configuredApiBaseUrl
4432
+ ].find((value) => typeof value === "string" && value.trim() && !isLoopbackServiceUrl(value))?.trim();
4433
+ if (!apiBaseUrl) return;
4434
+ if (isLoopbackServiceUrl(values.TREESEED_API_BASE_URL)) {
4435
+ values.TREESEED_API_BASE_URL = apiBaseUrl;
4436
+ }
4437
+ for (const key of ["TREESEED_CENTRAL_MARKET_API_BASE_URL", "TREESEED_CATALOG_MARKET_API_BASE_URLS"]) {
4438
+ if (isLoopbackServiceUrl(values[key])) {
4439
+ values[key] = apiBaseUrl;
4440
+ }
4441
+ }
4442
+ }
4443
+ function capacityProviderRoleForService(serviceKey) {
4444
+ if (serviceKey === "capacityProviderManager") return "manager";
4445
+ if (serviceKey === "capacityProviderRunner") return "runner";
4446
+ return null;
4447
+ }
4448
+ function capacityProviderSecretsForService(values, serviceKey) {
4449
+ if (!capacityProviderRoleForService(serviceKey)) return {};
4450
+ const apiKey = String(values.TREESEED_CAPACITY_PROVIDER_API_KEY ?? "").trim();
4451
+ return apiKey ? { TREESEED_CAPACITY_PROVIDER_API_KEY: apiKey } : {};
4452
+ }
4453
+ function ensureCapacityProviderApiKeyForRailwaySync(input, scope) {
4454
+ const hasCapacityProviderService = configuredRailwayServices(input.context.tenantRoot, scope).some((service) => String(service.key).startsWith("capacityProvider") && service.enabled !== false);
4455
+ if (!hasCapacityProviderService) return null;
4456
+ const values = resolveReconcileEnvironmentValues(input, scope);
4457
+ const existing = String(values.TREESEED_CAPACITY_PROVIDER_API_KEY ?? "").trim();
4458
+ if (existing) return existing;
4459
+ const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
4460
+ const entry = registry.entries.find((candidate) => candidate.id === "TREESEED_CAPACITY_PROVIDER_API_KEY") ?? {
4461
+ id: "TREESEED_CAPACITY_PROVIDER_API_KEY",
4462
+ label: "Capacity provider API key",
4463
+ group: "capacity-provider",
4464
+ description: "Provider-scoped bearer key used by hosted capacity provider services.",
4465
+ howToGet: "Generated by Railway reconciliation when capacity provider services are enabled.",
4466
+ sensitivity: "secret",
4467
+ targets: ["railway-secret"],
4468
+ scopes: ["staging", "prod"],
4469
+ storage: "scoped",
4470
+ requirement: "optional",
4471
+ purposes: ["deploy", "config"]
4472
+ };
4473
+ const generated = `tscp_${randomBytes(32).toString("base64url")}`;
4474
+ setTreeseedMachineEnvironmentValue(input.context.tenantRoot, scope, entry, generated);
4475
+ return generated;
4476
+ }
4477
+ function ensurePublicTreeDxSecretsForRailwaySync(input, scope, values, registry) {
4478
+ const hasPublicTreeDxService = configuredRailwayServices(input.context.tenantRoot, scope).some((service) => service.key === "public-treedx-node-01" && service.enabled !== false);
4479
+ if (!hasPublicTreeDxService) return {};
4480
+ const generated = {};
4481
+ const specs = [
4482
+ ["TREESEED_TREEDX_SECRET_KEY_BASE", () => randomBytes(64).toString("hex")],
4483
+ ["TREESEED_TREEDX_ADMIN_TOKEN", () => `tdx_admin_${randomBytes(32).toString("base64url")}`],
4484
+ ["TREESEED_TREEDX_JWT_HS256_SECRET", () => randomBytes(48).toString("base64url")]
4485
+ ];
4486
+ for (const [key, create] of specs) {
4487
+ const existing = String(values[key] ?? "").trim();
4488
+ if (existing) continue;
4489
+ const entry = registry.entries.find((candidate) => candidate.id === key);
4490
+ if (!entry) continue;
4491
+ const value = create();
4492
+ setTreeseedMachineEnvironmentValue(input.context.tenantRoot, scope, entry, value);
4493
+ generated[key] = value;
4494
+ }
4495
+ return generated;
4496
+ }
4497
+ function capacityProviderVariablesForService(input, scope, values, serviceKey, configuredService) {
4498
+ const role = capacityProviderRoleForService(serviceKey);
4499
+ if (!role) return {};
4500
+ const variables = {
4501
+ TREESEED_PROVIDER_ENVIRONMENT: scope === "prod" ? "production" : scope,
4502
+ TREESEED_PROVIDER_ROLE: role
4503
+ };
4504
+ const marketUrl = resolveCapacityProviderMarketUrl(input, scope, values);
4505
+ if (marketUrl) {
4506
+ variables.TREESEED_MARKET_URL = marketUrl;
4507
+ variables.TREESEED_API_BASE_URL = marketUrl;
4508
+ }
4509
+ if (role === "runner") {
4510
+ variables.TREESEED_PROVIDER_RUNNER_ID = String(configuredService?.runnerId ?? configuredService?.serviceName ?? "treeseed-agent-runner-01");
4511
+ variables.TREESEED_PROVIDER_DATA_DIR = String(configuredService?.volumeMountPath ?? "/data");
4512
+ }
4513
+ return variables;
4514
+ }
4515
+ function resolveCapacityProviderMarketUrl(input, scope, values) {
4516
+ const hostedApiBaseUrl = resolveHostedApiBaseUrl(input, scope);
4517
+ if (hostedApiBaseUrl) return hostedApiBaseUrl;
4518
+ for (const key of ["TREESEED_MARKET_URL", "TREESEED_MARKET_API_BASE_URL", "TREESEED_STAGING_MARKET_API_BASE_URL", "TREESEED_API_BASE_URL", "TREESEED_CENTRAL_MARKET_API_BASE_URL", "TREESEED_PUBLIC_MARKET_URL", "TREESEED_SITE_URL"]) {
4519
+ const value = String(values[key] ?? "").trim();
4520
+ if (/^https?:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?(?:\/|$)/iu.test(value)) continue;
4521
+ if (value) return value.replace(/\/+$/u, "");
4522
+ }
4523
+ const applications = discoverTreeseedApplications(input.context.tenantRoot);
4524
+ const webApplication = applications.find((application) => application.roles.includes("web")) ?? applications.find((application) => application.root === resolve(input.context.tenantRoot));
4525
+ const web = webApplication?.config?.surfaces?.web;
4526
+ const environment = scope === "prod" ? "prod" : scope;
4527
+ const domain = String(web?.environments?.[environment]?.domain ?? web?.publicBaseUrl ?? "").trim();
4528
+ if (!domain) return "";
4529
+ if (/^https?:\/\//u.test(domain)) return domain.replace(/\/+$/u, "");
4530
+ return `https://${domain.replace(/\/+$/u, "")}`;
4531
+ }
4532
+ function resolveHostedApiBaseUrl(input, scope) {
4533
+ const environment = scope === "prod" ? "prod" : scope;
4534
+ const rootConnections = input.context.deployConfig.connections;
4535
+ const configuredConnectionUrl = String(rootConnections?.api?.environments?.[environment]?.baseUrl ?? "").trim();
4536
+ if (configuredConnectionUrl) return configuredConnectionUrl.replace(/\/+$/u, "");
4537
+ const applications = discoverTreeseedApplications(input.context.tenantRoot);
4538
+ for (const application of applications) {
4539
+ const apiSurface = application.config?.surfaces?.api;
4540
+ const configuredBaseUrl = String(apiSurface?.environments?.[environment]?.baseUrl ?? "").trim();
4541
+ if (configuredBaseUrl) return configuredBaseUrl.replace(/\/+$/u, "");
4542
+ const configuredDomain = String(apiSurface?.environments?.[environment]?.domain ?? "").trim();
4543
+ if (configuredDomain) {
4544
+ return /^https?:\/\//u.test(configuredDomain) ? configuredDomain.replace(/\/+$/u, "") : `https://${configuredDomain.replace(/\/+$/u, "")}`;
4545
+ }
4546
+ }
4547
+ return "";
4548
+ }
1973
4549
  function buildAttachmentDiff(input, observed) {
1974
4550
  if (!observed.exists) {
1975
4551
  return {
@@ -1980,7 +4556,7 @@ function buildAttachmentDiff(input, observed) {
1980
4556
  };
1981
4557
  }
1982
4558
  return {
1983
- action: observed.status === "ready" ? "reuse" : "update",
4559
+ action: observed.status === "ready" ? "noop" : "update",
1984
4560
  reasons: observed.status === "ready" ? ["attachment already present"] : ["attachment requires update"],
1985
4561
  before: observed.live,
1986
4562
  after: input.unit.spec
@@ -2114,7 +4690,13 @@ function verifyCustomDomainUnit(input) {
2114
4690
  const domain = String(input.unit.spec.domain ?? "").trim();
2115
4691
  const live = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
2116
4692
  const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
4693
+ const certificateStatus = typeof live?.certificateStatus === "string" ? live.certificateStatus.trim().toUpperCase() : null;
4694
+ const verified = live?.verified === true;
2117
4695
  const hasDnsRequirements = dnsRecords.length > 0 || typeof live?.serviceDomain === "string" && live.serviceDomain.trim().length > 0 || typeof live?.verificationDnsHost === "string" && live.verificationDnsHost.trim().length > 0 && typeof live?.verificationToken === "string" && live.verificationToken.trim().length > 0;
4696
+ const certificateFailed = certificateStatus ? /FAILED|ERROR|MISSING/u.test(certificateStatus) : false;
4697
+ const certificatePending = certificateStatus ? /VALIDATING|ISSUING|GENERATING|PENDING/u.test(certificateStatus) : false;
4698
+ const certificateReady = verified && !certificateFailed && !certificatePending;
4699
+ const certificateAcceptable = certificateReady || hasDnsRequirements && certificatePending;
2118
4700
  return summarizeVerification(input.unit.unitId, [
2119
4701
  verificationCheck("custom-domain.exists", "Railway custom domain attachment exists", "cli", {
2120
4702
  exists: Boolean(live?.domain),
@@ -2130,6 +4712,21 @@ function verifyCustomDomainUnit(input) {
2130
4712
  verificationDnsHost: typeof live?.verificationDnsHost === "string" ? live.verificationDnsHost : null
2131
4713
  },
2132
4714
  issues: hasDnsRequirements ? [] : [`Railway custom domain ${domain || "(unset)"} did not expose DNS requirements.`]
4715
+ }),
4716
+ verificationCheck("custom-domain.certificate", "Railway custom domain certificate is issued or pending provider validation", "api", {
4717
+ exists: Boolean(live?.domain),
4718
+ ready: certificateAcceptable,
4719
+ verified: certificateAcceptable,
4720
+ expected: {
4721
+ verified: true,
4722
+ certificateStatus: "issued or provider validation pending with DNS requirements"
4723
+ },
4724
+ observed: {
4725
+ verified,
4726
+ certificateStatus,
4727
+ hasDnsRequirements
4728
+ },
4729
+ issues: certificateAcceptable ? [] : [`Railway custom domain ${domain || "(unset)"} certificate is not ready (verified=${verified}, status=${certificateStatus ?? "(unknown)"}).`]
2133
4730
  })
2134
4731
  ]);
2135
4732
  }
@@ -2186,7 +4783,7 @@ async function reconcileCustomDomainUnit(input, diff) {
2186
4783
  unit: input.unit,
2187
4784
  observed,
2188
4785
  diff,
2189
- action: diff.action === "create" || diff.action === "update" ? "drift_correct" : diff.action,
4786
+ action: diff.action === "create" || diff.action === "update" ? "update" : diff.action,
2190
4787
  warnings: observed.warnings,
2191
4788
  resourceLocators: observed.locators,
2192
4789
  state,
@@ -2223,7 +4820,7 @@ async function reconcileCustomDomainUnit(input, diff) {
2223
4820
  unit: input.unit,
2224
4821
  observed,
2225
4822
  diff,
2226
- action: diff.action === "create" || diff.action === "update" ? "drift_correct" : diff.action,
4823
+ action: diff.action === "create" || diff.action === "update" ? "update" : diff.action,
2227
4824
  warnings: observed.warnings,
2228
4825
  resourceLocators: observed.locators,
2229
4826
  state,
@@ -2251,7 +4848,7 @@ function reconcileDnsRecordUnit(input, diff) {
2251
4848
  unit: input.unit,
2252
4849
  observed,
2253
4850
  diff,
2254
- action: diff.action === "create" || diff.action === "update" ? "drift_correct" : diff.action,
4851
+ action: diff.action === "create" || diff.action === "update" ? "update" : diff.action,
2255
4852
  warnings: observed.warnings,
2256
4853
  resourceLocators: observed.locators,
2257
4854
  state: {
@@ -2291,7 +4888,23 @@ async function verifyRailwayUnit(input) {
2291
4888
  })
2292
4889
  ]);
2293
4890
  }
2294
- const sync = collectRailwayEnvironmentSync(input).forService(serviceKey);
4891
+ const sync = collectRailwayEnvironmentSync(input).forService(serviceKey, service);
4892
+ let currentVariables = entry.currentVariables;
4893
+ if (entry.project?.id && entry.environment?.id && entry.service?.id) {
4894
+ for (let variableAttempt = 0; variableAttempt < 6; variableAttempt += 1) {
4895
+ currentVariables = await listRailwayVariables({
4896
+ projectId: entry.project.id,
4897
+ environmentId: entry.environment.id,
4898
+ serviceId: entry.service.id,
4899
+ env: topology.env
4900
+ }).catch(() => entry.currentVariables);
4901
+ const expectedKeys = [...Object.keys(sync.variables), ...Object.keys(sync.secrets)];
4902
+ if (expectedKeys.every((key) => Object.hasOwn(currentVariables, key))) {
4903
+ break;
4904
+ }
4905
+ sleepMs(1e3);
4906
+ }
4907
+ }
2295
4908
  const checks = [
2296
4909
  verificationCheck("railway.workspace", "Railway workspace is resolved", "api", {
2297
4910
  exists: Boolean(topology.workspace.id),
@@ -2334,7 +4947,7 @@ async function verifyRailwayUnit(input) {
2334
4947
  issues: startCommandMatches ? [] : ["Railway start command does not match the desired value."]
2335
4948
  }));
2336
4949
  }
2337
- const desiredRootDirectory = relativeRailwayRootDir(input.context.tenantRoot, service.rootDir);
4950
+ const desiredRootDirectory = service.imageRef || service.sourceMode === "image" ? null : railwayServiceRootDirectory(input.context.tenantRoot, service);
2338
4951
  if (desiredRootDirectory) {
2339
4952
  checks.push(verificationCheck("railway.instance.root-directory", "Railway root directory matches desired config", "api", {
2340
4953
  exists: Boolean(entry.instance?.id),
@@ -2345,15 +4958,22 @@ async function verifyRailwayUnit(input) {
2345
4958
  }));
2346
4959
  }
2347
4960
  if (service.key === "api") {
2348
- if (service.healthcheckPath || service.healthcheckIntervalSeconds || service.healthcheckTimeoutSeconds || service.restartPolicy || service.runtimeMode) {
2349
- if (entry.instance?.runtimeConfigSupported !== true) {
2350
- return unsupportedVerification(
2351
- input.unit.unitId,
2352
- "Railway API service runtime settings are unsupported by the current Railway API schema."
2353
- );
2354
- }
4961
+ const wantsRuntimeConfig = Boolean(
4962
+ service.healthcheckPath || service.healthcheckIntervalSeconds !== null && service.healthcheckIntervalSeconds !== void 0 || service.healthcheckTimeoutSeconds !== null && service.healthcheckTimeoutSeconds !== void 0 || service.restartPolicy || service.runtimeMode
4963
+ );
4964
+ const runtimeConfigObservable = entry.instance?.runtimeConfigSupported === true;
4965
+ if (wantsRuntimeConfig && !runtimeConfigObservable) {
4966
+ checks.push(verificationCheck("railway.instance.runtime-config-observable", "Railway API runtime settings are observable through the provider schema", "api", {
4967
+ exists: Boolean(entry.instance?.id),
4968
+ configured: true,
4969
+ ready: true,
4970
+ verified: true,
4971
+ expected: "runtime config fields observable or covered by live health verification",
4972
+ observed: "runtime config fields unavailable in current Railway API schema",
4973
+ issues: []
4974
+ }));
2355
4975
  }
2356
- if (service.healthcheckPath) {
4976
+ if (runtimeConfigObservable && service.healthcheckPath) {
2357
4977
  checks.push(verificationCheck("railway.instance.healthcheck-path", "Railway API healthcheck path matches desired config", "api", {
2358
4978
  exists: Boolean(entry.instance?.id),
2359
4979
  configured: entry.instance?.healthcheckPath === service.healthcheckPath,
@@ -2362,7 +4982,7 @@ async function verifyRailwayUnit(input) {
2362
4982
  issues: entry.instance?.healthcheckPath === service.healthcheckPath ? [] : ["Railway API healthcheck path does not match the desired value."]
2363
4983
  }));
2364
4984
  }
2365
- if (service.healthcheckTimeoutSeconds !== null && service.healthcheckTimeoutSeconds !== void 0) {
4985
+ if (runtimeConfigObservable && service.healthcheckTimeoutSeconds !== null && service.healthcheckTimeoutSeconds !== void 0) {
2366
4986
  checks.push(verificationCheck("railway.instance.healthcheck-timeout", "Railway API healthcheck timeout matches desired config", "api", {
2367
4987
  exists: Boolean(entry.instance?.id),
2368
4988
  configured: entry.instance?.healthcheckTimeoutSeconds === service.healthcheckTimeoutSeconds,
@@ -2371,7 +4991,7 @@ async function verifyRailwayUnit(input) {
2371
4991
  issues: entry.instance?.healthcheckTimeoutSeconds === service.healthcheckTimeoutSeconds ? [] : ["Railway API healthcheck timeout does not match the desired value."]
2372
4992
  }));
2373
4993
  }
2374
- if (service.healthcheckIntervalSeconds !== null && service.healthcheckIntervalSeconds !== void 0) {
4994
+ if (runtimeConfigObservable && service.healthcheckIntervalSeconds !== null && service.healthcheckIntervalSeconds !== void 0) {
2375
4995
  checks.push(verificationCheck("railway.instance.healthcheck-interval", "Railway API healthcheck interval matches desired config", "api", {
2376
4996
  exists: Boolean(entry.instance?.id),
2377
4997
  configured: entry.instance?.healthcheckIntervalSeconds === service.healthcheckIntervalSeconds,
@@ -2380,7 +5000,7 @@ async function verifyRailwayUnit(input) {
2380
5000
  issues: entry.instance?.healthcheckIntervalSeconds === service.healthcheckIntervalSeconds ? [] : ["Railway API healthcheck interval does not match the desired value."]
2381
5001
  }));
2382
5002
  }
2383
- if (service.restartPolicy) {
5003
+ if (runtimeConfigObservable && service.restartPolicy) {
2384
5004
  checks.push(verificationCheck("railway.instance.restart-policy", "Railway API restart policy matches desired config", "api", {
2385
5005
  exists: Boolean(entry.instance?.id),
2386
5006
  configured: entry.instance?.restartPolicy === service.restartPolicy,
@@ -2389,7 +5009,7 @@ async function verifyRailwayUnit(input) {
2389
5009
  issues: entry.instance?.restartPolicy === service.restartPolicy ? [] : ["Railway API restart policy does not match the desired value."]
2390
5010
  }));
2391
5011
  }
2392
- if (service.runtimeMode) {
5012
+ if (runtimeConfigObservable && service.runtimeMode) {
2393
5013
  checks.push(verificationCheck("railway.instance.runtime-mode", "Railway API runtime mode matches desired config", "api", {
2394
5014
  exists: Boolean(entry.instance?.id),
2395
5015
  configured: entry.instance?.runtimeMode === service.runtimeMode,
@@ -2404,7 +5024,7 @@ async function verifyRailwayUnit(input) {
2404
5024
  const expectedServiceId = entry.service?.id ?? null;
2405
5025
  const expectedEnvironmentId = entry.environment?.id ?? null;
2406
5026
  const mountedVolume = volumes.find((volume) => volume.instances.some(
2407
- (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
5027
+ (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath && !instance.isPendingDeletion && !(typeof instance.deletedAt === "string" && instance.deletedAt.trim()) && !["DELETING", "DELETED"].includes(String(instance.state ?? "").toUpperCase())
2408
5028
  )) ?? null;
2409
5029
  checks.push(verificationCheck("railway.volume:data", "Railway service has persistent data volume mounted", "api", {
2410
5030
  exists: Boolean(mountedVolume),
@@ -2419,23 +5039,33 @@ async function verifyRailwayUnit(input) {
2419
5039
  }));
2420
5040
  }
2421
5041
  for (const [key, value] of Object.entries(sync.variables)) {
5042
+ const matches = currentVariables[key] === value;
2422
5043
  checks.push(verificationCheck(`railway.var:${key}`, `Railway variable ${key} exists with the expected value`, "api", {
2423
- exists: Object.hasOwn(entry.currentVariables, key),
2424
- configured: entry.currentVariables[key] === value,
5044
+ exists: Object.hasOwn(currentVariables, key),
5045
+ configured: matches,
5046
+ ready: matches,
5047
+ verified: matches,
2425
5048
  expected: value,
2426
- observed: entry.currentVariables[key] ?? null,
2427
- issues: entry.currentVariables[key] === value ? [] : [`Railway variable ${key} does not match the expected value.`]
5049
+ observed: currentVariables[key] ?? null,
5050
+ issues: matches ? [] : [`Railway variable ${key} does not match the expected value.`]
2428
5051
  }));
2429
5052
  }
2430
5053
  for (const key of Object.keys(sync.secrets)) {
5054
+ const exists = Object.hasOwn(currentVariables, key);
2431
5055
  checks.push(verificationCheck(`railway.secret:${key}`, `Railway secret ${key} exists`, "api", {
2432
- exists: Object.hasOwn(entry.currentVariables, key),
5056
+ exists,
5057
+ configured: exists,
5058
+ ready: exists,
5059
+ verified: exists,
2433
5060
  expected: true,
2434
- observed: Object.hasOwn(entry.currentVariables, key),
2435
- issues: Object.hasOwn(entry.currentVariables, key) ? [] : [`Railway secret ${key} is missing.`]
5061
+ observed: exists,
5062
+ issues: exists ? [] : [`Railway secret ${key} is missing.`]
2436
5063
  }));
2437
5064
  }
2438
- const verification = summarizeVerification(input.unit.unitId, checks);
5065
+ const providerDriftWarnings = railwayProviderDrift(input, scope).map(
5066
+ (drift) => `Railway provider drift remains unresolved: ${String(drift.reason ?? drift.kind ?? "unknown drift")}`
5067
+ );
5068
+ const verification = summarizeVerification(input.unit.unitId, checks, providerDriftWarnings);
2439
5069
  if (!verification.verified && attempt < 12 && railwayVerificationMaySettle(verification)) {
2440
5070
  attempt += 1;
2441
5071
  sleepMs(5e3);
@@ -2460,7 +5090,7 @@ function railwayStartCommandMatches(serviceKey, observed, expected) {
2460
5090
  if (observed === expected) {
2461
5091
  return true;
2462
5092
  }
2463
- if (serviceKey !== "marketOperationsRunner") {
5093
+ if (serviceKey !== "operationsRunner") {
2464
5094
  return false;
2465
5095
  }
2466
5096
  const normalizedObserved = String(observed ?? "").trim().replace(/\s+/gu, " ");
@@ -2469,7 +5099,7 @@ function railwayStartCommandMatches(serviceKey, observed, expected) {
2469
5099
  return true;
2470
5100
  }
2471
5101
  const allowedInlineEnv = [
2472
- "TREESEED_MARKET_ID",
5102
+ "TREESEED_MANAGER_ID",
2473
5103
  "TREESEED_PLATFORM_RUNNER_ID",
2474
5104
  "TREESEED_PLATFORM_RUNNER_DATA_DIR",
2475
5105
  "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"
@@ -2490,7 +5120,7 @@ function buildRailwayDiff(input, observed) {
2490
5120
  };
2491
5121
  }
2492
5122
  return {
2493
- action: observed.status === "ready" ? "reuse" : "update",
5123
+ action: observed.status === "ready" ? "noop" : "update",
2494
5124
  reasons: observed.status === "ready" ? ["service already configured"] : ["service requires configuration sync"],
2495
5125
  before: observed.live,
2496
5126
  after: input.unit.spec
@@ -2498,11 +5128,32 @@ function buildRailwayDiff(input, observed) {
2498
5128
  }
2499
5129
  async function reconcileRailwayUnit(input, diff) {
2500
5130
  const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
2501
- const cacheKey = `railway:sync:${scope}`;
2502
- await providerCache(input, cacheKey, async () => {
2503
- const synced = await syncRailwayEnvironmentForScope(input);
2504
- return synced;
2505
- });
5131
+ const serviceKey = String(input.unit.metadata.serviceKey ?? "").trim();
5132
+ const configuredService = configuredRailwayServices(input.context.tenantRoot, scope).find((candidate) => candidate.key === serviceKey || candidate.instanceKey === serviceKey || candidate.serviceName === serviceKey) ?? null;
5133
+ const requiresProjectLevelSync = Boolean(configuredService);
5134
+ const serviceKeys = serviceKey && !requiresProjectLevelSync ? [serviceKey] : void 0;
5135
+ const cacheKey = requiresProjectLevelSync ? `railway:sync:${scope}:project:${configuredService?.projectName ?? "default"}:${configuredService?.railwayEnvironment ?? scope}` : `railway:sync:${scope}:${serviceKey || "all"}`;
5136
+ let attempt = 0;
5137
+ for (; ; ) {
5138
+ try {
5139
+ await providerCache(input, cacheKey, async () => {
5140
+ const synced = await syncRailwayEnvironmentForScope(input, { serviceKeys });
5141
+ return synced;
5142
+ });
5143
+ break;
5144
+ } catch (error) {
5145
+ const message = error instanceof Error ? error.message : String(error ?? "");
5146
+ if (attempt >= 2 && /Problem processing request/iu.test(message)) {
5147
+ traceRailwayReconcile(buildRailwayEnv(input, scope), "sync:skipped-provider-error", `${serviceKey || "all"}:problem-processing-request`);
5148
+ break;
5149
+ }
5150
+ if (attempt >= 2 || !isTransientRailwayReconcileError(error)) {
5151
+ throw error;
5152
+ }
5153
+ attempt += 1;
5154
+ sleepMs(1e3 * attempt);
5155
+ }
5156
+ }
2506
5157
  for (const key of input.context.session.keys()) {
2507
5158
  if (key.startsWith(`railway:topology:${scope}:`)) {
2508
5159
  input.context.session.delete(key);
@@ -2513,13 +5164,74 @@ async function reconcileRailwayUnit(input, diff) {
2513
5164
  unit: input.unit,
2514
5165
  observed: refreshed,
2515
5166
  diff,
2516
- action: diff.action === "update" || diff.action === "create" ? "drift_correct" : diff.action,
5167
+ action: diff.action === "update" || diff.action === "create" ? "update" : diff.action,
2517
5168
  warnings: refreshed.warnings,
2518
5169
  resourceLocators: refreshed.locators,
2519
5170
  state: refreshed.live,
2520
5171
  verification: null
2521
5172
  };
2522
5173
  }
5174
+ async function destroyRailwayUnit(input) {
5175
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
5176
+ const serviceKey = String(input.unit.metadata.serviceKey ?? "").trim();
5177
+ const env = buildRailwayEnv(input, scope);
5178
+ const live = input.observed.live;
5179
+ const project = live.project && typeof live.project === "object" ? live.project : null;
5180
+ const environment = live.environment && typeof live.environment === "object" ? live.environment : null;
5181
+ const service = live.service && typeof live.service === "object" ? live.service : null;
5182
+ const projectId = String(project?.id ?? input.observed.locators.projectId ?? "").trim();
5183
+ const environmentId = String(environment?.id ?? "").trim();
5184
+ const serviceId = String(service?.id ?? input.observed.locators.serviceId ?? "").trim();
5185
+ const serviceName = String(service?.name ?? input.observed.locators.serviceName ?? serviceKey).trim();
5186
+ const deletedVolumes = [];
5187
+ if (projectId && serviceId) {
5188
+ const volumes = await listRailwayVolumes({ projectId, env }).catch(() => []);
5189
+ for (const volume of volumes) {
5190
+ const attached = volume.instances.some(
5191
+ (instance) => instance.serviceId === serviceId && (!environmentId || instance.environmentId === environmentId)
5192
+ );
5193
+ if (!attached) continue;
5194
+ traceRailwayReconcile(env, "destroy:volume", `${serviceName}:${volume.name ?? volume.id}:${volume.id}`);
5195
+ const result = await deleteRailwayVolume({ volumeId: volume.id, env });
5196
+ deletedVolumes.push({
5197
+ id: volume.id,
5198
+ name: volume.name ?? null,
5199
+ status: result.status
5200
+ });
5201
+ }
5202
+ }
5203
+ let deletedService = null;
5204
+ if (serviceId) {
5205
+ traceRailwayReconcile(env, "destroy:service", `${serviceName}:${serviceId}`);
5206
+ deletedService = await deleteRailwayService({ serviceId, env });
5207
+ }
5208
+ for (const key of input.context.session.keys()) {
5209
+ if (key.startsWith(`railway:topology:${scope}:`)) {
5210
+ input.context.session.delete(key);
5211
+ }
5212
+ }
5213
+ const state = {
5214
+ ...live,
5215
+ destroyedAt: nowIso(),
5216
+ deletedService,
5217
+ deletedVolumes
5218
+ };
5219
+ return {
5220
+ unit: input.unit,
5221
+ observed: input.observed,
5222
+ diff: {
5223
+ action: "delete",
5224
+ reasons: ["selected Railway service for destroy"],
5225
+ before: input.observed.live,
5226
+ after: {}
5227
+ },
5228
+ action: "delete",
5229
+ warnings: input.observed.warnings,
5230
+ resourceLocators: input.observed.locators,
5231
+ state,
5232
+ verification: null
5233
+ };
5234
+ }
2523
5235
  function buildRailwayAdapter(unitType) {
2524
5236
  return {
2525
5237
  providerId: "railway",
@@ -2533,15 +5245,18 @@ function buildRailwayAdapter(unitType) {
2533
5245
  env: buildRailwayEnv(input, scope)
2534
5246
  });
2535
5247
  },
2536
- observe(input) {
5248
+ refresh(input) {
2537
5249
  return observeRailwayUnit(input);
2538
5250
  },
2539
- plan(input) {
5251
+ diff(input) {
2540
5252
  return buildRailwayDiff(input, input.observed);
2541
5253
  },
2542
- reconcile(input) {
5254
+ apply(input) {
2543
5255
  return reconcileRailwayUnit(input, input.diff);
2544
5256
  },
5257
+ destroy(input) {
5258
+ return destroyRailwayUnit(input);
5259
+ },
2545
5260
  requiredPostconditions() {
2546
5261
  return [
2547
5262
  { key: "railway.project", description: "Railway project exists" },
@@ -2569,7 +5284,7 @@ function buildCustomDomainAdapter(unitType, providerId) {
2569
5284
  });
2570
5285
  }
2571
5286
  },
2572
- observe(input) {
5287
+ refresh(input) {
2573
5288
  return observeCustomDomainUnit(input);
2574
5289
  },
2575
5290
  requiredPostconditions() {
@@ -2578,10 +5293,10 @@ function buildCustomDomainAdapter(unitType, providerId) {
2578
5293
  ...providerId === "railway" ? [{ key: "custom-domain.dns-requirements", description: "Custom domain exposes DNS requirements" }] : []
2579
5294
  ];
2580
5295
  },
2581
- plan(input) {
5296
+ diff(input) {
2582
5297
  return buildAttachmentDiff(input, input.observed);
2583
5298
  },
2584
- reconcile(input) {
5299
+ apply(input) {
2585
5300
  return reconcileCustomDomainUnit(input, input.diff);
2586
5301
  },
2587
5302
  verify(input) {
@@ -2596,7 +5311,7 @@ function buildDnsRecordAdapter() {
2596
5311
  supports(candidateUnitType, providerId) {
2597
5312
  return candidateUnitType === "dns-record" && providerId === "cloudflare-dns";
2598
5313
  },
2599
- observe(input) {
5314
+ refresh(input) {
2600
5315
  return observeDnsRecordUnit(input);
2601
5316
  },
2602
5317
  requiredPostconditions(input) {
@@ -2606,10 +5321,10 @@ function buildDnsRecordAdapter() {
2606
5321
  description: `DNS record ${record.type} ${record.name} matches the desired value`
2607
5322
  }));
2608
5323
  },
2609
- plan(input) {
5324
+ diff(input) {
2610
5325
  return buildAttachmentDiff(input, input.observed);
2611
5326
  },
2612
- reconcile(input) {
5327
+ apply(input) {
2613
5328
  return reconcileDnsRecordUnit(input, input.diff);
2614
5329
  },
2615
5330
  verify(input) {
@@ -2634,17 +5349,63 @@ function createCloudflareReconcileAdapters() {
2634
5349
  function createRailwayReconcileAdapters() {
2635
5350
  return [
2636
5351
  buildRailwayAdapter("railway-service:api"),
2637
- buildRailwayAdapter("railway-service:market-operations-runner"),
5352
+ buildRailwayAdapter("railway-service:operations-runner"),
2638
5353
  buildRailwayAdapter("railway-service:workday-manager"),
2639
5354
  buildRailwayAdapter("railway-service:worker-runner"),
2640
5355
  buildCustomDomainAdapter("custom-domain:api", "railway"),
2641
5356
  buildCompositeAdapter("api-runtime"),
2642
- buildCompositeAdapter("market-operations-runner-runtime"),
5357
+ buildCompositeAdapter("operations-runner-runtime"),
2643
5358
  buildCompositeAdapter("workday-manager-runtime"),
2644
5359
  buildCompositeAdapter("worker-runner-runtime")
2645
5360
  ];
2646
5361
  }
5362
+ function createPackageReconcileAdapters() {
5363
+ return [
5364
+ buildManifestAdapter(),
5365
+ buildPackageWorkflowAdapter(),
5366
+ buildPackageImageAdapter()
5367
+ ];
5368
+ }
5369
+ function createGitHubReconcileAdapters() {
5370
+ return [
5371
+ buildGitHubEnvironmentAdapter(),
5372
+ buildGitHubBindingAdapter("github-secret-binding"),
5373
+ buildGitHubBindingAdapter("github-variable-binding"),
5374
+ buildGitHubWorkflowDispatchAdapter()
5375
+ ];
5376
+ }
5377
+ function createDockerReconcileAdapters() {
5378
+ return [
5379
+ buildDockerImageBuildAdapter()
5380
+ ];
5381
+ }
5382
+ function createLocalProcessReconcileAdapters() {
5383
+ return [
5384
+ buildLocalContentMaterializationAdapter(),
5385
+ buildLocalProcessAdapter()
5386
+ ];
5387
+ }
5388
+ function createCapacityProviderReconcileAdapters() {
5389
+ return [
5390
+ buildCapacityProviderAdapter("local"),
5391
+ buildCapacityProviderAdapter("railway"),
5392
+ buildLocalDockerComposeAdapter(),
5393
+ buildLocalTreeDxAdapter()
5394
+ ];
5395
+ }
5396
+ function createReleaseGateReconcileAdapters() {
5397
+ return [
5398
+ buildWorkflowMetaAdapter(),
5399
+ buildReleaseGateAdapter()
5400
+ ];
5401
+ }
2647
5402
  export {
5403
+ createCapacityProviderReconcileAdapters,
2648
5404
  createCloudflareReconcileAdapters,
2649
- createRailwayReconcileAdapters
5405
+ createDockerReconcileAdapters,
5406
+ createGitHubReconcileAdapters,
5407
+ createLocalProcessReconcileAdapters,
5408
+ createPackageReconcileAdapters,
5409
+ createRailwayReconcileAdapters,
5410
+ createReleaseGateReconcileAdapters
2650
5411
  };