@treeseed/sdk 0.11.0 → 0.12.5

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 (473) hide show
  1. package/README.md +9 -4
  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 +117 -60
  27. package/dist/capacity-provider.js +215 -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 +2 -2
  36. package/dist/copilot.d.ts +3 -0
  37. package/dist/copilot.js +16 -1
  38. package/dist/d1-http.d.ts +1 -1
  39. package/dist/d1-store.d.ts +32 -57
  40. package/dist/d1-store.js +115 -318
  41. package/dist/db/d1.d.ts +1 -232
  42. package/dist/db/index.d.ts +3 -3
  43. package/dist/db/market-schema.d.ts +49037 -11843
  44. package/dist/db/market-schema.js +1788 -50
  45. package/dist/db/node-sqlite.d.ts +1 -232
  46. package/dist/db/schema.d.ts +0 -462
  47. package/dist/db/schema.js +2 -22
  48. package/dist/dispatch.d.ts +1 -1
  49. package/dist/dispatch.js +0 -11
  50. package/dist/fixture-support.js +52 -4
  51. package/dist/governance.d.ts +133 -0
  52. package/dist/governance.js +285 -0
  53. package/dist/graph/build.d.ts +2 -2
  54. package/dist/graph/context-query-contracts.d.ts +1 -1
  55. package/dist/graph/dsl.d.ts +1 -1
  56. package/dist/graph/query.d.ts +4 -4
  57. package/dist/graph/ranking.d.ts +1 -1
  58. package/dist/graph/schema.d.ts +1 -1
  59. package/dist/graph.d.ts +11 -11
  60. package/dist/guarantees/index.d.ts +425 -0
  61. package/dist/guarantees/index.js +1146 -0
  62. package/dist/hosting/apps.d.ts +1 -1
  63. package/dist/hosting/builtins.d.ts +1 -1
  64. package/dist/hosting/builtins.js +9 -198
  65. package/dist/hosting/contracts.d.ts +3 -2
  66. package/dist/hosting/graph.d.ts +24 -21
  67. package/dist/hosting/graph.js +280 -294
  68. package/dist/hosting/index.d.ts +4 -4
  69. package/dist/index.d.ts +88 -74
  70. package/dist/index.js +81 -10
  71. package/dist/local-dev/managed-dev.d.ts +85 -0
  72. package/dist/local-dev/managed-dev.js +368 -0
  73. package/dist/managed-dependencies.d.ts +9 -0
  74. package/dist/managed-dependencies.js +10 -8
  75. package/dist/market-client.d.ts +265 -7
  76. package/dist/market-client.js +381 -9
  77. package/dist/model-registry.d.ts +1 -1
  78. package/dist/model-registry.js +19 -19
  79. package/dist/operations/agent-tools.d.ts +2 -11
  80. package/dist/operations/agent-tools.js +2 -14
  81. package/dist/operations/providers/default.d.ts +1 -1
  82. package/dist/operations/providers/default.js +20 -37
  83. package/dist/operations/repository-operations.d.ts +26 -2
  84. package/dist/operations/repository-operations.js +159 -1
  85. package/dist/operations/runtime.d.ts +3 -3
  86. package/dist/operations/services/bootstrap-runner.d.ts +1 -1
  87. package/dist/operations/services/build-warning-policy.d.ts +34 -0
  88. package/dist/operations/services/build-warning-policy.js +81 -82
  89. package/dist/operations/services/commit-message-provider.js +4 -4
  90. package/dist/operations/services/config-runtime.d.ts +73 -19
  91. package/dist/operations/services/config-runtime.js +276 -100
  92. package/dist/operations/services/deploy.d.ts +41 -622
  93. package/dist/operations/services/deploy.js +181 -213
  94. package/dist/operations/services/deployment-readiness.d.ts +1 -1
  95. package/dist/operations/services/deployment-readiness.js +38 -7
  96. package/dist/operations/services/git-remote-policy.js +12 -4
  97. package/dist/operations/services/git-runner.d.ts +66 -0
  98. package/dist/operations/services/git-runner.js +479 -0
  99. package/dist/operations/services/git-workflow.d.ts +43 -5
  100. package/dist/operations/services/git-workflow.js +242 -20
  101. package/dist/operations/services/github-actions-verification.d.ts +3 -1
  102. package/dist/operations/services/github-actions-verification.js +11 -3
  103. package/dist/operations/services/github-api.js +21 -5
  104. package/dist/operations/services/github-automation.d.ts +3 -3
  105. package/dist/operations/services/github-automation.js +15 -18
  106. package/dist/operations/services/github-credentials.js +2 -1
  107. package/dist/operations/services/hosted-service-checks.d.ts +19 -1
  108. package/dist/operations/services/hosted-service-checks.js +162 -11
  109. package/dist/operations/services/hosting-audit.d.ts +2 -2
  110. package/dist/operations/services/hosting-audit.js +7 -7
  111. package/dist/operations/services/hub-launch.d.ts +1 -1
  112. package/dist/operations/services/hub-provider-launch.d.ts +5 -5
  113. package/dist/operations/services/hub-provider-launch.js +19 -14
  114. package/dist/operations/services/live-hosted-service-checks.d.ts +2 -1
  115. package/dist/operations/services/live-hosted-service-checks.js +193 -80
  116. package/dist/operations/services/local-dev.js +0 -1
  117. package/dist/operations/services/managed-host-security.d.ts +1 -1
  118. package/dist/operations/services/managed-repositories.d.ts +37 -0
  119. package/dist/operations/services/managed-repositories.js +177 -0
  120. package/dist/operations/services/operations-runner-smoke.d.ts +3 -2
  121. package/dist/operations/services/operations-runner-smoke.js +22 -4
  122. package/dist/operations/services/package-adapters.d.ts +77 -2
  123. package/dist/operations/services/package-adapters.js +629 -56
  124. package/dist/operations/services/package-reference-policy.d.ts +12 -88
  125. package/dist/operations/services/package-reference-policy.js +81 -213
  126. package/dist/operations/services/project-host-operations.d.ts +4 -4
  127. package/dist/operations/services/project-platform.d.ts +19 -193
  128. package/dist/operations/services/project-platform.js +31 -103
  129. package/dist/operations/services/project-web-monitor.d.ts +2 -2
  130. package/dist/operations/services/project-web-monitor.js +97 -2
  131. package/dist/operations/services/railway-api.d.ts +36 -7
  132. package/dist/operations/services/railway-api.js +331 -100
  133. package/dist/operations/services/railway-deploy.d.ts +7 -95
  134. package/dist/operations/services/railway-deploy.js +334 -742
  135. package/dist/operations/services/release-history.js +3 -4
  136. package/dist/operations/services/release-proof-ledger.d.ts +13 -0
  137. package/dist/operations/services/release-proof-ledger.js +81 -0
  138. package/dist/operations/services/release-proof-planner.d.ts +48 -0
  139. package/dist/operations/services/release-proof-planner.js +195 -0
  140. package/dist/operations/services/release-proof-runner.d.ts +16 -0
  141. package/dist/operations/services/release-proof-runner.js +169 -0
  142. package/dist/operations/services/release-proof.d.ts +81 -0
  143. package/dist/operations/services/release-proof.js +79 -0
  144. package/dist/operations/services/repository-save-orchestrator.d.ts +6 -7
  145. package/dist/operations/services/repository-save-orchestrator.js +353 -174
  146. package/dist/operations/services/runtime-paths.d.ts +1 -3
  147. package/dist/operations/services/runtime-paths.js +0 -4
  148. package/dist/operations/services/runtime-tools.d.ts +1 -192
  149. package/dist/operations/services/runtime-tools.js +4 -444
  150. package/dist/operations/services/save-deploy-preflight.js +9 -1
  151. package/dist/operations/services/template-host-bindings.d.ts +2 -2
  152. package/dist/operations/services/template-host-bindings.js +3 -2
  153. package/dist/operations/services/template-registry.d.ts +6 -6
  154. package/dist/operations/services/template-registry.js +33 -14
  155. package/dist/operations/services/template-secret-sync.d.ts +3 -3
  156. package/dist/operations/services/workflow-timing.d.ts +25 -0
  157. package/dist/operations/services/workflow-timing.js +79 -0
  158. package/dist/operations/services/workspace-dependency-mode.js +41 -4
  159. package/dist/operations/services/workspace-preflight.js +8 -8
  160. package/dist/operations/services/workspace-save-script.d.ts +4 -4
  161. package/dist/operations/services/workspace-save.d.ts +1 -0
  162. package/dist/operations/services/workspace-save.js +41 -18
  163. package/dist/operations/services/workspace-tools.js +40 -6
  164. package/dist/operations-registry.d.ts +1 -1
  165. package/dist/operations-registry.js +5 -6
  166. package/dist/operations-types.d.ts +1 -1
  167. package/dist/operations.d.ts +17 -11
  168. package/dist/operations.js +57 -3
  169. package/dist/platform/book-export.js +2 -0
  170. package/dist/platform/books-data.d.ts +1 -1
  171. package/dist/platform/config.d.ts +27 -0
  172. package/dist/platform/config.js +100 -0
  173. package/dist/platform/content-runtime-source.d.ts +54 -0
  174. package/dist/platform/content-runtime-source.js +169 -0
  175. package/dist/platform/contracts.d.ts +6 -1
  176. package/dist/platform/deploy-config.d.ts +1 -1
  177. package/dist/platform/deploy-config.js +7 -1
  178. package/dist/platform/deploy-runtime.d.ts +1 -1
  179. package/dist/platform/desired-state.d.ts +66 -0
  180. package/dist/platform/desired-state.js +1052 -0
  181. package/dist/platform/env.yaml +414 -19
  182. package/dist/platform/environment.d.ts +2 -2
  183. package/dist/platform/environment.js +54 -22
  184. package/dist/platform/local-content-materialization.d.ts +9 -0
  185. package/dist/platform/local-content-materialization.js +212 -0
  186. package/dist/platform/plugin.d.ts +5 -5
  187. package/dist/platform/plugins/runtime.d.ts +3 -3
  188. package/dist/platform/plugins/runtime.js +62 -1
  189. package/dist/platform/plugins.d.ts +6 -6
  190. package/dist/platform/published-content-pipeline.d.ts +3 -3
  191. package/dist/platform/published-content-pipeline.js +5 -1
  192. package/dist/platform/published-content.d.ts +4 -3
  193. package/dist/platform/site-config-schema.d.ts +1 -0
  194. package/dist/platform/site-config-schema.js +4 -1
  195. package/dist/platform/tenant/runtime-config.d.ts +152 -2
  196. package/dist/platform/tenant-config.d.ts +1 -1
  197. package/dist/platform/utils/site-config-schema.d.ts +154 -0
  198. package/dist/platform/utils/site-config-schema.js +400 -461
  199. package/dist/platform-operation-store.d.ts +1 -1
  200. package/dist/plugin-default.d.ts +1 -1
  201. package/dist/plugin-default.js +27 -12
  202. package/dist/project-import.d.ts +91 -0
  203. package/dist/project-import.js +257 -0
  204. package/dist/project-workflow.d.ts +1 -1
  205. package/dist/reconcile/bootstrap-systems.d.ts +2 -2
  206. package/dist/reconcile/bootstrap-systems.js +3 -3
  207. package/dist/reconcile/builtin-adapters.d.ts +7 -1
  208. package/dist/reconcile/builtin-adapters.js +2924 -346
  209. package/dist/reconcile/contracts.d.ts +15 -2
  210. package/dist/reconcile/desired-state.d.ts +2 -193
  211. package/dist/reconcile/desired-state.js +24 -33
  212. package/dist/reconcile/engine.d.ts +60 -397
  213. package/dist/reconcile/engine.js +182 -18
  214. package/dist/reconcile/index.d.ts +10 -10
  215. package/dist/reconcile/live-acceptance.d.ts +22 -1
  216. package/dist/reconcile/live-acceptance.js +456 -44
  217. package/dist/reconcile/providers/docker-private.d.ts +54 -0
  218. package/dist/reconcile/providers/docker-private.js +101 -0
  219. package/dist/reconcile/providers/github-private.d.ts +65 -0
  220. package/dist/reconcile/providers/github-private.js +119 -0
  221. package/dist/reconcile/providers/local-private.d.ts +28 -0
  222. package/dist/reconcile/providers/local-private.js +75 -0
  223. package/dist/reconcile/providers/railway-iac.d.ts +66 -0
  224. package/dist/reconcile/providers/railway-iac.js +275 -0
  225. package/dist/reconcile/providers/release-private.d.ts +92 -0
  226. package/dist/reconcile/providers/release-private.js +152 -0
  227. package/dist/reconcile/registry.d.ts +2 -2
  228. package/dist/reconcile/registry.js +16 -1
  229. package/dist/reconcile/state.d.ts +1 -1
  230. package/dist/reconcile/state.js +3 -40
  231. package/dist/reconcile/units.d.ts +1 -1
  232. package/dist/reconcile/units.js +7 -1
  233. package/dist/remote.d.ts +6 -6
  234. package/dist/scenes/artifacts.d.ts +16 -0
  235. package/dist/scenes/artifacts.js +61 -0
  236. package/dist/scenes/auth.d.ts +2 -0
  237. package/dist/scenes/auth.js +28 -0
  238. package/dist/scenes/base-url.d.ts +12 -0
  239. package/dist/scenes/base-url.js +123 -0
  240. package/dist/scenes/builtin-plugins.d.ts +2 -0
  241. package/dist/scenes/builtin-plugins.js +447 -0
  242. package/dist/scenes/checkpoints.d.ts +14 -0
  243. package/dist/scenes/checkpoints.js +33 -0
  244. package/dist/scenes/device-matrix.d.ts +2 -0
  245. package/dist/scenes/device-matrix.js +97 -0
  246. package/dist/scenes/devices.d.ts +11 -0
  247. package/dist/scenes/devices.js +22 -0
  248. package/dist/scenes/diagnostics.d.ts +5 -0
  249. package/dist/scenes/diagnostics.js +22 -0
  250. package/dist/scenes/diagram-providers.d.ts +2 -0
  251. package/dist/scenes/diagram-providers.js +181 -0
  252. package/dist/scenes/diagram-validation.d.ts +22 -0
  253. package/dist/scenes/diagram-validation.js +74 -0
  254. package/dist/scenes/environment.d.ts +2 -0
  255. package/dist/scenes/environment.js +65 -0
  256. package/dist/scenes/evidence-bundle.d.ts +5 -0
  257. package/dist/scenes/evidence-bundle.js +42 -0
  258. package/dist/scenes/evidence-report.d.ts +10 -0
  259. package/dist/scenes/evidence-report.js +73 -0
  260. package/dist/scenes/evidence.d.ts +16 -0
  261. package/dist/scenes/evidence.js +277 -0
  262. package/dist/scenes/index.d.ts +48 -0
  263. package/dist/scenes/index.js +312 -0
  264. package/dist/scenes/inspect.d.ts +9 -0
  265. package/dist/scenes/inspect.js +64 -0
  266. package/dist/scenes/loader.d.ts +3 -0
  267. package/dist/scenes/loader.js +38 -0
  268. package/dist/scenes/logs.d.ts +2 -0
  269. package/dist/scenes/logs.js +47 -0
  270. package/dist/scenes/operations.d.ts +3 -0
  271. package/dist/scenes/operations.js +128 -0
  272. package/dist/scenes/phase0.d.ts +9 -0
  273. package/dist/scenes/phase0.js +637 -0
  274. package/dist/scenes/planner.d.ts +12 -0
  275. package/dist/scenes/planner.js +166 -0
  276. package/dist/scenes/playwright-adapter.d.ts +2 -0
  277. package/dist/scenes/playwright-adapter.js +63 -0
  278. package/dist/scenes/plugins.d.ts +5 -0
  279. package/dist/scenes/plugins.js +115 -0
  280. package/dist/scenes/progress.d.ts +17 -0
  281. package/dist/scenes/progress.js +24 -0
  282. package/dist/scenes/publish-destinations.d.ts +12 -0
  283. package/dist/scenes/publish-destinations.js +99 -0
  284. package/dist/scenes/publish-plan-report.d.ts +10 -0
  285. package/dist/scenes/publish-plan-report.js +70 -0
  286. package/dist/scenes/publish-plan.d.ts +15 -0
  287. package/dist/scenes/publish-plan.js +248 -0
  288. package/dist/scenes/publish-redaction.d.ts +16 -0
  289. package/dist/scenes/publish-redaction.js +148 -0
  290. package/dist/scenes/publish-release-record.d.ts +13 -0
  291. package/dist/scenes/publish-release-record.js +34 -0
  292. package/dist/scenes/publish-report.d.ts +10 -0
  293. package/dist/scenes/publish-report.js +67 -0
  294. package/dist/scenes/publish.d.ts +16 -0
  295. package/dist/scenes/publish.js +298 -0
  296. package/dist/scenes/registry.d.ts +12 -0
  297. package/dist/scenes/registry.js +65 -0
  298. package/dist/scenes/remotion-adapter.d.ts +3 -0
  299. package/dist/scenes/remotion-adapter.js +94 -0
  300. package/dist/scenes/remotion-composition-registry.d.ts +2 -0
  301. package/dist/scenes/remotion-composition-registry.js +12 -0
  302. package/dist/scenes/remotion-compositions.d.ts +15 -0
  303. package/dist/scenes/remotion-compositions.js +464 -0
  304. package/dist/scenes/remotion-diagrams.d.ts +9 -0
  305. package/dist/scenes/remotion-diagrams.js +188 -0
  306. package/dist/scenes/remotion-input.d.ts +13 -0
  307. package/dist/scenes/remotion-input.js +291 -0
  308. package/dist/scenes/remotion-visuals.d.ts +29 -0
  309. package/dist/scenes/remotion-visuals.js +198 -0
  310. package/dist/scenes/render-media-assets.d.ts +9 -0
  311. package/dist/scenes/render-media-assets.js +117 -0
  312. package/dist/scenes/render-report.d.ts +10 -0
  313. package/dist/scenes/render-report.js +27 -0
  314. package/dist/scenes/render.d.ts +2 -0
  315. package/dist/scenes/render.js +218 -0
  316. package/dist/scenes/reporter.d.ts +3 -0
  317. package/dist/scenes/reporter.js +101 -0
  318. package/dist/scenes/resume.d.ts +2 -0
  319. package/dist/scenes/resume.js +79 -0
  320. package/dist/scenes/runner.d.ts +2 -0
  321. package/dist/scenes/runner.js +674 -0
  322. package/dist/scenes/schema.d.ts +6 -0
  323. package/dist/scenes/schema.js +994 -0
  324. package/dist/scenes/seed.d.ts +2 -0
  325. package/dist/scenes/seed.js +83 -0
  326. package/dist/scenes/segments.d.ts +15 -0
  327. package/dist/scenes/segments.js +79 -0
  328. package/dist/scenes/selectors.d.ts +3 -0
  329. package/dist/scenes/selectors.js +21 -0
  330. package/dist/scenes/timeline.d.ts +11 -0
  331. package/dist/scenes/timeline.js +26 -0
  332. package/dist/scenes/timeouts.d.ts +6 -0
  333. package/dist/scenes/timeouts.js +17 -0
  334. package/dist/scenes/training-captions.d.ts +14 -0
  335. package/dist/scenes/training-captions.js +98 -0
  336. package/dist/scenes/training-report.d.ts +10 -0
  337. package/dist/scenes/training-report.js +78 -0
  338. package/dist/scenes/training-transcript.d.ts +24 -0
  339. package/dist/scenes/training-transcript.js +204 -0
  340. package/dist/scenes/training.d.ts +10 -0
  341. package/dist/scenes/training.js +134 -0
  342. package/dist/scenes/types.d.ts +1872 -0
  343. package/dist/scenes/types.js +8 -0
  344. package/dist/scenes/visual-audit-contact-sheets.d.ts +6 -0
  345. package/dist/scenes/visual-audit-contact-sheets.js +119 -0
  346. package/dist/scenes/visual-audit-fixtures.d.ts +25 -0
  347. package/dist/scenes/visual-audit-fixtures.js +210 -0
  348. package/dist/scenes/visual-audit-report.d.ts +11 -0
  349. package/dist/scenes/visual-audit-report.js +85 -0
  350. package/dist/scenes/visual-audit-review.d.ts +23 -0
  351. package/dist/scenes/visual-audit-review.js +829 -0
  352. package/dist/scenes/visual-audit-routes.d.ts +13 -0
  353. package/dist/scenes/visual-audit-routes.js +308 -0
  354. package/dist/scenes/visual-audit.d.ts +3 -0
  355. package/dist/scenes/visual-audit.js +596 -0
  356. package/dist/scripts/build-dist.js +154 -40
  357. package/dist/scripts/check-build-warnings.js +42 -53
  358. package/dist/scripts/cleanup-markdown.js +1 -1
  359. package/dist/scripts/cli-test-fixtures.js +24 -28
  360. package/dist/scripts/migrate-journey-csv-to-guarantees.js +140 -0
  361. package/dist/scripts/package-tools.js +12 -1
  362. package/dist/scripts/prepare.js +3 -7
  363. package/dist/scripts/release-verify.js +2 -5
  364. package/dist/scripts/run-fixture-astro-command.js +1 -1
  365. package/dist/scripts/template-catalog.test.js +68 -82
  366. package/dist/scripts/tenant-dev.js +0 -10
  367. package/dist/scripts/test-cloudflare-local.js +1 -1
  368. package/dist/scripts/test-scaffold.js +0 -1
  369. package/dist/scripts/test-smoke.js +3 -3
  370. package/dist/scripts/treeseed-build-dist.js +4 -7
  371. package/dist/scripts/treeseed-release-verify.js +1 -1
  372. package/dist/scripts/verify-driver.js +30 -21
  373. package/dist/scripts/workflow-commands.test.js +18 -27
  374. package/dist/scripts/workspace-command-e2e.js +1 -1
  375. package/dist/scripts/workspace-lint.js +6 -0
  376. package/dist/scripts/workspace-release-test.js +90 -1
  377. package/dist/scripts/workspace-test.js +6 -0
  378. package/dist/sdk-dispatch.d.ts +1 -1
  379. package/dist/sdk-dispatch.js +0 -16
  380. package/dist/sdk-fields.d.ts +1 -1
  381. package/dist/sdk-filters.d.ts +1 -1
  382. package/dist/sdk-types.d.ts +1184 -113
  383. package/dist/sdk-types.js +393 -0
  384. package/dist/sdk.d.ts +74 -79
  385. package/dist/sdk.js +27 -36
  386. package/dist/secrets-capability.d.ts +421 -0
  387. package/dist/secrets-capability.js +530 -0
  388. package/dist/seeds/normalize.js +1 -0
  389. package/dist/seeds/planner.js +38 -0
  390. package/dist/seeds/schema.js +293 -4
  391. package/dist/seeds/types.d.ts +60 -0
  392. package/dist/seeds/types.js +9 -1
  393. package/dist/service-credentials.d.ts +54 -0
  394. package/dist/service-credentials.js +112 -0
  395. package/dist/stores/cursor-store.d.ts +2 -2
  396. package/dist/stores/envelopes.d.ts +1 -1
  397. package/dist/stores/helpers.d.ts +1 -1
  398. package/dist/stores/lease-store.d.ts +2 -2
  399. package/dist/stores/message-store.d.ts +2 -2
  400. package/dist/stores/operational-store.d.ts +3 -26
  401. package/dist/stores/operational-store.js +0 -225
  402. package/dist/stores/project-workflow-store.d.ts +2 -2
  403. package/dist/stores/run-store.d.ts +2 -3
  404. package/dist/stores/run-store.js +0 -98
  405. package/dist/stores/subscription-store.d.ts +2 -3
  406. package/dist/stores/subscription-store.js +0 -100
  407. package/dist/template-catalog.d.ts +1 -1
  408. package/dist/template-launch-requirements.d.ts +2 -2
  409. package/dist/template-launch-ui.d.ts +2 -2
  410. package/dist/treedx/adapters.d.ts +6 -6
  411. package/dist/treedx/client.d.ts +1 -1
  412. package/dist/treedx/errors.d.ts +1 -1
  413. package/dist/treedx/federated-client.d.ts +3 -3
  414. package/dist/treedx/graph-adapter.d.ts +2 -2
  415. package/dist/treedx/index.d.ts +14 -14
  416. package/dist/treedx/market-integration.d.ts +1 -1
  417. package/dist/treedx/ports.d.ts +30 -30
  418. package/dist/treedx/query-adapter.d.ts +3 -3
  419. package/dist/treedx/registry-client.d.ts +2 -2
  420. package/dist/treedx/repository-adapter.d.ts +4 -4
  421. package/dist/treedx/repository-adapter.js +14 -3
  422. package/dist/treedx/sdk-integration.d.ts +3 -3
  423. package/dist/treedx/types.d.ts +2 -2
  424. package/dist/treedx/workspace-adapter.d.ts +2 -2
  425. package/dist/treedx-backends.d.ts +13 -11
  426. package/dist/treedx-backends.js +52 -50
  427. package/dist/treedx-client.d.ts +3 -0
  428. package/dist/treedx-client.js +10 -1
  429. package/dist/treeseed/services/compose.yml +2 -3
  430. package/dist/treeseed/template-catalog/catalog.fixture.json +4 -4
  431. package/dist/types/agents.d.ts +256 -0
  432. package/dist/types/agents.js +42 -1
  433. package/dist/verification.d.ts +1 -0
  434. package/dist/verification.js +143 -14
  435. package/dist/workflow/operations.d.ts +510 -559
  436. package/dist/workflow/operations.js +1891 -1342
  437. package/dist/workflow/policy.js +19 -7
  438. package/dist/workflow/runs.d.ts +9 -3
  439. package/dist/workflow/runs.js +66 -26
  440. package/dist/workflow/session.d.ts +4 -1
  441. package/dist/workflow/session.js +50 -10
  442. package/dist/workflow/worktrees.d.ts +6 -2
  443. package/dist/workflow/worktrees.js +98 -15
  444. package/dist/workflow-state.d.ts +5 -5
  445. package/dist/workflow-state.js +27 -17
  446. package/dist/workflow-support.d.ts +26 -25
  447. package/dist/workflow-support.js +27 -31
  448. package/dist/workflow.d.ts +46 -16
  449. package/dist/workflow.js +19 -7
  450. package/drizzle/d1/0000_treeseed_d1.sql +0 -18
  451. package/drizzle/market/0000_market_control_plane.sql +1793 -1499
  452. package/drizzle/market/0005_agent_capacity_coordination.sql +145 -0
  453. package/drizzle/market/0006_provider_assignment_lifecycle.sql +9 -0
  454. package/drizzle/market/0007_agent_architecture_gap_closure.sql +157 -0
  455. package/drizzle/market/0008_agent_capacity_operational_closure.sql +27 -0
  456. package/drizzle/market/0009_agent_capacity_runtime_hardening.sql +29 -0
  457. package/drizzle/market/0010_secrets_capability_persistence.sql +130 -0
  458. package/drizzle/market/0011_github_app_adapter.sql +47 -0
  459. package/drizzle/market/0012_workday_test_runs.sql +44 -0
  460. package/drizzle/market/0013_runtime_task_lifecycle.sql +39 -0
  461. package/drizzle/market/0014_governance_pipeline.sql +189 -0
  462. package/package.json +75 -18
  463. package/scripts/{verify-driver.mjs → verify-driver.ts} +19 -4
  464. package/templates/github/deploy-web.workflow.yml +12 -8
  465. package/dist/operations/services/mailpit-runtime.d.ts +0 -20
  466. package/dist/operations/services/mailpit-runtime.js +0 -67
  467. package/dist/operations/services/release-candidate.d.ts +0 -60
  468. package/dist/operations/services/release-candidate.js +0 -953
  469. package/dist/scripts/ensure-mailpit.js +0 -48
  470. package/dist/scripts/logs-mailpit.js +0 -2
  471. package/dist/scripts/run-ts.js +0 -45
  472. package/dist/scripts/stop-mailpit.js +0 -5
  473. package/dist/scripts/treeseed-run-ts.js +0 -45
@@ -1,6 +1,15 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
+ import {
5
+ compileTreeseedDesiredResourceGraph,
6
+ compileTreeseedDesiredUnitsFromGraph
7
+ } from "../platform/desired-state.js";
8
+ import {
9
+ collectTreeseedReconcileStatus,
10
+ planTreeseedReconciliation,
11
+ reconcileTreeseedTarget
12
+ } from "../reconcile/index.js";
4
13
  import {
5
14
  applyTreeseedEnvironmentToProcess,
6
15
  applyTreeseedConfigValues,
@@ -8,6 +17,7 @@ import {
8
17
  assertTreeseedCommandEnvironment,
9
18
  checkTreeseedProviderConnections,
10
19
  collectTreeseedConfigContext,
20
+ collectTreeseedConfigSeedValues,
11
21
  collectTreeseedPrintEnvReport,
12
22
  ensureTreeseedSecretSessionForConfig,
13
23
  ensureTreeseedActVerificationTooling,
@@ -24,23 +34,18 @@ import {
24
34
  setTreeseedRemoteSession,
25
35
  writeTreeseedMachineConfig
26
36
  } from "../operations/services/config-runtime.js";
27
- import { collectTreeseedToolStatus, formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
37
+ import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
28
38
  import { ControlPlaneClient } from "../control-plane-client.js";
29
39
  import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
30
40
  import {
31
- assertDeploymentInitialized,
32
41
  buildProvisioningSummary,
33
42
  cleanupDestroyedState,
34
43
  createBranchPreviewDeployTarget,
35
44
  createPersistentDeployTarget,
36
- destroyCloudflareResources,
37
45
  destroyTreeseedEnvironmentResources,
38
- ensureGeneratedWranglerConfig,
39
- finalizeDeploymentState,
40
46
  loadDeployState,
41
47
  recordHostedDeploymentState,
42
- runRemoteD1Migrations,
43
- validateDeployPrerequisites,
48
+ resolveConfiguredSurfaceDomain,
44
49
  validateDestroyPrerequisites
45
50
  } from "../operations/services/deploy.js";
46
51
  import {
@@ -49,7 +54,6 @@ import {
49
54
  assertFeatureBranch,
50
55
  branchExists,
51
56
  checkoutBranch,
52
- checkoutDetachedOriginBranch,
53
57
  checkoutTaskBranchFromStaging,
54
58
  createDeprecatedTaskTag,
55
59
  deleteLocalBranch,
@@ -58,36 +62,36 @@ import {
58
62
  gitWorkflowRoot,
59
63
  headCommit,
60
64
  listTaskBranches,
61
- mergeBranchIntoTarget,
62
- prepareReleaseBranches,
65
+ mergeBranchDownIntoFeature,
63
66
  PRODUCTION_BRANCH,
67
+ promoteCommitToBranchWithExpectedHead,
64
68
  pushBranch,
65
- pushHeadToBranch,
66
69
  reattachDetachedHeadIfSafe,
70
+ remoteHeadCommit,
67
71
  remoteBranchExists,
68
72
  STAGING_BRANCH,
69
- squashMergeBranchIntoStaging,
70
- syncBranchWithOrigin
73
+ syncBranchWithOrigin,
74
+ waitForStagingAutomation
71
75
  } from "../operations/services/git-workflow.js";
72
76
  import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
73
77
  import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
74
- import { dispatchGitHubWorkflowRun } from "../operations/services/github-api.js";
75
78
  import {
76
79
  formatGitHubActionsGateFailure,
77
80
  inspectGitHubActionsVerification,
78
81
  skippedGitHubActionsGate,
79
82
  waitForGitHubActionsGate
80
83
  } from "../operations/services/github-actions-verification.js";
81
- import {
82
- runReleaseCandidateGate
83
- } from "../operations/services/release-candidate.js";
84
+ import { cleanProofLedger } from "../operations/services/release-proof-ledger.js";
85
+ import { buildTreeseedProofPlan, hostedWorkflowForPackage, summarizeTreeseedProofLedger } from "../operations/services/release-proof-planner.js";
86
+ import { runTreeseedProof } from "../operations/services/release-proof-runner.js";
87
+ import { createTreeseedWorkflowTimer } from "../operations/services/workflow-timing.js";
84
88
  import {
85
89
  collectReleaseHistoryCommits,
86
90
  renderAdministrativeCommitMessage,
87
91
  upsertReleaseChangelog
88
92
  } from "../operations/services/release-history.js";
89
- import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
90
- import { runTenantDeployPreflight, runWorkspaceReleasePreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
93
+ import { packageScriptPath } from "../operations/services/runtime-tools.js";
94
+ import { loadTreeseedPlatformConfig } from "../platform/config.js";
91
95
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
92
96
  import {
93
97
  collectMergeConflictReport,
@@ -104,20 +108,14 @@ import {
104
108
  } from "../operations/services/workspace-save.js";
105
109
  import {
106
110
  planRepositorySave,
107
- refreshAndValidateRootWorkspaceLockfileForSave,
108
111
  repositorySaveErrorDetails,
109
112
  runRepositorySaveOrchestrator
110
113
  } from "../operations/services/repository-save-orchestrator.js";
111
114
  import { discoverTreeseedPackageAdapters } from "../operations/services/package-adapters.js";
112
115
  import {
113
- assertNoInternalDevReferences,
114
- cleanupStaleTreeseedDevTags,
115
- collectTreeseedDevTagCleanupPlan,
116
116
  collectInternalDevReferenceIssues,
117
- devTagFromDependencySpec,
118
117
  installableInternalDependencyVersions,
119
- normalizeGitRemoteForManifest,
120
- rewriteProjectInternalDependenciesToStableVersions
118
+ normalizeGitRemoteForManifest
121
119
  } from "../operations/services/package-reference-policy.js";
122
120
  import {
123
121
  ensureLocalWorkspaceLinks,
@@ -125,7 +123,6 @@ import {
125
123
  unlinkLocalWorkspaceLinks
126
124
  } from "../operations/services/workspace-dependency-mode.js";
127
125
  import {
128
- hasCompleteTreeseedPackageCheckout,
129
126
  changedWorkspacePackages,
130
127
  publishableWorkspacePackages,
131
128
  run,
@@ -133,13 +130,13 @@ import {
133
130
  workspacePackages,
134
131
  workspaceRoot
135
132
  } from "../operations/services/workspace-tools.js";
136
- import { runTreeseedHostingAudit } from "../operations/services/hosting-audit.js";
137
- import { collectTreeseedHostedServiceChecks } from "../operations/services/hosted-service-checks.js";
133
+ import { classifyTreeseedGitMode, runTreeseedGit, runTreeseedGitOk, runTreeseedGitText } from "../operations/services/git-runner.js";
138
134
  import { collectTreeseedDeploymentReadiness } from "../operations/services/deployment-readiness.js";
139
135
  import { collectTreeseedLiveHostedServiceChecks } from "../operations/services/live-hosted-service-checks.js";
140
136
  import { discoverTreeseedApplications } from "../hosting/apps.js";
137
+ import { compileTreeseedHostingGraph } from "../hosting/graph.js";
141
138
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
142
- import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, filterTreeseedDesiredUnitsByBootstrapSystems, planTreeseedReconciliation, resolveTreeseedBootstrapSelection, reconcileTreeseedTarget } from "../reconcile/index.js";
139
+ import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, destroyTreeseedTargetUnits, filterTreeseedDesiredUnitsByBootstrapSystems, resolveTreeseedBootstrapSelection } from "../reconcile/index.js";
143
140
  import {
144
141
  acquireWorkflowLock,
145
142
  archiveWorkflowRun,
@@ -161,6 +158,7 @@ import {
161
158
  checkedOutWorkspacePackageRepos,
162
159
  resolveTreeseedWorkflowSession
163
160
  } from "./session.js";
161
+ import { checkedOutManagedWorkflowRepos } from "../operations/services/managed-repositories.js";
164
162
  import {
165
163
  classifyTreeseedBranchRole,
166
164
  resolveTreeseedWorkflowPaths
@@ -192,6 +190,14 @@ function defaultWrite(output, stream = "stdout") {
192
190
  (stream === "stderr" ? process.stderr : process.stdout).write(`${output}
193
191
  `);
194
192
  }
193
+ function runGit(args, options) {
194
+ return runTreeseedGitText(args, {
195
+ cwd: options.cwd,
196
+ mode: classifyTreeseedGitMode(args),
197
+ timeoutMs: options.timeoutMs,
198
+ maxBuffer: options.maxBuffer
199
+ });
200
+ }
195
201
  function shouldManageWorkspaceLinks(mode, env = process.env) {
196
202
  if (mode === "off") return false;
197
203
  const envMode = String(env?.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
@@ -223,7 +229,7 @@ function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
223
229
  const packages = [
224
230
  { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
225
231
  { name: "@treeseed/ui", dir: "packages/ui", artifacts: ["dist/index.js"] },
226
- { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/worker.js"] },
232
+ { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/manager.js", "dist/provider/runner.js"] },
227
233
  { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
228
234
  { name: "@treeseed/admin", dir: "packages/admin", artifacts: ["dist/plugin.js"] },
229
235
  { name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
@@ -267,7 +273,7 @@ function ensureWorkflowCommandBins(root, helpers) {
267
273
  }
268
274
  }
269
275
  function unresolvedMergePaths(repoDir) {
270
- return run("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
276
+ return runGit(["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
271
277
  }
272
278
  function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
273
279
  const gitRoot = repoRoot(root);
@@ -288,7 +294,7 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
288
294
  const entries = [];
289
295
  for (const pkg of packages) {
290
296
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
291
- run("git", ["add", pkg.repoPath], { cwd: gitRoot });
297
+ runGit(["add", pkg.repoPath], { cwd: gitRoot });
292
298
  entries.push({
293
299
  packageName: pkg.name,
294
300
  path: pkg.repoPath,
@@ -323,7 +329,7 @@ function normalizeSaveLane(lane) {
323
329
  }
324
330
  function normalizeSaveCiMode(mode, branch, lane = "fast") {
325
331
  if (mode === "hosted" || mode === "off") return mode;
326
- if (lane === "promotion") return branch === STAGING_BRANCH ? "hosted" : "off";
332
+ if (lane === "promotion") return branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH ? "hosted" : "off";
327
333
  return "off";
328
334
  }
329
335
  function normalizeSaveVerifyMode(mode) {
@@ -377,35 +383,27 @@ function helpersForCwd(helpers, cwd) {
377
383
  function shouldDispatchSwitchToManagedWorktree(root, input, env) {
378
384
  return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
379
385
  }
380
- function assertHostedGitHubWorkflowAuthReady(operation, root) {
381
- const tools = collectTreeseedToolStatus({
382
- tenantRoot: root,
383
- env: process.env
384
- });
385
- const github = tools.auth.github;
386
- if (!github.authenticated) {
387
- const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
388
- workflowError(
389
- operation,
390
- "github_auth_unavailable",
391
- [
392
- "Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
393
- github.detail,
394
- `Managed gh check: ${command}`,
395
- "Remediation:",
396
- ...github.remediation.map((item) => `- ${item}`)
397
- ].join("\n"),
398
- {
399
- details: {
400
- toolsHome: tools.toolsHome,
401
- ghConfigDir: tools.ghConfigDir,
402
- github,
403
- tools: tools.tools
404
- }
405
- }
406
- );
386
+ function assertHostedGitHubWorkflowCredentialsReady(operation, root, gates) {
387
+ const missing = [];
388
+ for (const gate of gates) {
389
+ const repository = gate.repository ?? resolveGitHubRepositorySlug(gate.repoPath);
390
+ const scope = gate.branch === PRODUCTION_BRANCH ? "prod" : "staging";
391
+ const values = resolveTreeseedMachineEnvironmentValues(root, scope);
392
+ const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
393
+ if (!credential.token) {
394
+ missing.push({ name: gate.name, repository: credential.repository, envName: credential.envName });
395
+ }
407
396
  }
408
- return tools;
397
+ if (missing.length === 0) return;
398
+ workflowError(
399
+ operation,
400
+ "github_auth_unavailable",
401
+ [
402
+ "Treeseed hosted GitHub workflow gates require Treeseed-prefixed GitHub credentials.",
403
+ ...missing.map((gate) => `- ${gate.name}: configure ${gate.envName} for ${gate.repository}, or TREESEED_GITHUB_TOKEN as a fallback.`)
404
+ ].join("\n"),
405
+ { details: { missing } }
406
+ );
409
407
  }
410
408
  async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
411
409
  if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
@@ -414,42 +412,49 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
414
412
  if (gates.length === 0) {
415
413
  return [];
416
414
  }
417
- assertHostedGitHubWorkflowAuthReady(operation, options.root ?? gates[0].repoPath);
415
+ if (operation === "save" && gates.every((gate) => !gate.repository && githubRepositoryForRepo(gate.repoPath) == null)) {
416
+ return gates.map((gate) => skippedGitHubActionsGate(gate, "non-github-repository"));
417
+ }
418
+ assertHostedGitHubWorkflowCredentialsReady(operation, options.root ?? gates[0].repoPath, gates);
418
419
  const results = [];
419
420
  for (const gate of gates) {
421
+ const gateWithTimeout = {
422
+ ...gate,
423
+ timeoutSeconds: gate.timeoutSeconds ?? HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS
424
+ };
420
425
  if (options.root && options.runId) {
421
426
  const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
422
- repository: gate.repository ?? null,
423
- workflow: gate.workflow,
424
- headSha: gate.headSha,
425
- branch: gate.branch
427
+ repository: gateWithTimeout.repository ?? null,
428
+ workflow: gateWithTimeout.workflow,
429
+ headSha: gateWithTimeout.headSha,
430
+ branch: gateWithTimeout.branch
426
431
  });
427
432
  if (cached) {
428
433
  results.push({
429
434
  ...cached.result,
430
- name: gate.name,
435
+ name: gateWithTimeout.name,
431
436
  cached: true
432
437
  });
433
438
  continue;
434
439
  }
435
440
  }
436
- const result = await waitForGitHubActionsGate(gate, {
441
+ const result = await waitForGitHubActionsGate(gateWithTimeout, {
437
442
  operation,
438
- env: githubWorkflowGateEnv(options.root, gate),
443
+ env: githubWorkflowGateEnv(options.root, gateWithTimeout),
439
444
  onProgress: options.onProgress
440
445
  });
441
446
  const normalized = {
442
- name: gate.name,
447
+ name: gateWithTimeout.name,
443
448
  ...result,
444
- workflow: String(result.workflow ?? gate.workflow),
445
- branch: String(result.branch ?? gate.branch),
446
- headSha: String(result.headSha ?? gate.headSha),
447
- timeoutSeconds: gate.timeoutSeconds ?? null,
449
+ workflow: String(result.workflow ?? gateWithTimeout.workflow),
450
+ branch: String(result.branch ?? gateWithTimeout.branch),
451
+ headSha: String(result.headSha ?? gateWithTimeout.headSha),
452
+ timeoutSeconds: gateWithTimeout.timeoutSeconds,
448
453
  cached: false
449
454
  };
450
455
  if (normalized.status === "completed" && normalized.conclusion !== "success") {
451
- workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gate, normalized), {
452
- details: { gate, workflow: normalized }
456
+ workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gateWithTimeout, normalized), {
457
+ details: { gate: gateWithTimeout, workflow: normalized }
453
458
  });
454
459
  }
455
460
  if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
@@ -469,6 +474,7 @@ function githubWorkflowGateEnv(root, gate) {
469
474
  if (!credential.token) return process.env;
470
475
  return {
471
476
  ...process.env,
477
+ TREESEED_GITHUB_TOKEN: credential.token,
472
478
  GH_TOKEN: credential.token,
473
479
  GITHUB_TOKEN: credential.token
474
480
  };
@@ -477,12 +483,127 @@ function githubWorkflowGateEnv(root, gate) {
477
483
  }
478
484
  }
479
485
  const HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
486
+ const HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS = 45 * 60;
480
487
  function hostedDeployGate(gate) {
481
488
  return {
482
489
  ...gate,
483
490
  timeoutSeconds: gate.timeoutSeconds ?? HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS
484
491
  };
485
492
  }
493
+ function saveHostedEnvironmentForBranch(branch) {
494
+ if (branch === STAGING_BRANCH) return "staging";
495
+ if (branch === PRODUCTION_BRANCH) return "prod";
496
+ return null;
497
+ }
498
+ function selectorFromWorkflowHostingGraph(graph) {
499
+ const includesApi = graph.units.some((unit) => unit.id === "api" || unit.config.serviceName === "treeseed-api");
500
+ const scope = graph.environment;
501
+ const target = createPersistentDeployTarget(scope);
502
+ const webDomain = resolveConfiguredSurfaceDomain(graph.deployConfig, target, "web");
503
+ const apiDomain = resolveConfiguredSurfaceDomain(graph.deployConfig, target, "api");
504
+ const domainServiceIds = [
505
+ webDomain,
506
+ webDomain ? `web:${webDomain}` : null,
507
+ apiDomain,
508
+ apiDomain ? `api:${apiDomain}` : null
509
+ ];
510
+ return {
511
+ host: [...new Set([
512
+ ...graph.units.map((unit) => unit.host.id),
513
+ ...includesApi ? ["cloudflare-dns"] : []
514
+ ].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
515
+ serviceId: [...new Set(graph.units.flatMap((unit) => [
516
+ unit.id,
517
+ typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
518
+ ]).concat(domainServiceIds).filter((value) => Boolean(value)))],
519
+ serviceType: [...new Set(graph.units.flatMap((unit) => {
520
+ if (unit.id === "api") return ["api-runtime", "railway-service:api", "custom-domain:api", "dns-record"];
521
+ if (unit.id === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
522
+ if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
523
+ if (unit.host.id === "cloudflare") return ["web-ui", "edge-worker", "content-store", "database", "kv-form-guard", "turnstile-widget", "pages-project", "custom-domain:web", "dns-record"];
524
+ return [];
525
+ }))]
526
+ };
527
+ }
528
+ async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId) {
529
+ const graph = compileTreeseedHostingGraph({ tenantRoot: root, environment });
530
+ const selector = selectorFromWorkflowHostingGraph(graph);
531
+ if (process.env.TREESEED_WORKFLOW_HOSTED_RECONCILE_MODE === "skip") {
532
+ return {
533
+ status: "skipped",
534
+ reason: "disabled",
535
+ environment,
536
+ selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
537
+ selectedResources: graph.units.map((unit) => ({
538
+ id: unit.id,
539
+ host: unit.host.id,
540
+ serviceType: unit.serviceType.id,
541
+ placement: unit.placement,
542
+ serviceName: typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
543
+ }))
544
+ };
545
+ }
546
+ const target = createPersistentDeployTarget(environment);
547
+ const env = {
548
+ ...helpers.context.env,
549
+ ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
550
+ };
551
+ helpers.write(`[save][workflow] Reconciling ${environment} hosted deployments for ${graph.units.length} selected resources.`);
552
+ const reconcile = await reconcileTreeseedTarget({
553
+ tenantRoot: root,
554
+ target,
555
+ env,
556
+ selector,
557
+ dryRun: false,
558
+ write: (line) => helpers.write(`[save][reconcile] ${line}`, "stderr"),
559
+ session: /* @__PURE__ */ new Map([["workflowRunId", workflowRunId]])
560
+ });
561
+ const status = await collectTreeseedReconcileStatus({
562
+ tenantRoot: root,
563
+ target,
564
+ env,
565
+ selector
566
+ });
567
+ if (!status.ready) {
568
+ workflowError("save", "hosted_reconcile_failed", `Hosted reconciliation for ${environment} did not verify:
569
+ ${status.blockers.join("\n")}`, {
570
+ details: { environment, selector, status, reconcile }
571
+ });
572
+ }
573
+ const live = await collectTreeseedLiveHostedServiceChecks({
574
+ tenantRoot: root,
575
+ target: environment,
576
+ strict: true,
577
+ requireLiveRailway: true,
578
+ requireLiveHttp: true,
579
+ env
580
+ });
581
+ const liveFailures = [
582
+ ...live.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
583
+ ...live.liveObservation.issues
584
+ ];
585
+ if (liveFailures.length > 0) {
586
+ workflowError("save", "hosted_live_verification_failed", `Hosted live verification for ${environment} failed:
587
+ ${liveFailures.join("\n")}`, {
588
+ details: { environment, selector, live, reconcile }
589
+ });
590
+ }
591
+ return {
592
+ status: "reconciled",
593
+ environment,
594
+ selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
595
+ selectedResources: graph.units.map((unit) => ({
596
+ id: unit.id,
597
+ host: unit.host.id,
598
+ serviceType: unit.serviceType.id,
599
+ placement: unit.placement,
600
+ serviceName: typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
601
+ })),
602
+ reconcile,
603
+ postApplyStatus: status,
604
+ liveVerification: live
605
+ };
606
+ }
486
607
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
487
608
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
488
609
  const releaseRecord = stringRecord(rootRelease) ?? {};
@@ -548,6 +669,22 @@ ${missing.map((check) => `${check.id}: ${check.path}`).join("\n")}`, {
548
669
  }
549
670
  return report;
550
671
  }
672
+ function ensureTreeseedLocalStateExcluded(root) {
673
+ const gitDir = runTreeseedGit(["rev-parse", "--git-dir"], { cwd: root, mode: "read", allowFailure: true }).stdout.trim();
674
+ if (!gitDir) return;
675
+ const excludePath = resolve(root, gitDir, "info", "exclude");
676
+ const current = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
677
+ const requiredEntries = ["/.treeseed/config/", "/.treeseed/workflow/", "/.treeseed/state/", "/.treeseed/workspace-links.json"];
678
+ const missing = requiredEntries.filter((entry) => !current.split(/\r?\n/u).includes(entry));
679
+ if (missing.length === 0) return;
680
+ mkdirSync(dirname(excludePath), { recursive: true });
681
+ writeFileSync(
682
+ excludePath,
683
+ `${current}${current.endsWith("\n") || current.length === 0 ? "" : "\n"}${missing.join("\n")}
684
+ `,
685
+ "utf8"
686
+ );
687
+ }
551
688
  function workflowError(operation, code, message, options = {}) {
552
689
  throw new TreeseedWorkflowError(operation, code, message, options);
553
690
  }
@@ -650,6 +787,9 @@ function createWorkspaceRootRepoReport(root) {
650
787
  function createWorkspacePackageReports(root) {
651
788
  return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
652
789
  }
790
+ function createManagedWorkflowRepoReports(root) {
791
+ return checkedOutManagedWorkflowRepos(root).map((repo) => createRepoReport(repo.name, repo.dir, currentBranch(repo.dir) || null, hasMeaningfulChanges(repo.dir)));
792
+ }
653
793
  function findReportByName(reports, name) {
654
794
  return reports.find((report) => report.name === name) ?? null;
655
795
  }
@@ -657,15 +797,20 @@ function findReportByPath(reports, path) {
657
797
  return reports.find((report) => report.path === path) ?? null;
658
798
  }
659
799
  function assertWorkspaceClean(root) {
660
- const repoDirs = [repoRoot(root), ...checkedOutWorkspacePackageRepos(root).map((pkg) => pkg.dir)];
800
+ const repoDirs = [repoRoot(root), ...checkedOutManagedWorkflowRepos(root).map((repo) => repo.dir)];
661
801
  assertCleanWorktrees(repoDirs);
662
802
  return repoDirs;
663
803
  }
664
804
  function buildWorkflowResult(operation, cwd, payload, options = {}) {
805
+ const timing = options.timing ?? createTreeseedWorkflowTimer().finish();
665
806
  const resolvedPayload = options.includeFinalState ?? true ? {
666
807
  ...payload,
808
+ timing,
667
809
  finalState: resolveWorkflowStateSnapshot(cwd)
668
- } : payload;
810
+ } : {
811
+ ...payload,
812
+ timing
813
+ };
669
814
  return {
670
815
  schemaVersion: 1,
671
816
  kind: "treeseed.workflow.result",
@@ -756,7 +901,7 @@ function selectWorkflowApplications(root, input = {}) {
756
901
  function singleSelectedWorkflowAppId(selection) {
757
902
  return selection.selected.length === 1 ? selection.selected[0] : void 0;
758
903
  }
759
- async function runWorkflowHostedResourceVerification(operation, root, helpers, environment, options = { enabled: true }) {
904
+ async function workflowHostedVerificationGateRequired(operation, root, helpers, environment, options = { enabled: true }) {
760
905
  if (!options.enabled) return null;
761
906
  const target = environment === "prod" ? "prod" : environment === "local" ? "local" : "staging";
762
907
  const readiness = collectTreeseedDeploymentReadiness({
@@ -771,53 +916,20 @@ ${failures.join("\n")}`, {
771
916
  details: { readiness }
772
917
  });
773
918
  }
774
- const hostingAudit = await runTreeseedHostingAudit({
775
- tenantRoot: root,
776
- environment,
777
- repair: false,
778
- hostKinds: options.appId === "api" ? ["repository"] : void 0,
779
- env: helpers.context.env,
780
- write: (line) => helpers.write(line)
781
- });
782
- const hostedServices = options.live ? await collectTreeseedLiveHostedServiceChecks({
783
- tenantRoot: root,
784
- target,
785
- strict: options.strict === true,
786
- requireLiveRailway: options.strict === true,
787
- requireLiveHttp: options.strict === true,
788
- appId: options.appId,
789
- retry: { attempts: 12, intervalMs: 1e4 },
790
- env: helpers.context.env
791
- }) : collectTreeseedHostedServiceChecks({
792
- tenantRoot: root,
793
- target,
794
- appId: options.appId
919
+ workflowError(operation, "validation_failed", `Hosted live verification for ${target} is reconciler-owned. Use stage/release release-gate:hosted-reconcile and release-gate:live-verify resources, or run trsd reconcile verify with a hosted selector.`, {
920
+ details: {
921
+ readiness,
922
+ live: options.live === true,
923
+ appId: options.appId ?? null
924
+ }
795
925
  });
796
- if (options.strict && !hostingAudit.ok) {
797
- workflowError(operation, "validation_failed", `Hosting audit failed for ${hostingAudit.environment}: ${hostingAudit.blockers.join("\n")}`, {
798
- details: { hostingAudit, hostedServices, readiness }
799
- });
800
- }
801
- if (options.strict && hostedServices.summary.failed > 0) {
802
- const failures = hostedServices.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || check.description}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
803
- workflowError(operation, "validation_failed", `Hosted service checks failed for ${hostedServices.target}:
804
- ${failures.join("\n")}`, {
805
- details: { hostingAudit, hostedServices, readiness }
806
- });
807
- }
808
- return {
809
- hostingAudit,
810
- hostedServices,
811
- readiness,
812
- live: options.live === true
813
- };
814
926
  }
815
927
  function normalizeExecutionMode(input) {
816
928
  return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
817
929
  }
818
930
  function submodulePointerForRef(repoDir, ref, relativeDir) {
819
931
  try {
820
- const output = run("git", ["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
932
+ const output = runGit(["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
821
933
  if (!output) {
822
934
  return null;
823
935
  }
@@ -905,13 +1017,13 @@ function stablePackageGitReferences(root, versions) {
905
1017
  }
906
1018
  function gitObjectCommit(repoDir, ref) {
907
1019
  try {
908
- return run("git", ["rev-list", "-n", "1", ref], { cwd: repoDir, capture: true }).trim() || null;
1020
+ return runGit(["rev-list", "-n", "1", ref], { cwd: repoDir, capture: true }).trim() || null;
909
1021
  } catch {
910
1022
  return null;
911
1023
  }
912
1024
  }
913
1025
  function remoteTagCommit(repoDir, tagName) {
914
- const output = run("git", ["ls-remote", "origin", `refs/tags/${tagName}`, `refs/tags/${tagName}^{}`], { cwd: repoDir, capture: true }).trim();
1026
+ const output = runGit(["ls-remote", "origin", `refs/tags/${tagName}`, `refs/tags/${tagName}^{}`], { cwd: repoDir, capture: true }).trim();
915
1027
  if (!output) return null;
916
1028
  const peeled = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
917
1029
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
@@ -923,14 +1035,14 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
923
1035
  throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
924
1036
  }
925
1037
  if (!localCommit) {
926
- run("git", ["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
1038
+ runGit(["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
927
1039
  }
928
1040
  const remoteCommit = remoteTagCommit(repoDir, tagName);
929
1041
  if (remoteCommit && remoteCommit !== commitSha) {
930
1042
  throw new Error(`Release tag ${tagName} already exists on origin at ${remoteCommit}, expected ${commitSha}.`);
931
1043
  }
932
1044
  if (!remoteCommit) {
933
- run("git", ["push", "origin", tagName], { cwd: repoDir });
1045
+ runGit(["push", "origin", tagName], { cwd: repoDir });
934
1046
  }
935
1047
  return {
936
1048
  tagName,
@@ -938,12 +1050,28 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
938
1050
  remote: remoteCommit ? "existing" : "pushed"
939
1051
  };
940
1052
  }
1053
+ function promoteCommitToProductionBranch(repoDir, commitSha) {
1054
+ const expectedBefore = remoteBranchExists(repoDir, PRODUCTION_BRANCH) ? remoteHeadCommit(repoDir, PRODUCTION_BRANCH) : null;
1055
+ const lease = expectedBefore ? `--force-with-lease=refs/heads/${PRODUCTION_BRANCH}:${expectedBefore}` : "--force-with-lease";
1056
+ runGit(["push", lease, "origin", `${commitSha}:refs/heads/${PRODUCTION_BRANCH}`], { cwd: repoDir });
1057
+ const observed = remoteHeadCommit(repoDir, PRODUCTION_BRANCH);
1058
+ if (observed !== commitSha) {
1059
+ throw new Error(`Production promotion verification failed; expected ${commitSha}, observed ${observed}.`);
1060
+ }
1061
+ return {
1062
+ targetBranch: PRODUCTION_BRANCH,
1063
+ expectedBefore,
1064
+ commitSha,
1065
+ pushed: true,
1066
+ verified: true
1067
+ };
1068
+ }
941
1069
  function commitAllIfChanged(repoDir, message) {
942
- run("git", ["add", "-A"], { cwd: repoDir });
1070
+ runGit(["add", "-A"], { cwd: repoDir });
943
1071
  if (!hasMeaningfulChanges(repoDir)) {
944
1072
  return { committed: false, commitSha: headCommit(repoDir) };
945
1073
  }
946
- run("git", ["commit", "-m", message], { cwd: repoDir });
1074
+ runGit(["commit", "-m", message], { cwd: repoDir });
947
1075
  return { committed: true, commitSha: headCommit(repoDir) };
948
1076
  }
949
1077
  function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
@@ -1021,6 +1149,11 @@ function defaultCiWorkflows(kind, branch) {
1021
1149
  }
1022
1150
  return ["verify.yml"];
1023
1151
  }
1152
+ function packageCiWorkflowsForRepo(repoDir) {
1153
+ const adapters = discoverTreeseedPackageAdapters(workspaceRoot(repoDir));
1154
+ const adapter = adapters.find((candidate) => resolve(candidate.dir) === resolve(repoDir));
1155
+ return adapter ? [hostedWorkflowForPackage(adapter)] : null;
1156
+ }
1024
1157
  function githubRepositoryForRepo(repoDir) {
1025
1158
  try {
1026
1159
  return resolveGitHubRepositorySlug(repoDir);
@@ -1030,7 +1163,7 @@ function githubRepositoryForRepo(repoDir) {
1030
1163
  }
1031
1164
  function ciTargetForRepo(repo, kind, input, workflowOverrides) {
1032
1165
  const branch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : repo.branchName;
1033
- const workflows = workflowOverrides.length > 0 ? workflowOverrides : defaultCiWorkflows(kind, branch);
1166
+ const workflows = workflowOverrides.length > 0 ? workflowOverrides : kind === "package" ? packageCiWorkflowsForRepo(repo.path) ?? defaultCiWorkflows(kind, branch) : defaultCiWorkflows(kind, branch);
1034
1167
  return {
1035
1168
  name: repo.name,
1036
1169
  repoPath: repo.path,
@@ -1083,7 +1216,7 @@ async function createCiResult(cwd, input) {
1083
1216
  function createTasksResult(cwd) {
1084
1217
  const tenantRoot = cwd;
1085
1218
  const repoDir = gitWorkflowRoot(tenantRoot);
1086
- const deployConfig = loadCliDeployConfig(tenantRoot);
1219
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
1087
1220
  const dirty = gitStatusPorcelain(repoDir).length > 0;
1088
1221
  const tasks = listTaskBranches(repoDir).map((branch) => {
1089
1222
  const previewState = loadDeployState(tenantRoot, deployConfig, {
@@ -1332,7 +1465,32 @@ function findAutoResumableSaveRun(root, branch) {
1332
1465
  if (branch === STAGING_BRANCH && (hasMeaningfulChanges(repoRoot(root)) || checkedOutWorkspacePackageRepos(root).some((repo) => hasMeaningfulChanges(repo.dir)))) {
1333
1466
  return null;
1334
1467
  }
1335
- return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
1468
+ const currentHeads = Object.fromEntries([
1469
+ ["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
1470
+ ...checkedOutWorkspacePackageRepos(root).map((repo) => [
1471
+ repo.name,
1472
+ runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
1473
+ ])
1474
+ ]);
1475
+ return listInterruptedWorkflowRuns(root).find((journal) => {
1476
+ if (journal.command !== "save" || !journal.resumable || journal.session.branchName !== branch) {
1477
+ return false;
1478
+ }
1479
+ const classification = classifyWorkflowRunJournal(journal, {
1480
+ currentBranch: branch,
1481
+ currentHeads
1482
+ });
1483
+ if (classification.state === "resumable") {
1484
+ return true;
1485
+ }
1486
+ if (classification.state === "stale") {
1487
+ archiveWorkflowRun(root, journal.runId, {
1488
+ ...classification,
1489
+ reasons: ["save auto-resume skipped stale failed save", ...classification.reasons]
1490
+ });
1491
+ }
1492
+ return false;
1493
+ }) ?? null;
1336
1494
  }
1337
1495
  function workflowFileExists(repoPath, workflow) {
1338
1496
  return existsSync(resolve(repoPath, ".github", "workflows", workflow));
@@ -1426,7 +1584,7 @@ function generatedReleaseMetadataFiles(repoDir) {
1426
1584
  return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
1427
1585
  if (existsSync(resolve(repoDir, filePath))) return true;
1428
1586
  try {
1429
- run("git", ["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
1587
+ runGit(["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
1430
1588
  return true;
1431
1589
  } catch {
1432
1590
  return false;
@@ -1455,12 +1613,12 @@ function restoreReleaseGeneratedMetadata(repo) {
1455
1613
  const restored = [];
1456
1614
  const skipped = [];
1457
1615
  for (const filePath of repo.files) {
1458
- const status = run("git", ["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
1616
+ const status = runGit(["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
1459
1617
  if (!status.trim()) {
1460
1618
  skipped.push(filePath);
1461
1619
  continue;
1462
1620
  }
1463
- run("git", ["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
1621
+ runGit(["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
1464
1622
  restored.push(filePath);
1465
1623
  }
1466
1624
  return { restored, skipped };
@@ -1697,7 +1855,12 @@ function failWorkflowRun(root, runId, error, recovery) {
1697
1855
  releaseWorkflowLock(root, runId);
1698
1856
  }
1699
1857
  function validatePackageReleaseWorkflows(root, packageNames) {
1700
- const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => !existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))).map((pkg) => pkg.name);
1858
+ const adapterById = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [adapter.id, adapter]));
1859
+ const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => {
1860
+ const adapter = adapterById.get(pkg.name);
1861
+ const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
1862
+ return !existsSync(resolve(pkg.dir, workflow));
1863
+ }).map((pkg) => pkg.name);
1701
1864
  if (missing.length > 0) {
1702
1865
  workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
1703
1866
  details: {
@@ -1706,6 +1869,26 @@ function validatePackageReleaseWorkflows(root, packageNames) {
1706
1869
  });
1707
1870
  }
1708
1871
  }
1872
+ function releaseWorkflowForPackage(root, packageName) {
1873
+ const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === packageName || entry.name === packageName);
1874
+ const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
1875
+ return workflow.split("/").at(-1) || "publish.yml";
1876
+ }
1877
+ function prepareAdapterReleaseMetadata(root, pkg, version) {
1878
+ const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
1879
+ if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
1880
+ run("tsx", ["scripts/bump-release-version.ts", version], { cwd: pkg.dir });
1881
+ return { status: "updated", adapter: adapter.id, command: "tsx scripts/bump-release-version.ts" };
1882
+ }
1883
+ if (existsSync(resolve(pkg.dir, "package.json"))) {
1884
+ return {
1885
+ status: "npm-install",
1886
+ adapter: adapter?.id ?? pkg.name,
1887
+ ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
1888
+ };
1889
+ }
1890
+ return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
1891
+ }
1709
1892
  function validateStagingWorkflowContracts(root) {
1710
1893
  if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
1711
1894
  return;
@@ -1725,17 +1908,38 @@ function validateStagingWorkflowContracts(root) {
1725
1908
  function shouldSkipReleaseInstall() {
1726
1909
  return process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
1727
1910
  }
1911
+ function npmCommandForWorkflowSpawn(args) {
1912
+ if (process.platform === "win32") {
1913
+ return { command: "npm", args };
1914
+ }
1915
+ return {
1916
+ command: "bash",
1917
+ args: [
1918
+ "-lc",
1919
+ 'ulimit -n 65535 2>/dev/null || ulimit -n 32768 2>/dev/null || ulimit -n 16384 2>/dev/null || true; exec npm "$@"',
1920
+ "npm-fd-guard",
1921
+ ...args
1922
+ ]
1923
+ };
1924
+ }
1728
1925
  function runReleaseNpmInstall(repoDir, options = {}) {
1729
1926
  if (shouldSkipReleaseInstall()) {
1730
1927
  return { status: "skipped", reason: "disabled" };
1731
1928
  }
1732
1929
  const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
1733
- const result = spawnSync("npm", args, {
1930
+ const spawnCommand = npmCommandForWorkflowSpawn(args);
1931
+ const result = spawnSync(spawnCommand.command, spawnCommand.args, {
1734
1932
  cwd: repoDir,
1735
1933
  env: {
1736
1934
  ...process.env,
1737
1935
  npm_config_audit: "false",
1738
- npm_config_fund: "false"
1936
+ npm_config_fetch_retries: "2",
1937
+ npm_config_fund: "false",
1938
+ npm_config_foreground_scripts: "true",
1939
+ npm_config_loglevel: "warn",
1940
+ npm_config_maxsockets: "4",
1941
+ npm_config_prefer_offline: "true",
1942
+ npm_config_progress: "false"
1739
1943
  },
1740
1944
  stdio: "pipe",
1741
1945
  encoding: "utf8"
@@ -1770,7 +1974,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1770
1974
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1771
1975
  checkoutBranch(repoDir, STAGING_BRANCH);
1772
1976
  try {
1773
- run("git", ["merge-base", "--is-ancestor", `origin/${PRODUCTION_BRANCH}`, "HEAD"], { cwd: repoDir, capture: true });
1977
+ runGit(["merge-base", "--is-ancestor", `origin/${PRODUCTION_BRANCH}`, "HEAD"], { cwd: repoDir, capture: true });
1774
1978
  return {
1775
1979
  status: "up-to-date",
1776
1980
  merged: false,
@@ -1782,7 +1986,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1782
1986
  } catch {
1783
1987
  }
1784
1988
  try {
1785
- run("git", ["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
1989
+ runGit(["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
1786
1990
  } catch (error) {
1787
1991
  const report = collectMergeConflictReport(repoDir);
1788
1992
  throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
@@ -1835,69 +2039,6 @@ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, optio
1835
2039
  packageStagingPointerCommit: pointerSync.commitSha
1836
2040
  };
1837
2041
  }
1838
- function collectActiveDevTagReferences(root) {
1839
- return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
1840
- }
1841
- function normalizeIncludePackages(value) {
1842
- if (!value) return null;
1843
- const values = Array.isArray(value) ? value : String(value).split(",");
1844
- const normalized = values.map((entry) => String(entry).trim()).filter(Boolean);
1845
- return normalized.length > 0 ? new Set(normalized) : null;
1846
- }
1847
- function normalizeDevTagBranchScope(value) {
1848
- return value === "staging" || value === "preview" || value === "all" ? value : "all";
1849
- }
1850
- function packageJsonVersion(repoDir) {
1851
- const packageJson = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
1852
- return String(packageJson.version ?? "");
1853
- }
1854
- function collectStaleDevTagCleanupReports(root, input, options) {
1855
- const includePackages = normalizeIncludePackages(input.includePackages);
1856
- const branchScope = normalizeDevTagBranchScope(input.branchScope);
1857
- const activeDevTags = collectActiveDevTagReferences(root);
1858
- const repos = checkedOutWorkspacePackageRepos(root).filter((pkg) => !includePackages || includePackages.has(pkg.name)).map((pkg) => {
1859
- const currentVersion = packageJsonVersion(pkg.dir);
1860
- const report = options.execute ? cleanupStaleTreeseedDevTags({
1861
- repoDir: pkg.dir,
1862
- packageName: pkg.name,
1863
- currentVersion,
1864
- activeReferences: activeDevTags,
1865
- branchScope
1866
- }) : {
1867
- ...collectTreeseedDevTagCleanupPlan({
1868
- repoDir: pkg.dir,
1869
- packageName: pkg.name,
1870
- currentVersion,
1871
- activeReferences: activeDevTags,
1872
- branchScope
1873
- }),
1874
- status: "planned",
1875
- candidateCount: 0,
1876
- cleaned: [],
1877
- cleanedCount: 0,
1878
- skippedCount: 0
1879
- };
1880
- if (!options.execute) {
1881
- const planned = report;
1882
- planned.candidateCount = planned.candidates.length;
1883
- planned.skippedCount = planned.skipped.length;
1884
- }
1885
- return {
1886
- name: pkg.name,
1887
- path: relative(root, pkg.dir),
1888
- ...report
1889
- };
1890
- });
1891
- return {
1892
- status: options.execute ? "completed" : "planned",
1893
- branchScope,
1894
- includePackages: includePackages ? [...includePackages].sort() : [],
1895
- repos,
1896
- candidateCount: repos.reduce((total, repo) => total + Number(repo.candidateCount ?? 0), 0),
1897
- cleanedCount: repos.reduce((total, repo) => total + Number(repo.cleanedCount ?? 0), 0),
1898
- skippedCount: repos.reduce((total, repo) => total + Number(repo.skippedCount ?? 0), 0)
1899
- };
1900
- }
1901
2042
  function releasePlanVersionMap(plannedVersions) {
1902
2043
  return new Map(
1903
2044
  Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
@@ -1929,38 +2070,233 @@ function stableDependencyVersionsForReleaseLine(root, options) {
1929
2070
  }
1930
2071
  return versions;
1931
2072
  }
1932
- function assertReleaseCandidatePassed(operation, report) {
1933
- if (report.status === "passed") {
1934
- return;
1935
- }
1936
- const rendered = report.failures.map((failure) => `- ${failure.scope}${failure.provider ? ` ${failure.provider}` : ""}: ${failure.message}`);
1937
- workflowError(operation, "validation_failed", [
1938
- "Treeseed release-candidate readiness failed.",
1939
- ...rendered
1940
- ].join("\n"), {
1941
- details: {
1942
- releaseCandidate: report
1943
- }
1944
- });
2073
+ function releaseCandidateProofDriver(mode, lane = "fast") {
2074
+ if (lane === "promotion" || mode === "strict") return "github-hosted";
2075
+ return "local";
1945
2076
  }
1946
- async function runReleaseCandidateForPlan(operation, root, plannedRelease, options = {}) {
1947
- const plannedVersions = plannedRelease.plannedVersions && typeof plannedRelease.plannedVersions === "object" && !Array.isArray(plannedRelease.plannedVersions) ? plannedRelease.plannedVersions : {};
1948
- const stableDependencyVersions = plannedRelease.stableDependencyVersions && typeof plannedRelease.stableDependencyVersions === "object" && !Array.isArray(plannedRelease.stableDependencyVersions) ? plannedRelease.stableDependencyVersions : {};
2077
+ async function runReleaseCandidateProofForPlan(operation, root, plannedRelease, options = {}) {
1949
2078
  const packageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
1950
- const report = await runReleaseCandidateGate({
2079
+ const mode = options.mode ?? normalizeReleaseCandidateMode(void 0, operation);
2080
+ const driver = releaseCandidateProofDriver(mode, options.lane ?? "fast");
2081
+ const proof = await runTreeseedProof({
1951
2082
  root,
1952
- plannedVersions: { ...stableDependencyVersions, ...plannedVersions },
1953
- selectedPackageNames: packageSelection.selected,
1954
- allowReuse: options.allowReuse,
1955
- mode: options.mode ?? normalizeReleaseCandidateMode(void 0, operation)
2083
+ target: operation === "release" ? "prod" : "staging",
2084
+ driver,
2085
+ write: options.write
1956
2086
  });
1957
- assertReleaseCandidatePassed(operation, report);
1958
- return report;
2087
+ if (proof.failures.length > 0) {
2088
+ const first = proof.failures[0];
2089
+ workflowError(operation, "validation_failed", [
2090
+ "Treeseed release-candidate proof failed.",
2091
+ `- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
2092
+ first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
2093
+ driver === "github-hosted" ? "Hosted GitHub workflow proof is authoritative; local action simulation is advisory." : "Local proof is exact-input cached in the proof ledger and reruns only missing or invalid subjects."
2094
+ ].filter(Boolean).join("\n"), { details: { proof } });
2095
+ }
2096
+ return {
2097
+ mode,
2098
+ driver,
2099
+ selectedPackageNames: packageSelection.selected,
2100
+ proof,
2101
+ status: "passed",
2102
+ reused: proof.reused.length,
2103
+ records: proof.records.length
2104
+ };
2105
+ }
2106
+ function parseProofOlderThan(value) {
2107
+ if (!value) return 30 * 24 * 60 * 60 * 1e3;
2108
+ const match = value.trim().match(/^(\d+)([smhd])?$/u);
2109
+ if (!match) return 30 * 24 * 60 * 60 * 1e3;
2110
+ const amount = Number(match[1]);
2111
+ const unit = match[2] ?? "d";
2112
+ if (!Number.isFinite(amount) || amount < 0) return 30 * 24 * 60 * 60 * 1e3;
2113
+ if (unit === "s") return amount * 1e3;
2114
+ if (unit === "m") return amount * 60 * 1e3;
2115
+ if (unit === "h") return amount * 60 * 60 * 1e3;
2116
+ return amount * 24 * 60 * 60 * 1e3;
2117
+ }
2118
+ async function workflowProof(helpers, input = {}) {
2119
+ try {
2120
+ return await withContextEnv(helpers.context.env, async () => {
2121
+ const timer = createTreeseedWorkflowTimer();
2122
+ const tenantRoot = resolveProjectRootOrThrow("proof", helpers.cwd());
2123
+ const root = workspaceRoot(tenantRoot);
2124
+ const action = input.action ?? (input.plan || input.dryRun ? "plan" : "status");
2125
+ const target = input.target ?? "staging";
2126
+ const driver = input.driver ?? "github-hosted";
2127
+ const executionMode = action === "plan" || input.plan || input.dryRun ? "plan" : "execute";
2128
+ let payload;
2129
+ if (action === "run") {
2130
+ const result = await timer.phaseAsync("proof-run", "Run release proof subjects", () => runTreeseedProof({
2131
+ root,
2132
+ target,
2133
+ driver,
2134
+ subject: input.subject ?? null,
2135
+ write: (line, stream) => helpers.write(line, stream)
2136
+ }));
2137
+ payload = {
2138
+ action,
2139
+ target,
2140
+ driver,
2141
+ subject: input.subject ?? null,
2142
+ ...result,
2143
+ authority: driver === "github-hosted" ? "authoritative" : "advisory"
2144
+ };
2145
+ if (result.failures.length > 0) {
2146
+ const first = result.failures[0];
2147
+ workflowError("proof", "validation_failed", [
2148
+ "Treeseed release proof failed.",
2149
+ `- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
2150
+ first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
2151
+ "Hosted GitHub workflow proof is authoritative; local action simulation is advisory."
2152
+ ].filter(Boolean).join("\n"), { details: { proof: payload } });
2153
+ }
2154
+ } else if (action === "clean") {
2155
+ payload = {
2156
+ action,
2157
+ target,
2158
+ ...timer.phase("proof-clean", "Clean old proof records", () => cleanProofLedger(root, {
2159
+ olderThanMs: parseProofOlderThan(input.olderThan)
2160
+ }))
2161
+ };
2162
+ } else if (action === "failures") {
2163
+ const ledger = timer.phase("proof-failures", "Inspect failed proof records", () => summarizeTreeseedProofLedger(root));
2164
+ payload = { action, target, failures: ledger.failures, summary: ledger.summary };
2165
+ } else if (action === "explain") {
2166
+ const ledger = timer.phase("proof-explain", "Explain proof duration and reuse", () => summarizeTreeseedProofLedger(root));
2167
+ payload = {
2168
+ action,
2169
+ target,
2170
+ latest: ledger.latest,
2171
+ slowest: ledger.slowest,
2172
+ reuse: {
2173
+ passed: ledger.summary.reusable,
2174
+ rerun: Math.max(0, ledger.summary.records - ledger.summary.reusable),
2175
+ blocked: ledger.summary.failed
2176
+ },
2177
+ summary: ledger.summary
2178
+ };
2179
+ } else {
2180
+ const plan = timer.phase("proof-plan", "Plan release proof subjects", () => buildTreeseedProofPlan({
2181
+ root,
2182
+ target,
2183
+ driver,
2184
+ subject: input.subject ?? null
2185
+ }));
2186
+ payload = { action, target, driver, plan, authority: driver === "github-hosted" ? "authoritative" : "advisory" };
2187
+ }
2188
+ const timing = timer.finish();
2189
+ return buildWorkflowResult("proof", root, payload, {
2190
+ executionMode,
2191
+ summary: action === "run" ? "Treeseed release proof run completed." : "Treeseed release proof report ready.",
2192
+ timing,
2193
+ nextSteps: createNextSteps([
2194
+ { operation: "proof", reason: "Run missing authoritative hosted proof before promotion.", input: { action: "run", target, driver: "github-hosted" } }
2195
+ ])
2196
+ });
2197
+ });
2198
+ } catch (error) {
2199
+ toError("proof", error);
2200
+ }
2201
+ }
2202
+ function normalizeReleaseCandidatePackages(value) {
2203
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
2204
+ if (typeof value === "string" && value.trim()) return [value.trim()];
2205
+ return [];
2206
+ }
2207
+ async function workflowReleaseCandidate(helpers, input = {}) {
2208
+ try {
2209
+ return await withContextEnv(helpers.context.env, async () => {
2210
+ const timer = createTreeseedWorkflowTimer();
2211
+ const tenantRoot = resolveProjectRootOrThrow("release-candidate", helpers.cwd());
2212
+ const root = workspaceRoot(tenantRoot);
2213
+ const executionMode = normalizeExecutionMode(input);
2214
+ const selectedPackageNames = normalizeReleaseCandidatePackages(input.package);
2215
+ const mode = input.mode ?? "strict";
2216
+ const driver = input.verifyDriver === "local" ? "local" : input.verifyDriver === "action" ? "act" : releaseCandidateProofDriver(mode === "skip" ? "hybrid" : mode);
2217
+ const proofSubject = selectedPackageNames.length === 1 ? `package:${selectedPackageNames[0]}` : null;
2218
+ const plan = timer.phase("proof-plan", "Plan release-candidate proof subjects", () => buildTreeseedProofPlan({
2219
+ root,
2220
+ target: "staging",
2221
+ driver,
2222
+ subject: proofSubject
2223
+ }));
2224
+ const payload = {
2225
+ mode,
2226
+ driver,
2227
+ verifyDriver: input.verifyDriver ?? "auto",
2228
+ selectedPackageNames,
2229
+ keepWorkspace: input.keepWorkspace === true,
2230
+ plan,
2231
+ plannedSteps: [
2232
+ { id: "proof-plan", description: "Discover reusable exact-input proof records for package subjects." },
2233
+ { id: "proof-run", description: "Run only missing or invalid release proof nodes." }
2234
+ ]
2235
+ };
2236
+ if (executionMode === "plan") {
2237
+ return buildWorkflowResult("release-candidate", root, payload, {
2238
+ executionMode,
2239
+ summary: "Treeseed release-candidate proof plan ready.",
2240
+ nextSteps: createNextSteps([
2241
+ { operation: "release-candidate", reason: "Run missing proof nodes before promotion." }
2242
+ ])
2243
+ });
2244
+ }
2245
+ if (mode === "skip") {
2246
+ return buildWorkflowResult("release-candidate", root, {
2247
+ ...payload,
2248
+ status: "skipped",
2249
+ failures: []
2250
+ }, {
2251
+ summary: "Treeseed release-candidate proof skipped.",
2252
+ timing: timer.finish()
2253
+ });
2254
+ }
2255
+ const proof = await timer.phaseAsync("proof-run", "Run release-candidate proof nodes", () => runTreeseedProof({
2256
+ root,
2257
+ target: "staging",
2258
+ driver,
2259
+ subject: proofSubject,
2260
+ write: (line, stream) => helpers.write(line, stream)
2261
+ }));
2262
+ const resultPayload = {
2263
+ ...payload,
2264
+ proof,
2265
+ failures: proof.failures
2266
+ };
2267
+ if (proof.failures.length > 0) {
2268
+ const first = proof.failures[0];
2269
+ workflowError("release-candidate", "validation_failed", [
2270
+ "Treeseed release-candidate proof failed.",
2271
+ `- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
2272
+ first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null
2273
+ ].filter(Boolean).join("\n"), {
2274
+ details: { releaseCandidate: resultPayload }
2275
+ });
2276
+ }
2277
+ return buildWorkflowResult("release-candidate", root, resultPayload, {
2278
+ summary: "Treeseed release-candidate proof passed.",
2279
+ timing: timer.finish(),
2280
+ nextSteps: createNextSteps([
2281
+ { operation: "stage", reason: "Run stage after the matching release proof passes." }
2282
+ ])
2283
+ });
2284
+ });
2285
+ } catch (error) {
2286
+ toError("release-candidate", error);
2287
+ }
1959
2288
  }
1960
2289
  function buildReleasePlanSnapshot(input) {
1961
2290
  const selectedPackageNames = new Set(input.packageSelection.selected);
1962
2291
  const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
1963
2292
  const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
2293
+ if (input.repairVersionLine !== true) {
2294
+ for (const adapter of discoverTreeseedPackageAdapters(input.root)) {
2295
+ if (!selectedPackageNames.has(adapter.id) || versionPlan.versions.has(adapter.id) || !adapter.version) continue;
2296
+ versionPlan.selected.add(adapter.id);
2297
+ versionPlan.versions.set(adapter.id, incrementVersion(adapter.version, input.level));
2298
+ }
2299
+ }
1964
2300
  const plannedSelected = [...versionPlan.selected].filter((name) => versionPlan.versions.has(name));
1965
2301
  const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(input.packageSelection.changed.filter((name) => plannedSelected.includes(name))));
1966
2302
  const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
@@ -1999,7 +2335,7 @@ function buildReleasePlanSnapshot(input) {
1999
2335
  plannedDevReferenceRewrites,
2000
2336
  plannedPublishWaits: plannedPackageSelection.selected.map((name) => ({
2001
2337
  name,
2002
- workflow: "publish.yml",
2338
+ workflow: releaseWorkflowForPackage(input.root, name),
2003
2339
  branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
2004
2340
  status: "planned"
2005
2341
  })),
@@ -2018,7 +2354,6 @@ function buildReleasePlanSnapshot(input) {
2018
2354
  })),
2019
2355
  { id: "release-root", description: `Release market ${rootVersion}` },
2020
2356
  { id: "release-back-merge", description: "Back-merge production release history into staging" },
2021
- { id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
2022
2357
  { id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
2023
2358
  ],
2024
2359
  blockers: input.blockers
@@ -2059,10 +2394,26 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames, ciMode)
2059
2394
  if (ciMode === "off") {
2060
2395
  return;
2061
2396
  }
2062
- assertHostedGitHubWorkflowAuthReady("release", root);
2397
+ const values = resolveTreeseedMachineEnvironmentValues(root, "prod");
2398
+ const missing = [];
2063
2399
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2064
2400
  if (!selectedPackageNames.has(pkg.name)) continue;
2065
- resolveGitHubRepositorySlug(pkg.dir);
2401
+ const repository = resolveGitHubRepositorySlug(pkg.dir);
2402
+ const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
2403
+ if (!credential.token) {
2404
+ missing.push({ packageName: pkg.name, repository: credential.repository, envName: credential.envName });
2405
+ }
2406
+ }
2407
+ if (missing.length > 0) {
2408
+ workflowError(
2409
+ "release",
2410
+ "github_auth_unavailable",
2411
+ [
2412
+ "Treeseed release automation requires Treeseed-prefixed GitHub credentials.",
2413
+ ...missing.map((pkg) => `- ${pkg.packageName}: configure ${pkg.envName} for ${pkg.repository}, or TREESEED_GITHUB_TOKEN as a fallback.`)
2414
+ ].join("\n"),
2415
+ { details: { missing } }
2416
+ );
2066
2417
  }
2067
2418
  }
2068
2419
  function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
@@ -2089,31 +2440,31 @@ function assertSessionBranchSafety(operation, session, {
2089
2440
  requireCurrentBranch = false,
2090
2441
  allowPackageReposWithoutOrigin = false
2091
2442
  } = {}) {
2092
- const detached = session.packageRepos.filter((repo) => repo.detached).map((repo) => repo.name);
2443
+ const detached = session.managedRepos.filter((repo) => repo.kind !== "fixture" && repo.detached).map((repo) => repo.name);
2093
2444
  if (detached.length > 0) {
2094
- workflowError(operation, "validation_failed", `Detached package heads detected: ${detached.join(", ")}.`, {
2445
+ workflowError(operation, "validation_failed", `Detached managed repository heads detected: ${detached.join(", ")}.`, {
2095
2446
  details: { detached }
2096
2447
  });
2097
2448
  }
2098
2449
  if (requireCleanPackages) {
2099
- const dirty = session.packageRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
2450
+ const dirty = session.managedRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
2100
2451
  if (dirty.length > 0) {
2101
- workflowError(operation, "validation_failed", `Dirty package repos block ${operation}: ${dirty.join(", ")}.`, {
2452
+ workflowError(operation, "validation_failed", `Dirty managed repos block ${operation}: ${dirty.join(", ")}.`, {
2102
2453
  details: { dirty }
2103
2454
  });
2104
2455
  }
2105
2456
  }
2106
2457
  if (requireCurrentBranch && session.branchName) {
2107
- const missing = session.packageRepos.filter((repo) => repo.branchName !== session.branchName).map((repo) => ({ name: repo.name, branchName: repo.branchName }));
2458
+ const missing = session.managedRepos.filter((repo) => repo.kind !== "fixture").filter((repo) => repo.branchName !== session.branchName).map((repo) => ({ name: repo.name, branchName: repo.branchName }));
2108
2459
  if (missing.length > 0) {
2109
- workflowError(operation, "validation_failed", `Package branch alignment is required for ${operation}.`, {
2460
+ workflowError(operation, "validation_failed", `Managed repository branch alignment is required for ${operation}.`, {
2110
2461
  details: { expectedBranch: session.branchName, repos: missing }
2111
2462
  });
2112
2463
  }
2113
2464
  }
2114
2465
  const missingOriginRepos = [
2115
2466
  session.rootRepo,
2116
- ...allowPackageReposWithoutOrigin ? [] : session.packageRepos
2467
+ ...allowPackageReposWithoutOrigin ? [] : session.managedRepos
2117
2468
  ].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
2118
2469
  if (missingOriginRepos.length > 0 && operation !== "destroy") {
2119
2470
  workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
@@ -2122,7 +2473,7 @@ function assertSessionBranchSafety(operation, session, {
2122
2473
  }
2123
2474
  }
2124
2475
  function previewStateFor(tenantRoot, branchName) {
2125
- const deployConfig = loadCliDeployConfig(tenantRoot);
2476
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
2126
2477
  return loadDeployState(tenantRoot, deployConfig, {
2127
2478
  target: createBranchPreviewDeployTarget(branchName)
2128
2479
  });
@@ -2135,59 +2486,71 @@ function branchPreviewInitialized(tenantRoot, branchName) {
2135
2486
  return false;
2136
2487
  }
2137
2488
  }
2138
- async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
2139
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
2140
- assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
2141
- const target = createBranchPreviewDeployTarget(branchName);
2142
- const existingState = previewStateFor(tenantRoot, branchName);
2143
- if (initialize && !existingState.readiness?.initialized) {
2144
- validateDeployPrerequisites(tenantRoot, { requireRemote: true });
2145
- await reconcileTreeseedTarget({
2146
- tenantRoot,
2147
- target,
2148
- env: { ...process.env, ...context.env ?? {} },
2149
- write: (line) => context.write?.(line)
2150
- });
2151
- runRemoteD1Migrations(tenantRoot, { target });
2152
- } else {
2153
- assertDeploymentInitialized(tenantRoot, { target });
2154
- runRemoteD1Migrations(tenantRoot, { target });
2155
- }
2156
- runTenantDeployPreflight({ cwd: tenantRoot, scope: "staging" });
2157
- const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
2158
- runNodeScript("tenant-build", context, tenantRoot, "tenant build");
2159
- const deployResult = spawnSync(process.execPath, [resolveWranglerBin(), "deploy", "--config", wranglerPath], {
2160
- cwd: tenantRoot,
2161
- env: { ...process.env, ...context.env ?? {} },
2162
- stdio: "inherit"
2489
+ async function reconcileWorkflowBranchPreview(tenantRoot, branchName, context, { initialize }) {
2490
+ return reconcileTreeseedBranchPreview({
2491
+ root: tenantRoot,
2492
+ branch: branchName,
2493
+ dryRun: false,
2494
+ execute: true,
2495
+ workflowRunId: context.workflow?.resumeRunId ?? void 0,
2496
+ initialize,
2497
+ env: context.env
2498
+ });
2499
+ }
2500
+ async function reconcileTreeseedBranchPreview(input) {
2501
+ const target = { kind: "branch", branchName: input.branch };
2502
+ const graph = compileTreeseedDesiredResourceGraph({ tenantRoot: input.root, target });
2503
+ const selector = {
2504
+ environment: "staging",
2505
+ resourceKind: ["branch-preview"],
2506
+ ...input.appId?.length ? { appId: input.appId } : {}
2507
+ };
2508
+ const units = compileTreeseedDesiredUnitsFromGraph(graph, selector);
2509
+ const reconcile = input.dryRun || !input.execute ? await planTreeseedReconciliation({ tenantRoot: input.root, target, env: input.env ?? process.env, units, selector }) : await reconcileTreeseedTarget({
2510
+ tenantRoot: input.root,
2511
+ target,
2512
+ env: input.env ?? process.env,
2513
+ units,
2514
+ selector,
2515
+ dryRun: false
2163
2516
  });
2164
- if ((deployResult.status ?? 1) !== 0) {
2165
- workflowError("switch", "unsupported_state", "Preview deployment failed.", {
2166
- exitCode: deployResult.status ?? 1,
2167
- details: { branchName, wranglerPath }
2168
- });
2169
- }
2170
- const state = finalizeDeploymentState(tenantRoot, { target });
2171
2517
  return {
2172
- initialized: existingState.readiness?.initialized !== true,
2173
- previewUrl: state.lastDeployedUrl ?? null,
2174
- lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null,
2175
- wranglerPath
2518
+ status: input.dryRun || !input.execute ? "planned" : "reconciled",
2519
+ branch: input.branch,
2520
+ initialize: input.initialize === true,
2521
+ reconcile
2176
2522
  };
2177
2523
  }
2178
- function destroyPreviewIfPresent(tenantRoot, branchName) {
2179
- const previewTarget = createBranchPreviewDeployTarget(branchName);
2180
- const deployConfig = loadCliDeployConfig(tenantRoot);
2181
- const previewState = loadDeployState(tenantRoot, deployConfig, { target: previewTarget });
2182
- if (previewState.readiness?.initialized) {
2183
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
2184
- validateDestroyPrerequisites(tenantRoot, { requireRemote: true });
2185
- destroyCloudflareResources(tenantRoot, { target: previewTarget });
2186
- }
2187
- cleanupDestroyedState(tenantRoot, { target: previewTarget });
2524
+ async function destroyWorkflowBranchPreviewIfPresent(tenantRoot, branchName, context) {
2525
+ return destroyTreeseedBranchPreview({
2526
+ root: tenantRoot,
2527
+ branch: branchName,
2528
+ dryRun: false,
2529
+ execute: true,
2530
+ reason: "close",
2531
+ env: context?.env
2532
+ });
2533
+ }
2534
+ async function destroyTreeseedBranchPreview(input) {
2535
+ const target = { kind: "branch", branchName: input.branch };
2536
+ const graph = compileTreeseedDesiredResourceGraph({ tenantRoot: input.root, target });
2537
+ const selector = {
2538
+ environment: "staging",
2539
+ resourceKind: ["branch-preview", "branch-preview-cleanup"]
2540
+ };
2541
+ const units = compileTreeseedDesiredUnitsFromGraph(graph, selector).map((unit) => unit.unitType === "branch-preview-cleanup" ? { ...unit, spec: { ...unit.spec, reason: input.reason } } : unit);
2542
+ const reconcile = input.dryRun || !input.execute ? await planTreeseedReconciliation({ tenantRoot: input.root, target, env: input.env ?? process.env, units, selector }) : await destroyTreeseedTargetUnits({
2543
+ tenantRoot: input.root,
2544
+ target,
2545
+ env: input.env ?? process.env,
2546
+ units,
2547
+ selector
2548
+ });
2188
2549
  return {
2189
- performed: previewState.readiness?.initialized === true,
2190
- state: previewState
2550
+ status: input.dryRun || !input.execute ? "planned" : "destroyed",
2551
+ branch: input.branch,
2552
+ reason: input.reason,
2553
+ reconcile
2191
2554
  };
2192
2555
  }
2193
2556
  function resolveDestroyConfirmation(context, expected, input) {
@@ -2211,8 +2574,8 @@ function resolveDestroyConfirmation(context, expected, input) {
2211
2574
  function syncCurrentBranchToOrigin(operation, repoDir, branch) {
2212
2575
  try {
2213
2576
  if (remoteBranchExists(repoDir, branch)) {
2214
- run("git", ["pull", "--rebase", "origin", branch], { cwd: repoDir });
2215
- run("git", ["push", "origin", branch], { cwd: repoDir });
2577
+ runGit(["pull", "--rebase", "origin", branch], { cwd: repoDir });
2578
+ runGit(["push", "origin", branch], { cwd: repoDir });
2216
2579
  return {
2217
2580
  remoteBranchExisted: true,
2218
2581
  pulledRebase: true,
@@ -2221,7 +2584,7 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
2221
2584
  conflicts: false
2222
2585
  };
2223
2586
  }
2224
- run("git", ["push", "-u", "origin", branch], { cwd: repoDir });
2587
+ runGit(["push", "-u", "origin", branch], { cwd: repoDir });
2225
2588
  return {
2226
2589
  remoteBranchExisted: false,
2227
2590
  pulledRebase: false,
@@ -2270,10 +2633,10 @@ function checkoutOrCreateSaveBranch(repoDir, branch) {
2270
2633
  return branch;
2271
2634
  }
2272
2635
  if (remoteBranchExists(repoDir, branch)) {
2273
- run("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
2636
+ runGit(["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
2274
2637
  return branch;
2275
2638
  }
2276
- run("git", ["checkout", "-b", branch], { cwd: repoDir });
2639
+ runGit(["checkout", "-b", branch], { cwd: repoDir });
2277
2640
  return branch;
2278
2641
  }
2279
2642
  function runPackageVerifyLocal(pkgDir) {
@@ -2283,8 +2646,8 @@ function branchNeedsSync(repoDir, branch) {
2283
2646
  if (!remoteBranchExists(repoDir, branch)) {
2284
2647
  return true;
2285
2648
  }
2286
- const localHead = run("git", ["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
2287
- const remoteHead = run("git", ["rev-parse", `origin/${branch}`], { cwd: repoDir, capture: true }).trim();
2649
+ const localHead = runGit(["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
2650
+ const remoteHead = runGit(["rev-parse", `origin/${branch}`], { cwd: repoDir, capture: true }).trim();
2288
2651
  return localHead !== remoteHead;
2289
2652
  }
2290
2653
  function savePackageRepo(report, message, branch, shouldVerify) {
@@ -2294,7 +2657,7 @@ function savePackageRepo(report, message, branch, shouldVerify) {
2294
2657
  const needsSync = branchNeedsSync(report.path, branch);
2295
2658
  if (!report.dirty && !needsSync) {
2296
2659
  report.skippedReason = "clean";
2297
- report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
2660
+ report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
2298
2661
  return report;
2299
2662
  }
2300
2663
  if (shouldVerify && report.dirty) {
@@ -2302,11 +2665,11 @@ function savePackageRepo(report, message, branch, shouldVerify) {
2302
2665
  report.verified = true;
2303
2666
  }
2304
2667
  if (report.dirty) {
2305
- run("git", ["add", "-A"], { cwd: report.path });
2306
- run("git", ["commit", "-m", message], { cwd: report.path });
2668
+ runGit(["add", "-A"], { cwd: report.path });
2669
+ runGit(["commit", "-m", message], { cwd: report.path });
2307
2670
  report.committed = true;
2308
2671
  }
2309
- report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
2672
+ report.commitSha = runGit(["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
2310
2673
  const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
2311
2674
  report.pushed = branchSync.pushed === true;
2312
2675
  if (!report.dirty && needsSync) {
@@ -2344,14 +2707,14 @@ function ensureLocalTaskBranch(repoDir, branchName) {
2344
2707
  }
2345
2708
  return true;
2346
2709
  }
2347
- function cleanupTaskBranchReport(report, branchName, message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
2710
+ function cleanupTaskBranchReport(report, branchName, _message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
2348
2711
  if (!ensureLocalTaskBranch(report.path, branchName)) {
2349
2712
  report.skippedReason = "branch-missing";
2350
2713
  return report;
2351
2714
  }
2352
- const deprecatedTag = createDeprecatedTaskTag(report.path, branchName, message);
2353
- report.tagName = deprecatedTag.tagName;
2354
- report.commitSha = deprecatedTag.head;
2715
+ const tag = createDeprecatedTaskTag(report.path, branchName, _message);
2716
+ report.tagName = tag.tagName;
2717
+ report.commitSha = updateHead(report.path);
2355
2718
  report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
2356
2719
  syncBranchWithOrigin(report.path, targetBranch);
2357
2720
  if (deleteBranch) {
@@ -2415,7 +2778,7 @@ function collectReleasePackageSelection(root) {
2415
2778
  };
2416
2779
  }
2417
2780
  function hasStagedChanges(repoDir) {
2418
- return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
2781
+ return runGit(["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
2419
2782
  }
2420
2783
  async function workflowStatus(helpers, input = {}) {
2421
2784
  return withContextEnv(helpers.context.env, async () => {
@@ -2837,7 +3200,7 @@ async function workflowSwitch(helpers, input) {
2837
3200
  const mode = session.mode;
2838
3201
  const repoDir = session.gitRoot;
2839
3202
  const rootRepo = createWorkspaceRootRepoReport(root);
2840
- const packageReports = createWorkspacePackageReports(root);
3203
+ const packageReports = createManagedWorkflowRepoReports(root);
2841
3204
  let previewResult = null;
2842
3205
  const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
2843
3206
  if (executionMode === "plan") {
@@ -2847,6 +3210,14 @@ async function workflowSwitch(helpers, input) {
2847
3210
  report.created = !local && !remote;
2848
3211
  report.resumed = local || remote;
2849
3212
  }
3213
+ const previewPlan = preview ? await reconcileTreeseedBranchPreview({
3214
+ root,
3215
+ branch: branchName,
3216
+ dryRun: true,
3217
+ execute: false,
3218
+ initialize: !branchPreviewInitialized(root, branchName),
3219
+ env: helpers.context.env
3220
+ }) : null;
2850
3221
  return buildWorkflowResult(
2851
3222
  "switch",
2852
3223
  root,
@@ -2864,7 +3235,8 @@ async function workflowSwitch(helpers, input) {
2864
3235
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
2865
3236
  { id: "workspace-link", description: "Apply local workspace links for integrated development" },
2866
3237
  ...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
2867
- ]
3238
+ ],
3239
+ previewPlan
2868
3240
  },
2869
3241
  {
2870
3242
  executionMode,
@@ -2914,8 +3286,8 @@ async function workflowSwitch(helpers, input) {
2914
3286
  rootRepo.resumed = rootSwitch.resumed;
2915
3287
  rootRepo.commitSha = headCommit(repoDir);
2916
3288
  rootRepo.pushed = rootSwitch.created;
2917
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2918
- const report = findReportByName(packageReports, pkg.name);
3289
+ for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
3290
+ const report = findReportByName(packageReports, managedRepo.name);
2919
3291
  if (!report) {
2920
3292
  continue;
2921
3293
  }
@@ -2923,16 +3295,16 @@ async function workflowSwitch(helpers, input) {
2923
3295
  root,
2924
3296
  workflowRun.runId,
2925
3297
  `switch-${report.name}`,
2926
- () => checkoutTaskBranchFromStaging(pkg.dir, branchName, {
3298
+ () => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
2927
3299
  createIfMissing: input.createIfMissing !== false,
2928
3300
  pushIfCreated: false
2929
3301
  })
2930
3302
  );
2931
- report.branch = currentBranch(pkg.dir) || branchName;
3303
+ report.branch = currentBranch(managedRepo.dir) || branchName;
2932
3304
  report.created = packageSwitch.created;
2933
3305
  report.resumed = packageSwitch.resumed;
2934
- report.commitSha = headCommit(pkg.dir);
2935
- report.dirty = hasMeaningfulChanges(pkg.dir);
3306
+ report.commitSha = headCommit(managedRepo.dir);
3307
+ report.dirty = hasMeaningfulChanges(managedRepo.dir);
2936
3308
  }
2937
3309
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
2938
3310
  const stateAfterSwitch = resolveTreeseedWorkflowState(root);
@@ -2941,7 +3313,7 @@ async function workflowSwitch(helpers, input) {
2941
3313
  root,
2942
3314
  workflowRun.runId,
2943
3315
  "preview",
2944
- () => deployBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
3316
+ () => reconcileWorkflowBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
2945
3317
  ) ?? null;
2946
3318
  }
2947
3319
  const state = resolveTreeseedWorkflowState(root);
@@ -2995,32 +3367,401 @@ async function workflowSwitch(helpers, input) {
2995
3367
  toError("switch", error);
2996
3368
  }
2997
3369
  }
2998
- async function workflowDev(helpers, input = {}) {
2999
- try {
3000
- return withContextEnv(helpers.context.env, async () => {
3001
- if (helpers.context.transport === "api") {
3002
- workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
3003
- }
3004
- const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
3005
- const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
3006
- const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
3007
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
3008
- assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
3009
- const args = [packageScriptPath("tenant-dev")];
3010
- if (input.watch) {
3011
- args.push("--watch");
3012
- }
3013
- if (input.port !== void 0) {
3014
- args.push("--port", String(input.port));
3015
- }
3016
- const env = resolveTreeseedLaunchEnvironment({
3017
- tenantRoot,
3018
- scope: "local",
3019
- baseEnv: { ...process.env, ...helpers.context.env ?? {} }
3020
- });
3021
- if (input.background) {
3022
- const child = spawn(process.execPath, args, {
3023
- cwd: tenantRoot,
3370
+ function normalizeUpdateStrategy(strategy) {
3371
+ return strategy === "ff-only" ? "ff-only" : "merge";
3372
+ }
3373
+ function normalizeUpdateSource(source) {
3374
+ const normalized = String(source ?? STAGING_BRANCH).trim();
3375
+ return normalized || STAGING_BRANCH;
3376
+ }
3377
+ function gitOutput(args, cwd, allowFailure = false) {
3378
+ return runTreeseedGit(args, {
3379
+ cwd,
3380
+ mode: classifyTreeseedGitMode(args),
3381
+ allowFailure
3382
+ }).stdout.trim();
3383
+ }
3384
+ function updateHead(repoDir) {
3385
+ return gitOutput(["rev-parse", "HEAD"], repoDir, true) || null;
3386
+ }
3387
+ function updateStatusLines(repoDir) {
3388
+ const output = gitOutput(["status", "--porcelain"], repoDir, true);
3389
+ return output ? output.split(/\r?\n/u).filter(Boolean) : [];
3390
+ }
3391
+ function updateChangedFiles(repoDir) {
3392
+ return updateStatusLines(repoDir).map((line) => line.slice(3).trim()).filter(Boolean);
3393
+ }
3394
+ function updateConflictedFiles(repoDir) {
3395
+ return updateStatusLines(repoDir).filter((line) => {
3396
+ const status = line.slice(0, 2);
3397
+ return status.includes("U") || ["AA", "DD"].includes(status);
3398
+ }).map((line) => line.slice(3).trim()).filter(Boolean);
3399
+ }
3400
+ function sourceBranchExists(repoDir, sourceBranch) {
3401
+ return runTreeseedGit(["ls-remote", "--exit-code", "--heads", "origin", sourceBranch], {
3402
+ cwd: repoDir,
3403
+ mode: "read",
3404
+ allowFailure: true
3405
+ }).status === 0;
3406
+ }
3407
+ function localRemoteRefExists(repoDir, sourceBranch) {
3408
+ return runTreeseedGitOk(["show-ref", "--verify", `refs/remotes/origin/${sourceBranch}`], {
3409
+ cwd: repoDir,
3410
+ mode: "read"
3411
+ });
3412
+ }
3413
+ function updateAheadBehind(repoDir, branch, sourceRef) {
3414
+ if (!localRemoteRefExists(repoDir, sourceRef.replace(/^origin\//u, ""))) {
3415
+ return { ahead: null, behind: null };
3416
+ }
3417
+ const output = gitOutput(["rev-list", "--left-right", "--count", `${branch}...${sourceRef}`], repoDir, true);
3418
+ const [aheadRaw, behindRaw] = output.split(/\s+/u);
3419
+ const ahead = Number.parseInt(aheadRaw ?? "", 10);
3420
+ const behind = Number.parseInt(behindRaw ?? "", 10);
3421
+ return {
3422
+ ahead: Number.isFinite(ahead) ? ahead : null,
3423
+ behind: Number.isFinite(behind) ? behind : null
3424
+ };
3425
+ }
3426
+ function updatePlanChangedFiles(repoDir, sourceRef) {
3427
+ if (!runTreeseedGitOk(["show-ref", "--verify", `refs/remotes/${sourceRef}`], { cwd: repoDir, mode: "read" })) {
3428
+ return [];
3429
+ }
3430
+ const output = gitOutput(["diff", "--name-only", `HEAD...${sourceRef}`], repoDir, true);
3431
+ return output ? output.split(/\r?\n/u).filter(Boolean).slice(0, 50) : [];
3432
+ }
3433
+ function planUpdateRepo(name, repoDir, branch, sourceBranch, strategy) {
3434
+ const sourceRef = `origin/${sourceBranch}`;
3435
+ const blockers = [];
3436
+ if (!sourceBranchExists(repoDir, sourceBranch)) {
3437
+ blockers.push(`origin/${sourceBranch} does not exist`);
3438
+ }
3439
+ const { ahead, behind } = blockers.length === 0 ? updateAheadBehind(repoDir, branch, sourceRef) : { ahead: null, behind: null };
3440
+ const status = blockers.length > 0 ? "blocked" : behind === 0 ? "up-to-date" : strategy === "ff-only" && ahead === 0 ? "fast-forward" : "merge-needed";
3441
+ return {
3442
+ name,
3443
+ path: repoDir,
3444
+ branch,
3445
+ sourceRef,
3446
+ action: blockers.length > 0 ? "blocked" : "planned",
3447
+ beforeHead: updateHead(repoDir),
3448
+ afterHead: null,
3449
+ pushed: false,
3450
+ changedFiles: updatePlanChangedFiles(repoDir, sourceRef),
3451
+ blockers,
3452
+ ahead,
3453
+ behind,
3454
+ status
3455
+ };
3456
+ }
3457
+ function ensureUpdateRepoReady(operation, repo, expectedBranch) {
3458
+ if (repo.detached || !repo.branchName) {
3459
+ workflowError(operation, "validation_failed", `${repo.name} is detached; update requires attached branches.`, {
3460
+ details: { repo }
3461
+ });
3462
+ }
3463
+ if (expectedBranch && repo.branchName !== expectedBranch) {
3464
+ workflowError(operation, "validation_failed", `${repo.name} is on ${repo.branchName}, expected ${expectedBranch}.`, {
3465
+ details: { repo, expectedBranch }
3466
+ });
3467
+ }
3468
+ if (repo.dirty) {
3469
+ workflowError(operation, "validation_failed", `${repo.name} has local changes. Run \`npx trsd save --json "checkpoint before update"\` first.`, {
3470
+ details: { repo }
3471
+ });
3472
+ }
3473
+ if (!repo.hasOriginRemote) {
3474
+ workflowError(operation, "validation_failed", `${repo.name} is missing an origin remote.`, {
3475
+ details: { repo }
3476
+ });
3477
+ }
3478
+ }
3479
+ function formatUpdateConflict(repoName, repoDir, sourceBranch, targetBranch) {
3480
+ const files = updateConflictedFiles(repoDir);
3481
+ const status = updateStatusLines(repoDir);
3482
+ return {
3483
+ message: [
3484
+ `Treeseed update hit a merge conflict in ${repoName}.`,
3485
+ `Repository: ${repoDir}`,
3486
+ `Target branch: ${targetBranch}`,
3487
+ `Source branch: origin/${sourceBranch}`,
3488
+ files.length > 0 ? `Conflicted files:
3489
+ ${files.map((file) => `- ${file}`).join("\n")}` : "Conflicted files: inspect git status.",
3490
+ 'Resolve the conflicts in that repository, then run `npx trsd save --json "resolve update conflict"` or abort manually and rerun `npx trsd update --from staging --json`.'
3491
+ ].join("\n"),
3492
+ files,
3493
+ status
3494
+ };
3495
+ }
3496
+ function mergeUpdateRepo(input) {
3497
+ const sourceRef = `origin/${input.sourceBranch}`;
3498
+ const beforeHead = updateHead(input.repoDir);
3499
+ runTreeseedGit(["fetch", "origin"], { cwd: input.repoDir, mode: "mutate" });
3500
+ if (!sourceBranchExists(input.repoDir, input.sourceBranch)) {
3501
+ return {
3502
+ name: input.name,
3503
+ path: input.repoDir,
3504
+ branch: input.branch,
3505
+ sourceRef,
3506
+ action: "blocked",
3507
+ beforeHead,
3508
+ afterHead: beforeHead,
3509
+ pushed: false,
3510
+ changedFiles: [],
3511
+ blockers: [`origin/${input.sourceBranch} does not exist`]
3512
+ };
3513
+ }
3514
+ const mergeArgs = input.strategy === "ff-only" ? ["merge", "--ff-only", sourceRef] : ["merge", "--no-edit", sourceRef];
3515
+ const merge = runTreeseedGit(mergeArgs, {
3516
+ cwd: input.repoDir,
3517
+ mode: "mutate",
3518
+ allowFailure: true
3519
+ });
3520
+ if (merge.status !== 0) {
3521
+ const conflict = formatUpdateConflict(input.name, input.repoDir, input.sourceBranch, input.branch);
3522
+ throw new TreeseedWorkflowError("update", "merge_conflict", conflict.message, {
3523
+ details: {
3524
+ repo: input.name,
3525
+ path: input.repoDir,
3526
+ files: conflict.files,
3527
+ status: conflict.status,
3528
+ sourceBranch: input.sourceBranch,
3529
+ targetBranch: input.branch
3530
+ },
3531
+ exitCode: 12
3532
+ });
3533
+ }
3534
+ const afterHead = updateHead(input.repoDir);
3535
+ const changed = beforeHead !== afterHead;
3536
+ let pushed = false;
3537
+ if (changed && input.push) {
3538
+ runTreeseedGit(["push", "origin", input.branch], { cwd: input.repoDir, mode: "mutate" });
3539
+ pushed = true;
3540
+ }
3541
+ return {
3542
+ name: input.name,
3543
+ path: input.repoDir,
3544
+ branch: input.branch,
3545
+ sourceRef,
3546
+ action: changed ? input.strategy === "ff-only" ? "fast-forwarded" : "merged" : "up-to-date",
3547
+ beforeHead,
3548
+ afterHead,
3549
+ pushed,
3550
+ changedFiles: [],
3551
+ blockers: []
3552
+ };
3553
+ }
3554
+ function commitRootUpdateIfNeeded(root, branch, push) {
3555
+ const changedFiles = updateChangedFiles(repoRoot(root));
3556
+ if (changedFiles.length === 0) {
3557
+ let pushed2 = false;
3558
+ if (push) {
3559
+ runTreeseedGit(["push", "origin", branch], { cwd: repoRoot(root), mode: "mutate" });
3560
+ pushed2 = true;
3561
+ }
3562
+ return {
3563
+ committed: false,
3564
+ pushed: pushed2,
3565
+ commitSha: updateHead(repoRoot(root)),
3566
+ changedFiles
3567
+ };
3568
+ }
3569
+ runTreeseedGit(["add", "-A"], { cwd: repoRoot(root), mode: "mutate" });
3570
+ runTreeseedGit(["commit", "-m", `chore(workflow): update ${branch} from staging`], { cwd: repoRoot(root), mode: "mutate" });
3571
+ const commitSha = updateHead(repoRoot(root));
3572
+ let pushed = false;
3573
+ if (push) {
3574
+ runTreeseedGit(["push", "origin", branch], { cwd: repoRoot(root), mode: "mutate" });
3575
+ pushed = true;
3576
+ }
3577
+ return {
3578
+ committed: true,
3579
+ pushed,
3580
+ commitSha,
3581
+ changedFiles
3582
+ };
3583
+ }
3584
+ async function workflowUpdate(helpers, input = {}) {
3585
+ try {
3586
+ return await withContextEnv(helpers.context.env, async () => {
3587
+ const tenantRoot = resolveProjectRootOrThrow("update", helpers.cwd());
3588
+ const root = workspaceRoot(tenantRoot);
3589
+ const session = resolveTreeseedWorkflowSession(root);
3590
+ const sourceBranch = normalizeUpdateSource(input.from);
3591
+ const strategy = normalizeUpdateStrategy(input.strategy);
3592
+ const push = input.push !== false;
3593
+ const executionMode = normalizeExecutionMode(input);
3594
+ const branch = session.branchName;
3595
+ if (!branch) {
3596
+ workflowError("update", "validation_failed", "Treeseed update requires an attached current branch.");
3597
+ }
3598
+ if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
3599
+ workflowError("update", "validation_failed", `Treeseed update must run from a task branch, not ${branch}.`, {
3600
+ details: { branch }
3601
+ });
3602
+ }
3603
+ if (sourceBranch === branch) {
3604
+ workflowError("update", "validation_failed", "Treeseed update source branch cannot match the current branch.", {
3605
+ details: { branch, sourceBranch }
3606
+ });
3607
+ }
3608
+ ensureUpdateRepoReady("update", session.rootRepo);
3609
+ for (const repo of session.managedRepos) {
3610
+ ensureUpdateRepoReady("update", repo, branch);
3611
+ }
3612
+ const repoPlans = session.managedRepos.map((repo) => planUpdateRepo(repo.name, repo.path, branch, sourceBranch, strategy));
3613
+ const rootPlan = planUpdateRepo("@treeseed/market", session.gitRoot, branch, sourceBranch, strategy);
3614
+ const blockers = [...repoPlans, rootPlan].flatMap((repo) => repo.blockers.map((blocker) => `${repo.name}: ${blocker}`));
3615
+ if (executionMode === "plan") {
3616
+ return buildWorkflowResult("update", root, {
3617
+ mode: session.mode,
3618
+ branch,
3619
+ sourceBranch,
3620
+ sourceRef: `origin/${sourceBranch}`,
3621
+ strategy,
3622
+ pushed: false,
3623
+ plan: true,
3624
+ repos: repoPlans,
3625
+ rootRepo: rootPlan,
3626
+ conflicts: [],
3627
+ blockers,
3628
+ ...worktreePayload(root, input.worktreeMode)
3629
+ }, {
3630
+ executionMode,
3631
+ includeFinalState: false,
3632
+ nextSteps: createNextSteps([
3633
+ { operation: "update", reason: "Run without --plan to merge staging into the current branch.", input: { from: sourceBranch } }
3634
+ ])
3635
+ });
3636
+ }
3637
+ if (blockers.length > 0) {
3638
+ workflowError("update", "validation_failed", `Treeseed update is blocked:
3639
+ ${blockers.join("\n")}`, {
3640
+ details: { blockers, repos: repoPlans, rootRepo: rootPlan }
3641
+ });
3642
+ }
3643
+ const workflowRun = acquireWorkflowRun(
3644
+ "update",
3645
+ session,
3646
+ { from: sourceBranch, strategy, push, workspaceLinks: input.workspaceLinks ?? "auto" },
3647
+ [
3648
+ { id: "validate-update", description: `Validate update from ${sourceBranch}`, repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
3649
+ ...session.managedRepos.map((repo) => ({
3650
+ id: `update-${repo.name}`,
3651
+ description: `Merge origin/${sourceBranch} into ${repo.name}`,
3652
+ repoName: repo.name,
3653
+ repoPath: repo.path,
3654
+ branch,
3655
+ resumable: true
3656
+ })),
3657
+ { id: "update-root", description: `Merge origin/${sourceBranch} into market`, repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
3658
+ { id: "refresh-root-pointers", description: "Commit updated root pointers if package heads changed", repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true },
3659
+ { id: "restore-workspace-links", description: "Restore local workspace links", repoName: session.rootRepo.name, repoPath: session.rootRepo.path, branch, resumable: true }
3660
+ ],
3661
+ helpers.context
3662
+ );
3663
+ try {
3664
+ await executeJournalStep(root, workflowRun.runId, "validate-update", () => ({
3665
+ branch,
3666
+ sourceBranch,
3667
+ strategy,
3668
+ push
3669
+ }));
3670
+ const repos = [];
3671
+ for (const repo of session.managedRepos) {
3672
+ const result = await executeJournalStep(root, workflowRun.runId, `update-${repo.name}`, () => mergeUpdateRepo({
3673
+ name: repo.name,
3674
+ repoDir: repo.path,
3675
+ branch,
3676
+ sourceBranch,
3677
+ strategy,
3678
+ push
3679
+ }));
3680
+ if (result) repos.push(result);
3681
+ }
3682
+ const rootMerge = await executeJournalStep(root, workflowRun.runId, "update-root", () => mergeUpdateRepo({
3683
+ name: "@treeseed/market",
3684
+ repoDir: session.gitRoot,
3685
+ branch,
3686
+ sourceBranch,
3687
+ strategy,
3688
+ push: false
3689
+ }));
3690
+ const rootCommit = await executeJournalStep(root, workflowRun.runId, "refresh-root-pointers", () => commitRootUpdateIfNeeded(root, branch, push));
3691
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "restore-workspace-links", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
3692
+ const rootRepo = {
3693
+ ...rootMerge,
3694
+ action: rootCommit?.committed ? "committed" : rootMerge.action,
3695
+ commitSha: rootCommit?.commitSha ?? rootMerge.afterHead,
3696
+ pushed: rootCommit?.pushed ?? false,
3697
+ changedFiles: rootCommit?.changedFiles ?? rootMerge.changedFiles
3698
+ };
3699
+ const payload = {
3700
+ mode: session.mode,
3701
+ branch,
3702
+ sourceBranch,
3703
+ sourceRef: `origin/${sourceBranch}`,
3704
+ strategy,
3705
+ pushed: push,
3706
+ plan: false,
3707
+ repos,
3708
+ rootRepo,
3709
+ conflicts: [],
3710
+ workspaceLinks,
3711
+ ...worktreePayload(root, input.worktreeMode)
3712
+ };
3713
+ completeWorkflowRun(root, workflowRun.runId, payload);
3714
+ return buildWorkflowResult("update", root, payload, {
3715
+ runId: workflowRun.runId,
3716
+ summary: `Treeseed update merged ${sourceBranch} into ${branch}.`,
3717
+ includeFinalState: false,
3718
+ nextSteps: createNextSteps([
3719
+ { operation: "save", reason: "Checkpoint any follow-up conflict resolutions or generated pointer changes.", input: { message: "sync with staging" } },
3720
+ { operation: "stage", reason: "Merge the updated task branch into staging when it is ready.", input: { message: "describe the resolution" } }
3721
+ ])
3722
+ });
3723
+ } catch (error) {
3724
+ failWorkflowRun(root, workflowRun.runId, error, {
3725
+ resumable: true,
3726
+ runId: workflowRun.runId,
3727
+ command: "update",
3728
+ message: `Resume the interrupted update for ${branch}.`,
3729
+ recoverCommand: "treeseed recover",
3730
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
3731
+ });
3732
+ throw error;
3733
+ }
3734
+ });
3735
+ } catch (error) {
3736
+ toError("update", error);
3737
+ }
3738
+ }
3739
+ async function workflowDev(helpers, input = {}) {
3740
+ try {
3741
+ return withContextEnv(helpers.context.env, async () => {
3742
+ if (helpers.context.transport === "api") {
3743
+ workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
3744
+ }
3745
+ const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
3746
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
3747
+ const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
3748
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
3749
+ assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
3750
+ const args = [packageScriptPath("tenant-dev")];
3751
+ if (input.watch) {
3752
+ args.push("--watch");
3753
+ }
3754
+ if (input.port !== void 0) {
3755
+ args.push("--port", String(input.port));
3756
+ }
3757
+ const env = resolveTreeseedLaunchEnvironment({
3758
+ tenantRoot,
3759
+ scope: "local",
3760
+ baseEnv: { ...process.env, ...helpers.context.env ?? {} }
3761
+ });
3762
+ if (input.background) {
3763
+ const child = spawn(process.execPath, args, {
3764
+ cwd: tenantRoot,
3024
3765
  env,
3025
3766
  stdio: input.stdio ?? "inherit",
3026
3767
  detached: process.platform !== "win32"
@@ -3073,6 +3814,7 @@ async function workflowSave(helpers, input) {
3073
3814
  return await withContextEnv(helpers.context.env, async () => {
3074
3815
  const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
3075
3816
  const root = workspaceRoot(tenantRoot);
3817
+ ensureTreeseedLocalStateExcluded(root);
3076
3818
  const rootBranch = currentBranch(repoRoot(root)) || null;
3077
3819
  reattachRepairablePackageRepos(root, [rootBranch, STAGING_BRANCH, PRODUCTION_BRANCH].filter((branch2) => Boolean(branch2)), {
3078
3820
  operation: "save",
@@ -3104,7 +3846,7 @@ async function workflowSave(helpers, input) {
3104
3846
  if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
3105
3847
  workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
3106
3848
  }
3107
- const packageReports = createWorkspacePackageReports(root);
3849
+ const packageReports = createManagedWorkflowRepoReports(root);
3108
3850
  const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
3109
3851
  const blockers = [];
3110
3852
  if (executionMode === "plan") {
@@ -3121,7 +3863,7 @@ async function workflowSave(helpers, input) {
3121
3863
  message,
3122
3864
  bump: effectiveInput.bump ?? "patch",
3123
3865
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
3124
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
3866
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
3125
3867
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3126
3868
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3127
3869
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
@@ -3160,8 +3902,9 @@ async function workflowSave(helpers, input) {
3160
3902
  { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
3161
3903
  ...repositoryPlan.plannedSteps,
3162
3904
  { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
3163
- ...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{ id: "hosted-ci", description: `Wait for hosted save workflows on ${branch}` }] : [],
3164
- ...branch === STAGING_BRANCH ? [{ id: "release-candidate", description: `Run ${releaseCandidateMode} release-candidate readiness checks for the saved staging state` }] : [],
3905
+ ...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{ id: "hosted-ci", description: saveHostedEnvironmentForBranch(branch) ? `Reconcile and verify hosted deployments for ${saveHostedEnvironmentForBranch(branch)}` : `Wait for hosted save workflows on ${branch}` }] : [],
3906
+ ...saveLane === "promotion" ? [{ id: "release-proof", description: "Run or reuse authoritative hosted release proof records for exact package refs" }] : [],
3907
+ ...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{ id: "release-candidate", description: `Run ${releaseCandidateMode} release-candidate readiness checks for the saved staging state` }] : [],
3165
3908
  { id: "workspace-link", description: "Restore local workspace links after save" },
3166
3909
  ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
3167
3910
  ]
@@ -3193,7 +3936,7 @@ async function workflowSave(helpers, input) {
3193
3936
  verify: effectiveInput.verify !== false,
3194
3937
  bump: effectiveInput.bump ?? "patch",
3195
3938
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
3196
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
3939
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
3197
3940
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3198
3941
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3199
3942
  verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
@@ -3216,13 +3959,21 @@ async function workflowSave(helpers, input) {
3216
3959
  },
3217
3960
  ...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{
3218
3961
  id: "hosted-ci",
3219
- description: `Wait for hosted save workflows on ${branch}`,
3962
+ description: saveHostedEnvironmentForBranch(branch) ? `Reconcile and verify hosted deployments for ${saveHostedEnvironmentForBranch(branch)}` : `Wait for hosted save workflows on ${branch}`,
3963
+ repoName: rootRepo.name,
3964
+ repoPath: rootRepo.path,
3965
+ branch,
3966
+ resumable: true
3967
+ }] : [],
3968
+ ...saveLane === "promotion" ? [{
3969
+ id: "release-proof",
3970
+ description: "Run authoritative hosted release proof records",
3220
3971
  repoName: rootRepo.name,
3221
3972
  repoPath: rootRepo.path,
3222
3973
  branch,
3223
3974
  resumable: true
3224
3975
  }] : [],
3225
- ...branch === STAGING_BRANCH ? [{
3976
+ ...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{
3226
3977
  id: "release-candidate",
3227
3978
  description: "Run release-candidate readiness checks",
3228
3979
  repoName: rootRepo.name,
@@ -3263,19 +4014,21 @@ async function workflowSave(helpers, input) {
3263
4014
  message,
3264
4015
  bump: effectiveInput.bump ?? "patch",
3265
4016
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
3266
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
4017
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
3267
4018
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3268
4019
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3269
4020
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
3270
4021
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
3271
4022
  workflowRunId: workflowRun.runId,
4023
+ deferPushUntilVerified: branch === STAGING_BRANCH,
3272
4024
  onProgress: (line, stream) => helpers.write(line, stream),
3273
4025
  onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
3274
4026
  const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
3275
4027
  const rootReportForWave = nodes.some((node) => node.id === ".") ? waveRootRepo : null;
4028
+ const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
3276
4029
  const gates = [
3277
4030
  ...gatesForSavedRepositoryReports(root, nonRootReportsForWave),
3278
- ...rootReportForWave ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
4031
+ ...rootReportForWave && !hostedEnvironment ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
3279
4032
  ];
3280
4033
  if (gates.length === 0) {
3281
4034
  return [];
@@ -3283,7 +4036,7 @@ async function workflowSave(helpers, input) {
3283
4036
  const repositoryNames = gates.map((gate) => gate.name).join(", ");
3284
4037
  if (nonRootReportsForWave.length > 0) {
3285
4038
  helpers.write(`[save][workflow] Waiting for hosted repository gates before saving dependents: ${repositoryNames}.`);
3286
- } else if (rootReportForWave) {
4039
+ } else if (rootReportForWave && !hostedEnvironment) {
3287
4040
  helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
3288
4041
  }
3289
4042
  return waitForWorkflowGates("save", gates, "hosted", {
@@ -3300,7 +4053,7 @@ async function workflowSave(helpers, input) {
3300
4053
  const savedPackageReports = saveResult?.repos ?? packageReports;
3301
4054
  const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
3302
4055
  helpers.write("[save][workflow] Repository save phase complete; checking command readiness.");
3303
- const head = savedRootRepo.commitSha ?? run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
4056
+ const head = savedRootRepo.commitSha ?? runGit(["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
3304
4057
  const commitCreated = savedRootRepo.committed === true;
3305
4058
  const branchSync = {
3306
4059
  ...savedRootRepo.publishWait ?? {},
@@ -3317,42 +4070,12 @@ async function workflowSave(helpers, input) {
3317
4070
  }))
3318
4071
  };
3319
4072
  const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
3320
- if (branch === STAGING_BRANCH) {
4073
+ const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
4074
+ if (hostedEnvironment) {
3321
4075
  const workflowGates = saveResult?.workflowGates ?? [];
3322
- if (effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
3323
- return { workflowGates };
3324
- }
3325
- helpers.write("[save][workflow] Dispatching hosted market deploy gate for deployed resource verification.");
3326
- const repository = resolveGitHubRepositorySlug(savedRootRepo.path);
3327
- await dispatchGitHubWorkflowRun(repository, {
3328
- workflow: "deploy.yml",
3329
- branch,
3330
- inputs: {
3331
- environment: "staging",
3332
- action_kind: "deploy_web"
3333
- }
3334
- });
3335
- await sleep(5e3);
3336
- helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
3337
- const dispatchedGates = await waitForWorkflowGates("save", [
3338
- hostedDeployGate({
3339
- name: savedRootRepo.name,
3340
- repoPath: savedRootRepo.path,
3341
- repository,
3342
- workflow: "deploy.yml",
3343
- branch,
3344
- headSha: savedRootRepo.commitSha
3345
- })
3346
- ], "hosted", {
3347
- root,
3348
- runId: workflowRun.runId,
3349
- onProgress: (line, stream) => helpers.write(line, stream)
3350
- });
3351
4076
  return {
3352
- workflowGates: [
3353
- ...workflowGates.filter((gate) => !(gate.repository === repository && gate.workflow === "deploy.yml")),
3354
- ...dispatchedGates
3355
- ]
4077
+ workflowGates,
4078
+ hostedReconcile: await reconcileSaveHostedEnvironment(root, hostedEnvironment, helpers, workflowRun.runId)
3356
4079
  };
3357
4080
  }
3358
4081
  helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
@@ -3371,21 +4094,44 @@ async function workflowSave(helpers, input) {
3371
4094
  branch,
3372
4095
  headSha: savedRootRepo.commitSha
3373
4096
  })] : [],
3374
- ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
3375
- name: repo.name,
3376
- repoPath: repo.path,
3377
- workflow: "verify.yml",
3378
- branch: String(repo.branch),
3379
- headSha: String(repo.commitSha)
3380
- }))
4097
+ ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => {
4098
+ const workflow = hostedWorkflowForSavedRepository(root, repo);
4099
+ const gate = {
4100
+ name: repo.name,
4101
+ repoPath: repo.path,
4102
+ workflow,
4103
+ branch: String(repo.branch),
4104
+ headSha: String(repo.commitSha)
4105
+ };
4106
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
4107
+ })
3381
4108
  ], "hosted", {
3382
4109
  root,
3383
4110
  runId: workflowRun.runId,
3384
4111
  onProgress: (line, stream) => helpers.write(line, stream)
3385
4112
  }).then((workflowGates) => ({ workflowGates }));
3386
4113
  }) : { workflowGates: [] };
3387
- const releaseCandidate = branch === STAGING_BRANCH ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
3388
- helpers.write(`[save][workflow] Running staging release-candidate readiness checks (${releaseCandidateMode}).`);
4114
+ const releaseProof = saveLane === "promotion" && process.env.TREESEED_STAGE_WAIT_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-proof", async () => {
4115
+ helpers.write("[save][workflow] Running authoritative hosted release proof.");
4116
+ const proof = await runTreeseedProof({
4117
+ root,
4118
+ target: scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
4119
+ driver: "github-hosted",
4120
+ write: (line, stream) => helpers.write(line, stream)
4121
+ });
4122
+ if (proof.failures.length > 0) {
4123
+ const first = proof.failures[0];
4124
+ workflowError("save", "validation_failed", [
4125
+ "Treeseed promotion proof failed.",
4126
+ `- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
4127
+ first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null,
4128
+ "Hosted GitHub workflow proof is authoritative; local action simulation is advisory."
4129
+ ].filter(Boolean).join("\n"), { details: { proof } });
4130
+ }
4131
+ return proof;
4132
+ }) : saveLane === "promotion" ? (skipJournalStep(root, workflowRun.runId, "release-proof", { skippedReason: "disabled" }), { skipped: true, reason: "disabled" }) : null;
4133
+ const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
4134
+ helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
3389
4135
  const releaseSession = resolveTreeseedWorkflowSession(root);
3390
4136
  const stagingReleasePlan = buildReleasePlanSnapshot({
3391
4137
  root,
@@ -3396,19 +4142,23 @@ async function workflowSave(helpers, input) {
3396
4142
  rootRepo: savedRootRepo,
3397
4143
  blockers: []
3398
4144
  });
3399
- return runReleaseCandidateForPlan("save", root, stagingReleasePlan, { mode: releaseCandidateMode });
4145
+ return runReleaseCandidateProofForPlan("save", root, stagingReleasePlan, {
4146
+ mode: releaseCandidateMode,
4147
+ lane: saveLane,
4148
+ write: (line, stream) => helpers.write(line, stream)
4149
+ });
3400
4150
  }) : null;
3401
4151
  let previewAction = { status: "skipped" };
3402
4152
  if (beforeState.branchRole === "feature" && branch) {
3403
4153
  if (effectiveInput.preview === true) {
3404
4154
  previewAction = {
3405
4155
  status: previewInitialized ? "refreshed" : "created",
3406
- details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
4156
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
3407
4157
  };
3408
4158
  } else if (effectiveInput.refreshPreview !== false && previewInitialized) {
3409
4159
  previewAction = {
3410
4160
  status: "refreshed",
3411
- details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
4161
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => reconcileWorkflowBranchPreview(root, branch, helpers.context, { initialize: false }))
3412
4162
  };
3413
4163
  }
3414
4164
  }
@@ -3417,7 +4167,7 @@ async function workflowSave(helpers, input) {
3417
4167
  selected: savedPackageReports.map((report) => report.name)
3418
4168
  }
3419
4169
  });
3420
- const hostingAudit = await runWorkflowHostedResourceVerification(
4170
+ const hostingAudit = await workflowHostedVerificationGateRequired(
3421
4171
  "save",
3422
4172
  root,
3423
4173
  helpers,
@@ -3458,7 +4208,9 @@ async function workflowSave(helpers, input) {
3458
4208
  releaseCandidateMode,
3459
4209
  applicationSelection,
3460
4210
  workflowGates: saveWorkflowGates?.workflowGates ?? [],
4211
+ hostedReconcile: saveWorkflowGates?.hostedReconcile ?? null,
3461
4212
  releaseCandidate,
4213
+ releaseProof,
3462
4214
  hostingAudit,
3463
4215
  ...worktreePayload(root, effectiveInput.worktreeMode)
3464
4216
  };
@@ -3540,17 +4292,17 @@ async function workflowClose(helpers, input) {
3540
4292
  failure: planAutoResumeRun.failure
3541
4293
  } : null,
3542
4294
  ...worktreePayload(root, effectiveInput.worktreeMode),
3543
- autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
3544
- repos: createWorkspacePackageReports(root),
4295
+ autoSaveRequired: session.rootRepo.dirty || session.managedRepos.some((repo) => repo.dirty),
4296
+ repos: createManagedWorkflowRepoReports(root),
3545
4297
  rootRepo: createWorkspaceRootRepoReport(root),
3546
4298
  blockers,
3547
4299
  plannedSteps: [
3548
4300
  { id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
3549
4301
  { id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
3550
4302
  { id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
3551
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3552
- id: `cleanup-${pkg.name}`,
3553
- description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
4303
+ ...checkedOutManagedWorkflowRepos(root).map((repo) => ({
4304
+ id: `cleanup-${repo.name}`,
4305
+ description: `Archive and delete ${branchName ?? "(current task)"} in ${repo.name}`
3554
4306
  })),
3555
4307
  { id: "workspace-link", description: "Restore local workspace links on the final branch" },
3556
4308
  ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
@@ -3572,6 +4324,7 @@ async function workflowClose(helpers, input) {
3572
4324
  const featureBranch = assertFeatureBranch(root);
3573
4325
  const mode = activeSession.mode;
3574
4326
  const repoDir = activeSession.gitRoot;
4327
+ const managedWorktreeForClose = isManagedWorkflowWorktree(root);
3575
4328
  assertSessionBranchSafety("close", activeSession);
3576
4329
  if (mode === "recursive-workspace") {
3577
4330
  assertWorkspaceClean(root);
@@ -3579,7 +4332,7 @@ async function workflowClose(helpers, input) {
3579
4332
  assertCleanWorktree(root);
3580
4333
  }
3581
4334
  const rootRepo = createWorkspaceRootRepoReport(root);
3582
- const packageReports = createWorkspacePackageReports(root);
4335
+ const packageReports = createManagedWorkflowRepoReports(root);
3583
4336
  const workflowRun = acquireWorkflowRun(
3584
4337
  "close",
3585
4338
  activeSession,
@@ -3617,30 +4370,34 @@ async function workflowClose(helpers, input) {
3617
4370
  }
3618
4371
  try {
3619
4372
  await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3620
- const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
4373
+ const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyWorkflowBranchPreviewIfPresent(root, featureBranch, helpers.context));
3621
4374
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
3622
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
4375
+ const head = updateHead(repoDir);
4376
+ const tag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
3623
4377
  const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
3624
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
3625
- if (effectiveInput.deleteBranch !== false) {
4378
+ if (!managedWorktreeForClose) {
4379
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
4380
+ }
4381
+ if (effectiveInput.deleteBranch !== false && !managedWorktreeForClose) {
3626
4382
  deleteLocalBranch(repoDir, featureBranch);
3627
4383
  }
3628
4384
  return {
3629
- deprecatedTag,
4385
+ head,
4386
+ tagName: tag.tagName,
3630
4387
  deletedRemote,
3631
- deletedLocal: effectiveInput.deleteBranch !== false,
3632
- branch: currentBranch(repoDir) || STAGING_BRANCH,
4388
+ deletedLocal: effectiveInput.deleteBranch !== false && !managedWorktreeForClose,
4389
+ branch: managedWorktreeForClose ? featureBranch : currentBranch(repoDir) || STAGING_BRANCH,
3633
4390
  dirty: hasMeaningfulChanges(repoDir)
3634
4391
  };
3635
4392
  });
3636
- rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? null);
3637
- rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
4393
+ rootRepo.tagName = typeof rootCleanup?.tagName === "string" ? rootCleanup.tagName : null;
4394
+ rootRepo.commitSha = String(rootCleanup?.head ?? rootRepo.commitSha ?? "");
3638
4395
  rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
3639
4396
  rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
3640
4397
  rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
3641
4398
  rootRepo.dirty = rootCleanup?.dirty === true;
3642
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3643
- const report = findReportByName(packageReports, pkg.name);
4399
+ for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
4400
+ const report = findReportByName(packageReports, managedRepo.name);
3644
4401
  if (!report) {
3645
4402
  continue;
3646
4403
  }
@@ -3651,16 +4408,21 @@ async function workflowClose(helpers, input) {
3651
4408
  Object.assign(report, cleanup);
3652
4409
  }
3653
4410
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3654
- const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
4411
+ const finalBranch = managedWorktreeForClose ? STAGING_BRANCH : currentBranch(repoDir) || STAGING_BRANCH;
3655
4412
  const managedWorktree = managedWorkflowWorktreeMetadata(root);
3656
- const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
4413
+ const worktreeCleanup = managedWorktreeForClose ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root, {
4414
+ deleteBranch: effectiveInput.deleteBranch !== false
4415
+ })) : { removed: false, reason: "not-managed" };
4416
+ if (worktreeCleanup.deletedLocalBranch === true) {
4417
+ rootRepo.deletedLocal = true;
4418
+ rootRepo.branch = STAGING_BRANCH;
4419
+ }
3657
4420
  const payload = {
3658
4421
  mode,
3659
4422
  branchName: featureBranch,
3660
4423
  message,
3661
4424
  autoSaved: autoSave.performed,
3662
4425
  autoSaveResult: autoSave.save,
3663
- deprecatedTag: rootCleanup?.deprecatedTag ?? null,
3664
4426
  repos: packageReports,
3665
4427
  rootRepo,
3666
4428
  previewCleanup,
@@ -3681,6 +4443,7 @@ async function workflowClose(helpers, input) {
3681
4443
  payload,
3682
4444
  {
3683
4445
  runId: workflowRun.runId,
4446
+ includeFinalState: !managedWorktreeForClose,
3684
4447
  nextSteps: createNextSteps([
3685
4448
  { operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
3686
4449
  ])
@@ -3703,371 +4466,460 @@ async function workflowClose(helpers, input) {
3703
4466
  toError("close", error);
3704
4467
  }
3705
4468
  }
4469
+ function normalizeStageVerifyMode(value) {
4470
+ return value === "local" || value === "none" ? value : "action";
4471
+ }
4472
+ function normalizeStageCiMode(input) {
4473
+ if (input.ciMode === "hosted" || input.waitForStaging === true) return "hosted";
4474
+ return "off";
4475
+ }
4476
+ function normalizeStageCleanupMode(input) {
4477
+ if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
4478
+ return "manual";
4479
+ }
4480
+ function stageCandidateManifestPath(root, runId) {
4481
+ return {
4482
+ latest: resolve(root, ".treeseed", "workflow", "stage-candidates", "latest.json"),
4483
+ run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
4484
+ };
4485
+ }
4486
+ function writeStageCandidateManifest(root, runId, manifest) {
4487
+ const paths = stageCandidateManifestPath(root, runId);
4488
+ for (const filePath of [paths.latest, paths.run]) {
4489
+ mkdirSync(dirname(filePath), { recursive: true });
4490
+ writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
4491
+ `, "utf8");
4492
+ }
4493
+ return manifest;
4494
+ }
4495
+ function checkedOutStagePackageRepos(root) {
4496
+ return checkedOutManagedWorkflowRepos(root).filter((repo) => repo.kind === "package");
4497
+ }
4498
+ function buildStagePromotionPlan(root, branchName, input) {
4499
+ const gitRoot = repoRoot(root);
4500
+ const repos = [
4501
+ ...checkedOutStagePackageRepos(root).map((repo) => ({
4502
+ name: repo.name,
4503
+ path: repo.dir,
4504
+ kind: "managed",
4505
+ sourceBranch: branchName,
4506
+ targetBranch: STAGING_BRANCH,
4507
+ remoteSourceExists: remoteBranchExists(repo.dir, branchName),
4508
+ beforeHead: branchExists(repo.dir, branchName) ? headCommit(repo.dir, branchName) : null,
4509
+ stagingHeadBefore: remoteBranchExists(repo.dir, STAGING_BRANCH) ? remoteHeadCommit(repo.dir, STAGING_BRANCH) : null
4510
+ })),
4511
+ {
4512
+ name: "@treeseed/market",
4513
+ path: gitRoot,
4514
+ kind: "root",
4515
+ sourceBranch: branchName,
4516
+ targetBranch: STAGING_BRANCH,
4517
+ remoteSourceExists: remoteBranchExists(gitRoot, branchName),
4518
+ beforeHead: branchExists(gitRoot, branchName) ? headCommit(gitRoot, branchName) : null,
4519
+ stagingHeadBefore: remoteBranchExists(gitRoot, STAGING_BRANCH) ? remoteHeadCommit(gitRoot, STAGING_BRANCH) : null
4520
+ }
4521
+ ];
4522
+ return {
4523
+ schemaVersion: 1,
4524
+ branchName,
4525
+ targetBranch: STAGING_BRANCH,
4526
+ updateFrom: input.updateFrom,
4527
+ verifyMode: input.verifyMode,
4528
+ ciMode: input.ciMode,
4529
+ cleanupMode: input.cleanupMode,
4530
+ repos,
4531
+ phases: [
4532
+ "preflight",
4533
+ "merge-staging-down",
4534
+ "save-integrated-feature",
4535
+ "verify-integrated-feature",
4536
+ "promote-to-staging",
4537
+ "verify-staging-refs",
4538
+ "workspace-link-restore",
4539
+ "cleanup-source"
4540
+ ]
4541
+ };
4542
+ }
4543
+ function stagePreflightBlockers(root, branchName, plan) {
4544
+ const blockers = [];
4545
+ if (!branchName || branchName === STAGING_BRANCH || branchName === PRODUCTION_BRANCH) {
4546
+ blockers.push(`stage requires a feature branch; current branch is ${branchName || "(none)"}.`);
4547
+ }
4548
+ for (const repo of plan.repos) {
4549
+ const branch = currentBranch(repo.path) || null;
4550
+ if (hasMeaningfulChanges(repo.path)) {
4551
+ blockers.push(`${repo.name} has uncommitted changes.`);
4552
+ }
4553
+ if (branch !== branchName && repo.kind === "managed" && repo.remoteSourceExists) {
4554
+ blockers.push(`${repo.name} is on ${branch ?? "(detached)"} instead of ${branchName}.`);
4555
+ }
4556
+ if (repo.kind === "root" && branch !== branchName) {
4557
+ blockers.push(`${repo.name} is on ${branch ?? "(detached)"} instead of ${branchName}.`);
4558
+ }
4559
+ try {
4560
+ originRemoteUrl(repo.path);
4561
+ } catch {
4562
+ blockers.push(`${repo.name} has no readable origin remote.`);
4563
+ }
4564
+ if (repo.kind === "root" && !repo.remoteSourceExists) {
4565
+ blockers.push(`${repo.name} feature branch ${branchName} has not been pushed to origin.`);
4566
+ }
4567
+ if (repo.kind === "managed" && branchExists(repo.path, branchName) && repo.remoteSourceExists) {
4568
+ const localHead = headCommit(repo.path, branchName);
4569
+ const remoteHead = remoteHeadCommit(repo.path, branchName);
4570
+ if (localHead !== remoteHead) {
4571
+ blockers.push(`${repo.name} local ${branchName} (${localHead.slice(0, 12)}) does not match origin/${branchName} (${remoteHead.slice(0, 12)}). Run save first.`);
4572
+ }
4573
+ }
4574
+ }
4575
+ return blockers;
4576
+ }
4577
+ function stageConflictError(message, details) {
4578
+ return new TreeseedWorkflowError("stage", "merge_conflict", message, {
4579
+ details,
4580
+ exitCode: 12
4581
+ });
4582
+ }
4583
+ function createStageCandidateManifest(root, runId, branchName, plan, verification) {
4584
+ const gitRoot = repoRoot(root);
4585
+ const packageRepos = plan.repos.filter((repo) => repo.kind === "managed" && repo.remoteSourceExists);
4586
+ return {
4587
+ schemaVersion: 1,
4588
+ kind: "treeseed.stage-candidate",
4589
+ runId,
4590
+ branchName,
4591
+ targetBranch: STAGING_BRANCH,
4592
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4593
+ root: {
4594
+ repo: "@treeseed/market",
4595
+ commit: headCommit(gitRoot),
4596
+ verified: verification.status === "passed" || verification.status === "skipped"
4597
+ },
4598
+ packages: packageRepos.map((repo) => ({
4599
+ name: repo.name,
4600
+ path: repo.path,
4601
+ commit: headCommit(repo.path),
4602
+ remote: (() => {
4603
+ try {
4604
+ return originRemoteUrl(repo.path);
4605
+ } catch {
4606
+ return null;
4607
+ }
4608
+ })(),
4609
+ verified: verification.status === "passed" || verification.status === "skipped"
4610
+ })),
4611
+ verification,
4612
+ stagingHeadsBefore: Object.fromEntries(plan.repos.map((repo) => [repo.name, repo.stagingHeadBefore]))
4613
+ };
4614
+ }
4615
+ function cleanupStageSourceBranches(root, branchName, manifest) {
4616
+ const results = [];
4617
+ for (const repo of checkedOutStagePackageRepos(root)) {
4618
+ const manifestRepo = manifest.packages.find((entry) => entry.name === repo.name);
4619
+ if (!manifestRepo) continue;
4620
+ const remoteDeleted = deleteRemoteBranch(repo.dir, branchName);
4621
+ if ((currentBranch(repo.dir) || null) === branchName) {
4622
+ syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
4623
+ }
4624
+ const localExists = branchExists(repo.dir, branchName);
4625
+ if (localExists && (currentBranch(repo.dir) || null) !== branchName) {
4626
+ deleteLocalBranch(repo.dir, branchName);
4627
+ }
4628
+ results.push({
4629
+ name: repo.name,
4630
+ path: repo.dir,
4631
+ remoteDeleted,
4632
+ localDeleted: localExists && !branchExists(repo.dir, branchName)
4633
+ });
4634
+ }
4635
+ const gitRoot = repoRoot(root);
4636
+ const rootRemoteDeleted = deleteRemoteBranch(gitRoot, branchName);
4637
+ const managedWorktree = managedWorkflowWorktreeMetadata(root);
4638
+ const worktreeCleanup = managedWorktree ? removeManagedWorkflowWorktree(root, { deleteBranch: false }) : { removed: false, reason: "not-managed" };
4639
+ const branchDeletionRoot = managedWorktree?.primaryRoot ? repoRoot(managedWorktree.primaryRoot) : gitRoot;
4640
+ if (!managedWorktree && (currentBranch(gitRoot) || null) === branchName) {
4641
+ syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
4642
+ }
4643
+ const rootLocalExists = branchExists(branchDeletionRoot, branchName);
4644
+ if (rootLocalExists && (currentBranch(branchDeletionRoot) || null) !== branchName) {
4645
+ deleteLocalBranch(branchDeletionRoot, branchName);
4646
+ }
4647
+ results.push({
4648
+ name: "@treeseed/market",
4649
+ path: branchDeletionRoot,
4650
+ remoteDeleted: rootRemoteDeleted,
4651
+ localDeleted: rootLocalExists && !branchExists(branchDeletionRoot, branchName)
4652
+ });
4653
+ return {
4654
+ status: "completed",
4655
+ repos: results,
4656
+ worktreeCleanup
4657
+ };
4658
+ }
3706
4659
  async function workflowStage(helpers, input) {
3707
4660
  try {
3708
4661
  return await withContextEnv(helpers.context.env, async () => {
3709
4662
  const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
3710
4663
  const root = workspaceRoot(tenantRoot);
3711
4664
  const executionMode = normalizeExecutionMode(input);
3712
- const initialSession = resolveTreeseedWorkflowSession(root);
4665
+ const session = resolveTreeseedWorkflowSession(root);
3713
4666
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
3714
- const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
3715
- const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
4667
+ const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
4668
+ const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
4669
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
3716
4670
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
3717
4671
  const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
3718
- const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
3719
- const waitForStaging = effectiveInput.verifyDeployedResources === true || effectiveInput.waitForStaging !== false;
3720
- if (executionMode === "plan") {
3721
- const blockers = [];
3722
- if (initialSession.branchRole !== "feature") {
3723
- blockers.push("Stage only applies to task branches.");
3724
- }
3725
- try {
3726
- validateStagingWorkflowContracts(root);
3727
- } catch (error) {
3728
- blockers.push(error instanceof Error ? error.message : String(error));
3729
- }
3730
- const applicationSelection = selectWorkflowApplications(root, { packageSelection: initialSession.packageSelection });
3731
- const readiness = collectTreeseedDeploymentReadiness({
3732
- tenantRoot: root,
3733
- environment: "staging",
3734
- appId: singleSelectedWorkflowAppId(applicationSelection)
3735
- });
3736
- blockers.push(...readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
3737
- return buildWorkflowResult(
3738
- "stage",
3739
- root,
3740
- {
3741
- mode: initialSession.mode,
3742
- branchName: initialSession.branchName,
3743
- mergeTarget: STAGING_BRANCH,
3744
- mergeStrategy: "squash",
3745
- message,
3746
- ciMode,
3747
- applicationSelection,
3748
- autoResumeCandidate: planAutoResumeRun ? {
3749
- runId: planAutoResumeRun.runId,
3750
- branch: planAutoResumeRun.session.branchName,
3751
- failure: planAutoResumeRun.failure
3752
- } : null,
3753
- ...worktreePayload(root, effectiveInput.worktreeMode),
3754
- autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
3755
- blockers,
3756
- readiness,
3757
- rootRepo: createWorkspaceRootRepoReport(root),
3758
- repos: createWorkspacePackageReports(root),
3759
- plannedSteps: [
3760
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3761
- id: `merge-${pkg.name}`,
3762
- description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
3763
- })),
3764
- { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
3765
- { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
3766
- { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
3767
- { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
3768
- { id: "release-candidate", description: "Run release-candidate readiness checks for the exact staging state" },
3769
- { id: "preview-cleanup", description: "Destroy preview resources" },
3770
- { id: "cleanup-root", description: "Archive and delete the task branch from market" },
3771
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3772
- id: `cleanup-${pkg.name}`,
3773
- description: `Archive and delete the task branch from ${pkg.name}`
3774
- })),
3775
- { id: "workspace-link", description: "Restore local workspace links on staging" }
3776
- ]
3777
- },
3778
- {
3779
- executionMode,
3780
- nextSteps: createNextSteps([
3781
- { operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
3782
- ])
3783
- }
3784
- );
4672
+ if (effectiveInput.verifyDeployedResources === true) {
4673
+ workflowError("stage", "validation_failed", "Stage no longer verifies deployed resources. Promote refs with stage, then run staging release/hosting verification separately.");
3785
4674
  }
3786
- const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
3787
- message,
3788
- autoSave: effectiveInput.autoSave
3789
- });
3790
- const session = resolveTreeseedWorkflowSession(root);
3791
- const featureBranch = assertFeatureBranch(root);
3792
- const mode = session.mode;
3793
- assertSessionBranchSafety("stage", session);
3794
- if (mode === "recursive-workspace") {
3795
- assertWorkspaceClean(root);
3796
- } else {
3797
- assertCleanWorktree(root);
4675
+ const verifyMode = normalizeStageVerifyMode(effectiveInput.verifyMode);
4676
+ const ciMode = normalizeStageCiMode(effectiveInput);
4677
+ const cleanupMode = normalizeStageCleanupMode(effectiveInput);
4678
+ const updateFrom = effectiveInput.updateFrom ?? STAGING_BRANCH;
4679
+ if (updateFrom !== STAGING_BRANCH) {
4680
+ workflowError("stage", "validation_failed", `Stage currently supports only --update-from ${STAGING_BRANCH}. Received ${updateFrom}.`);
3798
4681
  }
3799
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3800
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3801
- validateStagingWorkflowContracts(root);
3802
- runWorkspaceSavePreflight({ cwd: root });
3803
- const stagingReadiness = collectTreeseedDeploymentReadiness({ tenantRoot: root, environment: "staging" });
3804
- if (!stagingReadiness.ok) {
3805
- workflowError(
3806
- "stage",
3807
- "validation_failed",
3808
- `Deployment readiness failed for staging:
3809
- ${stagingReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`).join("\n")}`,
3810
- { details: { readiness: stagingReadiness } }
3811
- );
4682
+ const applicationSelection = selectWorkflowApplications(root, { packageSelection: session.packageSelection });
4683
+ const featureBranch = executionMode === "execute" ? assertFeatureBranch(root) : session.branchName ?? "";
4684
+ let plan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
4685
+ let blockers = stagePreflightBlockers(root, featureBranch, plan);
4686
+ const basePayload = {
4687
+ mode: "stage-promotion",
4688
+ branchName: featureBranch,
4689
+ branchRole: session.branchRole,
4690
+ mergeTarget: STAGING_BRANCH,
4691
+ mergeStrategy: "merge-staging-down-then-exact-sha",
4692
+ message,
4693
+ verifyMode,
4694
+ ciMode,
4695
+ cleanupMode,
4696
+ updateFrom,
4697
+ waitForStaging: ciMode === "hosted",
4698
+ applicationSelection,
4699
+ plan,
4700
+ phases: plan.phases,
4701
+ blockers,
4702
+ autoResumeCandidate: planAutoResumeRun ? {
4703
+ runId: planAutoResumeRun.runId,
4704
+ branch: planAutoResumeRun.session.branchName,
4705
+ failure: planAutoResumeRun.failure
4706
+ } : null,
4707
+ legacyMutationPathDisabled: true,
4708
+ ...worktreePayload(root, effectiveInput.worktreeMode)
4709
+ };
4710
+ if (executionMode === "plan") {
4711
+ return buildWorkflowResult("stage", root, basePayload, {
4712
+ executionMode,
4713
+ summary: blockers.length > 0 ? "Treeseed stage plan blocked." : "Treeseed stage promotion plan ready.",
4714
+ includeFinalState: false,
4715
+ nextSteps: createNextSteps([
4716
+ blockers.length > 0 ? { operation: "status", reason: "Resolve blockers before staging." } : { operation: "stage", reason: "Promote the verified feature branch to staging.", input: { message } }
4717
+ ])
4718
+ });
3812
4719
  }
3813
- const repoDir = session.gitRoot;
3814
- const rootRepo = createWorkspaceRootRepoReport(root);
3815
- const packageReports = createWorkspacePackageReports(root);
3816
- const workflowRun = acquireWorkflowRun(
3817
- "stage",
3818
- session,
3819
- {
4720
+ if (effectiveInput.autoSave === true) {
4721
+ await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
3820
4722
  message,
3821
- waitForStaging,
3822
- deletePreview: effectiveInput.deletePreview !== false,
3823
- deleteBranch: effectiveInput.deleteBranch !== false,
3824
- ciMode,
3825
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
3826
- verifyDeployedResources: effectiveInput.verifyDeployedResources === true
3827
- },
3828
- [
3829
- { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3830
- ...packageReports.map((report) => ({
3831
- id: `merge-${report.name}`,
3832
- description: `Merge ${featureBranch} into ${report.name} staging`,
3833
- repoName: report.name,
3834
- repoPath: report.path,
3835
- branch: featureBranch,
3836
- resumable: true
3837
- })),
3838
- { id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3839
- { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3840
- { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3841
- { id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3842
- { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3843
- ...packageReports.map((report) => ({
3844
- id: `cleanup-${report.name}`,
3845
- description: `Archive ${featureBranch} in ${report.name}`,
3846
- repoName: report.name,
3847
- repoPath: report.path,
3848
- branch: featureBranch,
3849
- resumable: true
3850
- })),
3851
- { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3852
- ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
3853
- ],
3854
- autoResumeRun ? {
3855
- ...helpers.context,
3856
- workflow: {
3857
- ...helpers.context.workflow ?? {},
3858
- resumeRunId: autoResumeRun.runId
3859
- }
3860
- } : helpers.context
3861
- );
3862
- if (autoResumeRun) {
3863
- helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
4723
+ autoSave: true,
4724
+ verify: false,
4725
+ preview: false
4726
+ });
4727
+ plan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
4728
+ blockers = stagePreflightBlockers(root, featureBranch, plan);
4729
+ }
4730
+ if (blockers.length > 0) {
4731
+ workflowError("stage", "validation_failed", `stage is blocked:
4732
+ ${blockers.map((entry) => `- ${entry}`).join("\n")}`, {
4733
+ details: { blockers, plan }
4734
+ });
3864
4735
  }
4736
+ const workflowRun = acquireWorkflowRun("stage", resolveTreeseedWorkflowSession(root), {
4737
+ ...effectiveInput,
4738
+ verifyMode,
4739
+ ciMode,
4740
+ cleanupMode,
4741
+ updateFrom
4742
+ }, [
4743
+ { id: "preflight", description: "Validate clean feature branch before staging", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4744
+ { id: "merge-staging-down", description: "Merge staging into feature branches", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4745
+ { id: "save-integrated-feature", description: "Save integrated feature state after staging merge-down", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4746
+ { id: "verify-integrated-feature", description: "Run local proof before staging mutation", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4747
+ { id: "write-stage-candidate", description: "Write exact stage candidate manifest", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4748
+ { id: "promote-to-staging", description: "Promote exact verified refs to staging", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true },
4749
+ { id: "verify-staging-refs", description: "Verify remote staging refs match promoted commits", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
4750
+ { id: "hosted-ci", description: "Wait for hosted staging CI when explicitly requested", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
4751
+ { id: "workspace-link-restore", description: "Restore local workspace links after stage", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: STAGING_BRANCH, resumable: true },
4752
+ { id: "cleanup-source", description: "Clean up source branches and worktree after successful promotion", repoName: "@treeseed/market", repoPath: repoRoot(root), branch: featureBranch, resumable: true }
4753
+ ], helpers.context);
3865
4754
  try {
3866
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3867
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3868
- const report = findReportByName(packageReports, pkg.name);
3869
- if (!report) {
3870
- continue;
3871
- }
3872
- if (!ensureLocalTaskBranch(pkg.dir, featureBranch)) {
3873
- report.skippedReason = "branch-missing";
3874
- skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
3875
- continue;
4755
+ await executeJournalStep(root, workflowRun.runId, "preflight", () => {
4756
+ const currentPlan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
4757
+ const currentBlockers = stagePreflightBlockers(root, featureBranch, currentPlan);
4758
+ if (currentBlockers.length > 0) {
4759
+ workflowError("stage", "validation_failed", `stage is blocked:
4760
+ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
4761
+ details: { blockers: currentBlockers, plan: currentPlan }
4762
+ });
3876
4763
  }
4764
+ return { status: "passed", checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
4765
+ });
4766
+ const mergeDown = await executeJournalStep(root, workflowRun.runId, "merge-staging-down", () => {
4767
+ const results = [];
3877
4768
  try {
3878
- const mergeResult = await executeJournalStep(root, workflowRun.runId, `merge-${report.name}`, () => squashMergeBranchIntoStaging(pkg.dir, featureBranch, message, { pushTarget: true }));
3879
- report.merged = mergeResult.committed;
3880
- report.committed = mergeResult.committed;
3881
- report.pushed = mergeResult.pushed;
3882
- report.commitSha = mergeResult.commitSha;
3883
- report.branch = STAGING_BRANCH;
3884
- checkoutBranch(pkg.dir, featureBranch);
3885
- report.branch = featureBranch;
4769
+ for (const repo of checkedOutStagePackageRepos(root)) {
4770
+ if (!remoteBranchExists(repo.dir, featureBranch)) {
4771
+ results.push({ name: repo.name, path: repo.dir, skipped: true, reason: "remote-branch-missing" });
4772
+ continue;
4773
+ }
4774
+ results.push({
4775
+ name: repo.name,
4776
+ path: repo.dir,
4777
+ ...mergeBranchDownIntoFeature(repo.dir, {
4778
+ featureBranch,
4779
+ sourceBranch: STAGING_BRANCH,
4780
+ message: `stage: merge ${STAGING_BRANCH} into ${featureBranch}`,
4781
+ allowGeneratedMetadataAutoResolution: true
4782
+ })
4783
+ });
4784
+ }
4785
+ results.push({
4786
+ name: "@treeseed/market",
4787
+ path: repoRoot(root),
4788
+ ...mergeBranchDownIntoFeature(repoRoot(root), {
4789
+ featureBranch,
4790
+ sourceBranch: STAGING_BRANCH,
4791
+ message: `stage: merge ${STAGING_BRANCH} into ${featureBranch}`,
4792
+ allowGeneratedMetadataAutoResolution: true
4793
+ })
4794
+ });
3886
4795
  } catch (error) {
3887
- const reportData = collectMergeConflictReport(pkg.dir);
3888
- throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(reportData, pkg.dir, STAGING_BRANCH), {
3889
- details: { branch: featureBranch, packageName: pkg.name, report: reportData, originalError: error instanceof Error ? error.message : String(error) },
3890
- exitCode: 12
4796
+ const details = error && typeof error === "object" ? error : {};
4797
+ throw stageConflictError(error instanceof Error ? error.message : String(error), {
4798
+ ...details,
4799
+ results,
4800
+ branchName: featureBranch,
4801
+ targetBranch: STAGING_BRANCH
3891
4802
  });
3892
4803
  }
3893
- }
3894
- let rootMerge = null;
3895
- try {
3896
- rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
3897
- assertCleanWorktree(root);
3898
- if (isManagedWorkflowWorktree(root)) {
3899
- checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
3900
- } else {
3901
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
3902
- }
3903
- run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
3904
- if (mode === "recursive-workspace") {
3905
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
3906
- }
3907
- const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
3908
- root,
3909
- gitRoot: repoDir,
3910
- branch: STAGING_BRANCH,
3911
- onProgress: (line, stream) => helpers.write(line, stream)
3912
- });
3913
- if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
3914
- run("git", ["add", "-A"], { cwd: repoDir });
3915
- run("git", ["commit", "-m", message], { cwd: repoDir });
3916
- }
3917
- if (isManagedWorkflowWorktree(root)) {
3918
- pushHeadToBranch(repoDir, STAGING_BRANCH);
3919
- } else {
3920
- pushBranch(repoDir, STAGING_BRANCH);
3921
- }
3922
- return {
3923
- commitSha: headCommit(repoDir),
3924
- branch: STAGING_BRANCH,
3925
- committed: hasMeaningfulChanges(repoDir) ? false : true,
3926
- lockfileValidation: lockfileSafety.lockfileValidation,
3927
- lockfileInstall: lockfileSafety.install
3928
- };
3929
- });
3930
- rootRepo.merged = true;
3931
- rootRepo.committed = true;
3932
- rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
3933
- rootRepo.pushed = true;
3934
- rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
3935
- } catch (error) {
3936
- const report = collectMergeConflictReport(repoDir);
3937
- throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
3938
- details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
3939
- exitCode: 12
3940
- });
3941
- }
3942
- const stageWorkflowGateResult = !waitForStaging ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForWorkflowGates("stage", [
3943
- hostedDeployGate({
3944
- name: rootRepo.name,
3945
- repoPath: rootRepo.path,
3946
- workflow: "deploy.yml",
3947
- branch: STAGING_BRANCH,
3948
- headSha: rootRepo.commitSha
3949
- }),
3950
- ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
3951
- name: report.name,
3952
- repoPath: report.path,
3953
- workflow: "verify.yml",
3954
- branch: STAGING_BRANCH,
3955
- headSha: String(report.commitSha)
3956
- }))
3957
- ], ciMode, {
3958
- root,
3959
- runId: workflowRun.runId,
3960
- onProgress: (line, stream) => helpers.write(line, stream)
3961
- }).then((workflowGates) => ({
3962
- status: "completed",
3963
- workflowGates
3964
- })));
3965
- const stagingWait = {
3966
- status: String(stageWorkflowGateResult?.status ?? "completed"),
3967
- workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
3968
- };
3969
- const stageReleasePlan = buildReleasePlanSnapshot({
3970
- root,
3971
- mode,
3972
- level: "patch",
3973
- packageSelection: session.packageSelection,
3974
- packageReports,
3975
- rootRepo,
3976
- blockers: []
4804
+ return { status: "completed", results };
3977
4805
  });
3978
- const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("stage", root, stageReleasePlan));
3979
- const applicationSelection = selectWorkflowApplications(root, { packageSelection: session.packageSelection });
3980
- const hostingAudit = await runWorkflowHostedResourceVerification(
3981
- "stage",
3982
- root,
3983
- helpers,
3984
- "staging",
3985
- {
3986
- enabled: effectiveInput.verifyDeployedResources === true,
3987
- strict: true,
3988
- live: effectiveInput.verifyDeployedResources === true,
3989
- appId: singleSelectedWorkflowAppId(applicationSelection)
3990
- }
3991
- );
3992
- const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
3993
- const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
3994
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
3995
- const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
3996
- if (effectiveInput.deleteBranch !== false) {
3997
- deleteLocalBranch(repoDir, featureBranch);
4806
+ const mergeChanged = Array.isArray(mergeDown?.results) && mergeDown.results.some((entry) => Boolean(entry.merged));
4807
+ const saveResult = mergeChanged || hasMeaningfulChanges(repoRoot(root)) ? await executeJournalStep(root, workflowRun.runId, "save-integrated-feature", () => workflowSave(helpersForCwd(helpers, root), {
4808
+ message: `integrate staging before stage: ${message}`,
4809
+ verifyMode: "skip",
4810
+ ciMode: "off",
4811
+ refreshPreview: false,
4812
+ preview: false,
4813
+ workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
4814
+ })) : (skipJournalStep(root, workflowRun.runId, "save-integrated-feature", { skippedReason: "staging already integrated" }), null);
4815
+ const verification = verifyMode === "none" || process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE === "skip" ? (skipJournalStep(root, workflowRun.runId, "verify-integrated-feature", { mode: verifyMode, status: "skipped" }), {
4816
+ mode: verifyMode,
4817
+ status: "skipped",
4818
+ completedAt: null
4819
+ }) : await executeJournalStep(root, workflowRun.runId, "verify-integrated-feature", async () => {
4820
+ const proof = await runTreeseedProof({
4821
+ root,
4822
+ target: "staging",
4823
+ driver: verifyMode === "action" ? "act" : "local",
4824
+ write: (line, stream) => helpers.write(`[stage][verify] ${line}`, stream)
4825
+ });
4826
+ if (proof.failures.length > 0) {
4827
+ const first = proof.failures[0];
4828
+ workflowError("stage", "validation_failed", [
4829
+ "Treeseed stage proof failed.",
4830
+ `- ${first.subject.id}: ${first.invalidationReasons[0] ?? first.status}`,
4831
+ first.result.workflow?.url ? `Run: ${first.result.workflow.url}` : null
4832
+ ].join("\n"), {
4833
+ details: { proof }
4834
+ });
3998
4835
  }
3999
4836
  return {
4000
- deprecatedTag,
4001
- deletedRemote,
4002
- deletedLocal: effectiveInput.deleteBranch !== false,
4003
- branch: currentBranch(repoDir) || STAGING_BRANCH
4837
+ mode: verifyMode,
4838
+ status: "passed",
4839
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4840
+ proof
4004
4841
  };
4005
4842
  });
4006
- rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? rootRepo.tagName ?? "");
4007
- rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
4008
- rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
4009
- rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
4010
- rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
4011
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4012
- const report = findReportByName(packageReports, pkg.name);
4013
- if (!report) {
4014
- continue;
4015
- }
4016
- const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
4017
- deleteBranch: effectiveInput.deleteBranch !== false,
4018
- targetBranch: STAGING_BRANCH
4843
+ const manifest = await executeJournalStep(root, workflowRun.runId, "write-stage-candidate", () => {
4844
+ const currentPlan = buildStagePromotionPlan(root, featureBranch, { verifyMode, ciMode, cleanupMode, updateFrom });
4845
+ return writeStageCandidateManifest(root, workflowRun.runId, createStageCandidateManifest(root, workflowRun.runId, featureBranch, currentPlan, {
4846
+ mode: verifyMode,
4847
+ status: verification.status,
4848
+ completedAt: verification.completedAt
4019
4849
  }));
4020
- Object.assign(report, cleanup);
4021
- }
4022
- const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
4023
- const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
4024
- const managedWorktree = managedWorkflowWorktreeMetadata(root);
4025
- const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
4850
+ });
4851
+ const typedManifest = manifest;
4852
+ const promotion = await executeJournalStep(root, workflowRun.runId, "promote-to-staging", () => {
4853
+ const results = [];
4854
+ for (const pkg of typedManifest.packages) {
4855
+ results.push({
4856
+ name: pkg.name,
4857
+ ...promoteCommitToBranchWithExpectedHead(resolve(root, pkg.path), {
4858
+ commitSha: pkg.commit,
4859
+ targetBranch: STAGING_BRANCH,
4860
+ expectedBefore: typedManifest.stagingHeadsBefore[pkg.name] ?? null
4861
+ })
4862
+ });
4863
+ }
4864
+ results.push({
4865
+ name: "@treeseed/market",
4866
+ ...promoteCommitToBranchWithExpectedHead(repoRoot(root), {
4867
+ commitSha: typedManifest.root.commit,
4868
+ targetBranch: STAGING_BRANCH,
4869
+ expectedBefore: typedManifest.stagingHeadsBefore["@treeseed/market"] ?? null
4870
+ })
4871
+ });
4872
+ return { status: "completed", results };
4873
+ });
4874
+ const stagingRefs = await executeJournalStep(root, workflowRun.runId, "verify-staging-refs", () => {
4875
+ const refs = {};
4876
+ for (const pkg of typedManifest.packages) {
4877
+ const observed = remoteHeadCommit(resolve(root, pkg.path), STAGING_BRANCH);
4878
+ if (observed !== pkg.commit) {
4879
+ throw new Error(`${pkg.name} staging ref mismatch: expected ${pkg.commit}, observed ${observed}.`);
4880
+ }
4881
+ refs[pkg.name] = observed;
4882
+ }
4883
+ const rootObserved = remoteHeadCommit(repoRoot(root), STAGING_BRANCH);
4884
+ if (rootObserved !== typedManifest.root.commit) {
4885
+ throw new Error(`@treeseed/market staging ref mismatch: expected ${typedManifest.root.commit}, observed ${rootObserved}.`);
4886
+ }
4887
+ refs["@treeseed/market"] = rootObserved;
4888
+ return { status: "verified", refs };
4889
+ });
4890
+ const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForStagingAutomation(repoRoot(root))) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
4891
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
4892
+ const cleanup = cleanupMode === "success" ? await executeJournalStep(root, workflowRun.runId, "cleanup-source", () => cleanupStageSourceBranches(root, featureBranch, typedManifest)) : (skipJournalStep(root, workflowRun.runId, "cleanup-source", { skippedReason: "manual cleanup selected" }), { status: "skipped", reason: "manual cleanup selected" });
4026
4893
  const payload = {
4027
- mode,
4028
- branchName: featureBranch,
4029
- mergeTarget: STAGING_BRANCH,
4030
- mergeStrategy: "squash",
4031
- message,
4032
- autoSaved: autoSave.performed,
4033
- autoSaveResult: autoSave.save,
4034
- deprecatedTag: rootCleanup?.deprecatedTag ?? null,
4035
- repos: packageReports,
4036
- rootRepo,
4037
- stagingWait,
4038
- releaseCandidate,
4039
- applicationSelection,
4040
- previewCleanup,
4041
- lockfileValidation: rootMerge?.lockfileValidation ?? null,
4042
- lockfileInstall: rootMerge?.lockfileInstall ?? null,
4043
- remoteDeleted: rootRepo.deletedRemote,
4044
- localDeleted: rootRepo.deletedLocal,
4045
- finalBranch,
4894
+ ...basePayload,
4895
+ blockers: [],
4896
+ runId: workflowRun.runId,
4897
+ mergeDown,
4898
+ saveResult,
4899
+ verification,
4900
+ manifest: typedManifest,
4901
+ promotion,
4902
+ stagingRefs,
4903
+ hostedCi,
4904
+ cleanup,
4046
4905
  workspaceLinks,
4047
- ciMode,
4048
- workflowGates: stagingWait.workflowGates,
4049
- hostingAudit,
4050
- worktreeCleanup,
4051
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
4052
- managedWorktree,
4053
- worktreePath: managedWorktree?.worktreePath ?? null,
4054
- primaryRoot: managedWorktree?.primaryRoot ?? null
4906
+ finalBranch: cleanupMode === "success" ? STAGING_BRANCH : currentBranch(repoRoot(root)) || featureBranch,
4907
+ summary: "Stage promoted refs only. Hosted CI/CD was not waited on by default. Run `npx trsd ci --branch staging --json` or assign the staging release agent for CI/CD repair."
4055
4908
  };
4056
4909
  completeWorkflowRun(root, workflowRun.runId, payload);
4057
- return buildWorkflowResult(
4058
- "stage",
4059
- root,
4060
- payload,
4061
- {
4062
- runId: workflowRun.runId,
4063
- nextSteps: createNextSteps([
4064
- { operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
4065
- { operation: "status", reason: "Inspect staging readiness after the task branch merge." }
4066
- ])
4067
- }
4068
- );
4910
+ return buildWorkflowResult("stage", root, payload, {
4911
+ runId: workflowRun.runId,
4912
+ summary: "Treeseed stage completed successfully.",
4913
+ includeFinalState: false,
4914
+ nextSteps: createNextSteps([
4915
+ { operation: "ci", reason: "Inspect staging CI/CD status after branch promotion.", input: { branch: STAGING_BRANCH, failed: true } }
4916
+ ])
4917
+ });
4069
4918
  } catch (error) {
4070
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4919
+ try {
4920
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4921
+ } catch {
4922
+ }
4071
4923
  failWorkflowRun(root, workflowRun.runId, error, {
4072
4924
  resumable: true,
4073
4925
  runId: workflowRun.runId,
@@ -4083,44 +4935,97 @@ ${stagingReadiness.checks.filter((check) => check.status === "failed").map((chec
4083
4935
  toError("stage", error);
4084
4936
  }
4085
4937
  }
4086
- async function workflowTagsCleanup(helpers, input = {}) {
4087
- try {
4088
- return await withContextEnv(helpers.context.env, async () => {
4089
- const root = resolveProjectRootOrThrow("tags:cleanup", helpers.cwd());
4090
- const executionMode = input.plan === true || input.dryRun === true ? "plan" : "execute";
4091
- const cleanup = collectStaleDevTagCleanupReports(root, input, { execute: executionMode === "execute" });
4092
- return buildWorkflowResult("tags:cleanup", root, {
4093
- ...cleanup,
4094
- mode: hasCompleteTreeseedPackageCheckout(root) ? "recursive-workspace" : "root-only"
4095
- }, {
4096
- executionMode,
4097
- summary: executionMode === "plan" ? "Treeseed dev tag cleanup plan ready." : "Treeseed dev tag cleanup completed.",
4098
- nextSteps: executionMode === "plan" ? createNextSteps([{ operation: "tags:cleanup", reason: "Run without --plan to delete the stale Treeseed-managed dev tags.", input: { branchScope: cleanup.branchScope } }]) : createNextSteps([{ operation: "status", reason: "Inspect workspace state after tag cleanup." }])
4099
- });
4938
+ async function runReleaseGateReconcileFacade(operation, helpers, root, target, input, extraPayload = {}) {
4939
+ const executionMode = normalizeExecutionMode(input);
4940
+ const selector = {
4941
+ environment: target.kind === "persistent" ? target.scope : "staging",
4942
+ resourceKind: ["release-gate"],
4943
+ provider: ["treeseed"]
4944
+ };
4945
+ const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
4946
+ const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")) || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
4947
+ const rawUnitIds = new Set(rawUnits.map((unit) => unit.unitId));
4948
+ const units = rawUnits.map((unit) => ({
4949
+ ...unit,
4950
+ dependencies: unit.dependencies.filter((dependency) => rawUnitIds.has(dependency))
4951
+ }));
4952
+ const unitSelector = {
4953
+ environment: selector.environment,
4954
+ unitId: units.map((unit) => unit.unitId)
4955
+ };
4956
+ const plan = await planTreeseedReconciliation({
4957
+ tenantRoot: root,
4958
+ target,
4959
+ env: helpers.context.env,
4960
+ units,
4961
+ selector: unitSelector,
4962
+ write: (line) => helpers.write(`[${operation}][reconcile] ${line}`, "stderr")
4963
+ });
4964
+ const blockers = Array.isArray(extraPayload.blockers) ? extraPayload.blockers.map((blocker) => String(blocker)).filter(Boolean) : [];
4965
+ if (executionMode === "execute" && blockers.length > 0) {
4966
+ workflowError(operation, "validation_failed", `${operation} is blocked:
4967
+ ${blockers.join("\n")}`, {
4968
+ details: {
4969
+ blockers,
4970
+ target,
4971
+ plannedSteps: plan.plans.map((entry) => ({
4972
+ id: entry.unit.unitId,
4973
+ action: entry.diff.action,
4974
+ reasons: entry.diff.reasons
4975
+ }))
4976
+ }
4100
4977
  });
4101
- } catch (error) {
4102
- toError("tags:cleanup", error);
4103
4978
  }
4979
+ const result = executionMode === "execute" ? await reconcileTreeseedTarget({
4980
+ tenantRoot: root,
4981
+ target,
4982
+ env: helpers.context.env,
4983
+ units,
4984
+ selector: unitSelector,
4985
+ dryRun: input.execute !== true,
4986
+ write: (line) => helpers.write(`[${operation}][reconcile] ${line}`, "stderr")
4987
+ }) : null;
4988
+ const payload = {
4989
+ ...extraPayload,
4990
+ mode: "reconcile-release-gates",
4991
+ target,
4992
+ executionMode,
4993
+ verifyDeployedResources: input.verifyDeployedResources === true,
4994
+ desiredGraph,
4995
+ units: units.map((unit) => ({
4996
+ unitId: unit.unitId,
4997
+ unitType: unit.unitType,
4998
+ provider: unit.provider,
4999
+ logicalName: unit.logicalName,
5000
+ dependencies: unit.dependencies
5001
+ })),
5002
+ plannedSteps: plan.plans.map((entry) => ({
5003
+ id: entry.unit.unitId,
5004
+ description: `${entry.unit.provider}:${entry.unit.unitType} ${entry.unit.logicalName}`,
5005
+ action: entry.diff.action,
5006
+ reasons: entry.diff.reasons
5007
+ })),
5008
+ reconcile: result,
5009
+ legacyMutationPathDisabled: true
5010
+ };
5011
+ return buildWorkflowResult(operation, root, payload, {
5012
+ executionMode,
5013
+ summary: executionMode === "execute" ? `${operation} release gates reconciled through the canonical adapter path.` : `${operation} release gate plan ready.`,
5014
+ nextSteps: createNextSteps([
5015
+ operation === "stage" ? { operation: "release", reason: "Promote production after the staging release-gate candidate is green.", input: { bump: "patch", plan: true } } : { operation: "status", reason: "Inspect production readiness after release gates complete." }
5016
+ ])
5017
+ });
4104
5018
  }
4105
5019
  async function workflowRelease(helpers, input) {
4106
5020
  try {
4107
5021
  return await withContextEnv(helpers.context.env, async () => {
4108
- const root = resolveProjectRootOrThrow("release", helpers.cwd());
4109
- reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
4110
- operation: "release",
4111
- onProgress: (line, stream) => helpers.write(line, stream),
4112
- throwOnBlocker: true
4113
- });
5022
+ const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
4114
5023
  const session = resolveTreeseedWorkflowSession(root);
4115
- const gitRoot = session.gitRoot;
4116
- const mode = session.mode;
4117
5024
  const executionMode = normalizeExecutionMode(input);
4118
5025
  const rootRepo = createWorkspaceRootRepoReport(root);
4119
5026
  const packageReports = createWorkspacePackageReports(root);
4120
5027
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
4121
- const freshRelease = input.fresh === true && !explicitResumeRunId;
4122
- const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
4123
- const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: true }) : null;
5028
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
4124
5029
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
4125
5030
  const effectiveInput = autoResumeRun ? {
4126
5031
  ...autoResumeRun.input,
@@ -4128,11 +5033,10 @@ async function workflowRelease(helpers, input) {
4128
5033
  } : input;
4129
5034
  const level = effectiveInput.bump ?? "patch";
4130
5035
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
4131
- const isResume = Boolean(explicitResumeRunId || autoResumeRun);
4132
5036
  const packageSelection = session.packageSelection;
4133
5037
  const plannedRelease = buildReleasePlanSnapshot({
4134
5038
  root,
4135
- mode,
5039
+ mode: session.mode,
4136
5040
  level,
4137
5041
  repairVersionLine: effectiveInput.repairVersionLine === true,
4138
5042
  targetVersionLine: effectiveInput.targetVersionLine,
@@ -4141,9 +5045,8 @@ async function workflowRelease(helpers, input) {
4141
5045
  rootRepo,
4142
5046
  blockers: []
4143
5047
  });
4144
- const plannedPackageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
4145
- const selectedPackageNames = new Set(plannedPackageSelection.selected);
4146
- const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, plannedPackageSelection.selected, {
5048
+ const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5049
+ const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
4147
5050
  level,
4148
5051
  repairVersionLine: effectiveInput.repairVersionLine === true
4149
5052
  });
@@ -4152,68 +5055,81 @@ async function workflowRelease(helpers, input) {
4152
5055
  environment: "prod",
4153
5056
  appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
4154
5057
  });
4155
- if (!isResume) {
4156
- blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
4157
- }
5058
+ blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
4158
5059
  plannedRelease.blockers = blockers;
5060
+ const releaseBasePayload = {
5061
+ ...plannedRelease,
5062
+ readiness: plannedReadiness,
5063
+ ciMode,
5064
+ level,
5065
+ fresh: input.fresh === true,
5066
+ freshArchivedRuns: [],
5067
+ autoResumeCandidate: planAutoResumeRun ? {
5068
+ runId: planAutoResumeRun.runId,
5069
+ branch: planAutoResumeRun.session.branchName,
5070
+ failure: planAutoResumeRun.failure
5071
+ } : null,
5072
+ ...worktreePayload(root, effectiveInput.worktreeMode)
5073
+ };
4159
5074
  if (executionMode === "plan") {
4160
- return buildWorkflowResult("release", root, {
4161
- ...plannedRelease,
4162
- readiness: plannedReadiness,
4163
- ciMode,
4164
- fresh: input.fresh === true,
4165
- freshArchivedRuns: [],
4166
- ...worktreePayload(root, effectiveInput.worktreeMode),
4167
- autoResumeCandidate: planAutoResumeRun ? {
4168
- runId: planAutoResumeRun.runId,
4169
- branch: planAutoResumeRun.session.branchName,
4170
- failure: planAutoResumeRun.failure
4171
- } : null
4172
- }, {
4173
- executionMode,
4174
- nextSteps: createNextSteps([
4175
- { operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
4176
- ])
4177
- });
5075
+ return runReleaseGateReconcileFacade(
5076
+ "release",
5077
+ helpers,
5078
+ root,
5079
+ { kind: "persistent", scope: "prod" },
5080
+ {
5081
+ plan: true,
5082
+ verifyDeployedResources: effectiveInput.verifyDeployedResources
5083
+ },
5084
+ releaseBasePayload
5085
+ );
4178
5086
  }
4179
5087
  if (blockers.length > 0) {
4180
- workflowError("release", "validation_failed", blockers.join("\n"), {
4181
- details: { blockers }
5088
+ workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
5089
+ ${blockers.join("\n")}`, {
5090
+ details: { blockers, releasePlan: plannedRelease }
4182
5091
  });
4183
5092
  }
5093
+ const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
5094
+ const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
5095
+ const stableVersions = new Map([
5096
+ ...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
5097
+ ...selectedVersions.entries()
5098
+ ]);
5099
+ const allVersions = new Map([
5100
+ ["@treeseed/market", plannedRelease.rootVersion],
5101
+ ...stableVersions.entries()
5102
+ ]);
5103
+ const selectedPackageSet = new Set(selectedPackageNames);
4184
5104
  const workflowRun = acquireWorkflowRun(
4185
5105
  "release",
4186
5106
  session,
4187
5107
  {
5108
+ ...effectiveInput,
4188
5109
  bump: level,
4189
- repairVersionLine: effectiveInput.repairVersionLine === true,
4190
- targetVersionLine: effectiveInput.targetVersionLine,
4191
- devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
4192
- gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
4193
- gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
4194
5110
  ciMode,
4195
- fresh: input.fresh === true,
4196
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
4197
- workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
5111
+ fresh: input.fresh === true
4198
5112
  },
4199
5113
  [
4200
- { id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4201
- { id: "release-staging-gates", description: "Verify current staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4202
- { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4203
- { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4204
- ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
4205
- ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
4206
- id: `release-${report.name}`,
4207
- description: `Release ${report.name}`,
4208
- repoName: report.name,
4209
- repoPath: report.path,
4210
- branch: STAGING_BRANCH,
4211
- resumable: true
4212
- })),
4213
- { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4214
- { id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
4215
- { id: "release-back-merge", description: "Back-merge main into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
4216
- ...mode === "recursive-workspace" ? [{ id: "cleanup-dev-tags", description: "Clean replaced dev package tags", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : []
5114
+ { id: "release-plan", description: "Record immutable release plan and target versions", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5115
+ { id: "release-gates", description: "Run production release gates against staging evidence", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5116
+ { id: "workspace-unlink", description: "Remove local workspace links before stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5117
+ { id: "prepare-release-metadata", description: "Rewrite package metadata and lockfiles to production dependency mode", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5118
+ ...selectedPackageNames.map((name) => {
5119
+ const report = packageReports.find((entry) => entry.name === name);
5120
+ return {
5121
+ id: `release-${name}`,
5122
+ description: `Release ${name} ${selectedVersions.get(name) ?? "(planned)"}`,
5123
+ repoName: name,
5124
+ repoPath: report?.path ?? root,
5125
+ branch: STAGING_BRANCH,
5126
+ resumable: true
5127
+ };
5128
+ }),
5129
+ { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5130
+ { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5131
+ { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5132
+ { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
4217
5133
  ],
4218
5134
  autoResumeRun ? {
4219
5135
  ...helpers.context,
@@ -4224,565 +5140,188 @@ async function workflowRelease(helpers, input) {
4224
5140
  } : helpers.context
4225
5141
  );
4226
5142
  if (autoResumeRun) {
4227
- helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
4228
- }
4229
- const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
4230
- if (resumeAtRootGates) {
4231
- helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
5143
+ helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}.`);
4232
5144
  }
4233
- let releaseCleanupSnapshot = null;
4234
5145
  try {
4235
- const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
4236
- const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
4237
- const effectiveSelectedPackageNames = new Set(effectivePackageSelection.selected);
4238
- const effectiveVersions = releasePlanVersionMap(releasePlan.plannedVersions);
4239
- const effectiveStableDependencyVersions = releasePlanStableDependencyVersionMap(releasePlan);
4240
- const effectiveDependencyReplacementVersions = new Map([
4241
- ...effectiveStableDependencyVersions.entries(),
4242
- ...effectiveVersions.entries()
4243
- ]);
4244
- const rootVersion = String(releasePlan.rootVersion);
4245
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
4246
- assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
4247
- const productionReadiness = collectTreeseedDeploymentReadiness({ tenantRoot: root, environment: "prod" });
4248
- if (!productionReadiness.ok) {
4249
- workflowError(
4250
- "release",
4251
- "validation_failed",
4252
- `Deployment readiness failed for prod:
4253
- ${productionReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`).join("\n")}`,
4254
- { details: { readiness: productionReadiness } }
4255
- );
4256
- }
4257
- const stagingGateResult = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-staging-gates") : await executeJournalStep(root, workflowRun.runId, "release-staging-gates", () => {
4258
- helpers.write("[release][workflow] Verifying current staging gates before production release.");
4259
- const packageGates = checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => ({
5146
+ const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => ({
5147
+ ...releaseBasePayload,
5148
+ freshArchivedRuns: freshPreparation.archived
5149
+ }));
5150
+ const releaseGates = await executeJournalStep(root, workflowRun.runId, "release-gates", async () => runReleaseGateReconcileFacade(
5151
+ "release",
5152
+ helpers,
5153
+ root,
5154
+ { kind: "persistent", scope: "prod" },
5155
+ {
5156
+ execute: true,
5157
+ verifyDeployedResources: effectiveInput.verifyDeployedResources
5158
+ },
5159
+ {
5160
+ ...releaseBasePayload,
5161
+ freshArchivedRuns: freshPreparation.archived
5162
+ }
5163
+ ));
5164
+ const workspaceUnlink = await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
5165
+ const releaseMetadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
5166
+ applyStableWorkspaceVersionChanges(root, allVersions);
5167
+ const adapterMetadata = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => ({
4260
5168
  name: pkg.name,
4261
- repoPath: pkg.dir,
4262
- workflow: "verify.yml",
4263
- branch: STAGING_BRANCH,
4264
- headSha: headCommit(pkg.dir)
4265
- }));
4266
- return waitForWorkflowGates("release", [
4267
- hostedDeployGate({
4268
- name: rootRepo.name,
4269
- repoPath: rootRepo.path,
4270
- workflow: "deploy.yml",
4271
- branch: STAGING_BRANCH,
4272
- headSha: headCommit(gitRoot)
4273
- }),
4274
- ...packageGates
4275
- ], ciMode, {
4276
- root,
4277
- runId: workflowRun.runId,
4278
- onProgress: (line, stream) => helpers.write(line, stream)
4279
- }).then((workflowGates) => ({ workflowGates }));
4280
- });
4281
- const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
4282
- if (!resumeAtRootGates && !isResume) {
4283
- assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
4284
- assertCleanWorktree(root);
4285
- }
4286
- if (!resumeAtRootGates) {
4287
- prepareReleaseBranches(root);
4288
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4289
- runWorkspaceReleasePreflight({ cwd: root });
4290
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
4291
- }
4292
- if (mode === "root-only") {
4293
- const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
4294
- setRootPackageJsonVersion(root, rootVersion);
4295
- run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
4296
- const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
4297
- const changelog = updateReleaseChangelog(gitRoot, {
4298
- version: rootVersion,
4299
- commits: rootCommitsBeforeChangelog,
4300
- extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
4301
- });
4302
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4303
- subject: `release: ${level} bump`,
4304
- version: rootVersion,
4305
- tagName: rootVersion,
4306
- commits: rootCommitsBeforeChangelog,
4307
- changelog
4308
- }));
4309
- pushBranch(gitRoot, STAGING_BRANCH);
4310
- const stagingCommit = headCommit(gitRoot);
4311
- const rootCommits = releaseHistoryCommits(gitRoot);
4312
- const released = mergeBranchIntoTarget(root, {
4313
- sourceBranch: STAGING_BRANCH,
4314
- targetBranch: PRODUCTION_BRANCH,
4315
- message: releaseAdminMessage({
4316
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4317
- version: rootVersion,
4318
- tagName: rootVersion,
4319
- commits: rootCommits,
4320
- changelog
4321
- }),
4322
- pushTarget: true
4323
- });
4324
- const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
4325
- subject: `release: ${rootVersion}`,
4326
- version: rootVersion,
4327
- tagName: rootVersion,
4328
- commits: rootCommits,
4329
- changelog
4330
- }));
4331
- syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
4332
- return {
4333
- rootVersion,
4334
- stagingCommit,
4335
- releasedCommit: released.commitSha,
4336
- tag,
4337
- changelog,
4338
- adminCommitSummary: {
4339
- commitCount: rootCommits.length,
4340
- notableCommits: rootCommits.slice(0, 12)
4341
- }
4342
- };
4343
- });
4344
- rootRepo.committed = true;
4345
- rootRepo.pushed = true;
4346
- rootRepo.merged = true;
4347
- rootRepo.branch = PRODUCTION_BRANCH;
4348
- rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
4349
- rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
4350
- const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
4351
- hostedDeployGate({
4352
- name: rootRepo.name,
4353
- repoPath: rootRepo.path,
4354
- workflow: "deploy.yml",
4355
- branch: rootVersion,
4356
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
4357
- })
4358
- ].filter((gate) => gate.headSha), ciMode, {
4359
- root,
4360
- runId: workflowRun.runId,
4361
- onProgress: (line, stream) => helpers.write(line, stream)
4362
- }).then((workflowGates) => ({ workflowGates })));
4363
- const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
4364
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4365
- const applicationSelection2 = selectWorkflowApplications(root, { changedPaths: ["treeseed.site.yaml"] });
4366
- const hostingAudit2 = await runWorkflowHostedResourceVerification("release", root, helpers, "prod", {
4367
- enabled: true,
4368
- strict: effectiveInput.verifyDeployedResources === true,
4369
- live: effectiveInput.verifyDeployedResources === true,
4370
- appId: singleSelectedWorkflowAppId(applicationSelection2)
4371
- });
4372
- const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
4373
- version: rootVersion,
4374
- changelog: rootRelease2?.changelog ?? null
5169
+ version: selectedVersions.get(pkg.name) ?? null,
5170
+ result: selectedVersions.has(pkg.name) ? prepareAdapterReleaseMetadata(root, pkg, selectedVersions.get(pkg.name)) : { status: "skipped", reason: "no planned version" }
4375
5171
  }));
4376
- const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4377
- const payload2 = {
4378
- mode,
4379
- mergeStrategy: "merge-commit",
4380
- level,
4381
- fresh: input.fresh === true,
4382
- freshArchivedRuns: freshPreparation.archived,
4383
- resumed: workflowRun.resumed,
4384
- resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
4385
- autoResumed: autoResumeRun != null,
4386
- rootVersion: String(rootRelease2?.rootVersion ?? ""),
4387
- releaseTag: String(rootRelease2?.rootVersion ?? ""),
4388
- releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
4389
- stagingBranch: STAGING_BRANCH,
4390
- productionBranch: PRODUCTION_BRANCH,
4391
- touchedPackages: [],
4392
- packageSelection: { changed: [], dependents: [], selected: [] },
4393
- applicationSelection: applicationSelection2,
4394
- publishWait: [],
4395
- repos: [],
4396
- rootRepo,
4397
- releaseCandidate,
4398
- stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4399
- releaseBackMerge: releaseBackMerge2,
4400
- hostedDeploymentState: hostedDeploymentState2,
4401
- finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
4402
- pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
4403
- workspaceLinks: workspaceLinks2,
4404
- ciMode,
4405
- workflowGates: [
4406
- ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4407
- ...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
4408
- ],
4409
- hostingAudit: hostingAudit2,
4410
- ...worktreePayload(root, effectiveInput.worktreeMode)
4411
- };
4412
- completeWorkflowRun(root, workflowRun.runId, payload2);
4413
- return buildWorkflowResult("release", root, payload2, {
4414
- runId: workflowRun.runId,
4415
- nextSteps: createNextSteps([
4416
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
4417
- ])
4418
- });
4419
- }
4420
- if (!resumeAtRootGates) {
4421
- validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
4422
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4423
- if (effectiveSelectedPackageNames.has(pkg.name)) {
4424
- prepareReleaseBranches(pkg.dir);
4425
- }
5172
+ const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5173
+ const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet).filter((issue) => issue.reason !== "git-release-ref" && issue.reason !== "lockfile-git-release-ref");
5174
+ if (remainingDevReferences.length > 0) {
5175
+ const rendered = remainingDevReferences.map((issue) => `${issue.repoName}: ${issue.filePath} ${issue.dependencyName ?? ""} ${issue.reason} ${issue.spec}`).join("\n");
5176
+ throw new Error(`Stable release metadata still contains development references.
5177
+ ${rendered}`);
4426
5178
  }
4427
- }
4428
- releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
4429
- const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
4430
- const releasedPackageDevTags2 = Object.fromEntries(
4431
- checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
4432
- const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
4433
- return [pkg.name, String(packageJson.version ?? "")];
4434
- }).filter(([, version]) => version.includes("-dev."))
4435
- );
4436
- const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveDependencyReplacementVersions);
4437
- applyStableWorkspaceVersionChanges(root, effectiveVersions);
4438
- setRootPackageJsonVersion(root, rootVersion);
4439
- const releaseInstalls2 = [
4440
- { name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
4441
- ];
4442
- assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
4443
5179
  return {
4444
- releasedPackageDevTags: releasedPackageDevTags2,
4445
- replacedDevReferences: replacedDevReferences2,
4446
- releaseInstalls: releaseInstalls2
5180
+ versions: Object.fromEntries(allVersions.entries()),
5181
+ adapterMetadata,
5182
+ rootInstall,
5183
+ workspaceUnlink
4447
5184
  };
4448
- }, { rerunCompleted: workflowRun.resumed && !resumeAtRootGates });
4449
- const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
4450
- const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
4451
- const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
4452
- const publishWait = [];
4453
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4454
- const report = findReportByName(packageReports, pkg.name);
4455
- if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
4456
- if (report) {
4457
- report.skippedReason = "unchanged";
4458
- }
4459
- continue;
4460
- }
4461
- const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
4462
- checkoutBranch(pkg.dir, STAGING_BRANCH);
4463
- const tagName = String(effectiveVersions.get(pkg.name));
4464
- releaseInstalls.push({
4465
- name: pkg.name,
4466
- ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
4467
- });
4468
- assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
4469
- const packageCommitsBeforeChangelog = releaseHistoryCommits(pkg.dir);
5185
+ });
5186
+ const packageReleases = [];
5187
+ for (const pkg of checkedOutWorkspacePackageRepos(root).filter((entry) => selectedPackageSet.has(entry.name))) {
5188
+ const version = selectedVersions.get(pkg.name);
5189
+ if (!version) continue;
5190
+ const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, () => {
4470
5191
  const changelog = updateReleaseChangelog(pkg.dir, {
4471
- version: tagName,
4472
- commits: packageCommitsBeforeChangelog,
4473
- extraDependencyBullets: [`Release ${pkg.name} ${tagName}.`]
5192
+ version,
5193
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
5194
+ targetRef: "HEAD"
4474
5195
  });
4475
- if (hasMeaningfulChanges(pkg.dir)) {
4476
- run("git", ["add", "-A"], { cwd: pkg.dir });
4477
- run("git", ["commit", "-m", releaseAdminMessage({
4478
- subject: `release: ${tagName}`,
4479
- version: tagName,
4480
- tagName,
4481
- commits: packageCommitsBeforeChangelog,
4482
- changelog,
4483
- extraLines: [`Package: ${pkg.name}`]
4484
- })], { cwd: pkg.dir });
4485
- }
4486
- pushBranch(pkg.dir, STAGING_BRANCH);
4487
- const packageCommits = releaseHistoryCommits(pkg.dir);
4488
- const mergeResult = mergeBranchIntoTarget(pkg.dir, {
4489
- sourceBranch: STAGING_BRANCH,
4490
- targetBranch: PRODUCTION_BRANCH,
4491
- message: releaseAdminMessage({
4492
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4493
- version: tagName,
4494
- tagName,
4495
- commits: packageCommits,
4496
- changelog,
4497
- extraLines: [`Package: ${pkg.name}`]
4498
- }),
4499
- pushTarget: true,
4500
- allowUnrelatedHistories: true
4501
- });
4502
- const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
4503
- subject: `release: ${tagName}`,
4504
- version: tagName,
4505
- tagName,
4506
- commits: packageCommits,
4507
- changelog,
4508
- extraLines: [`Package: ${pkg.name}`]
4509
- }));
4510
- const workflowGates = await waitForWorkflowGates("release", [
4511
- {
4512
- name: pkg.name,
4513
- repoPath: pkg.dir,
4514
- workflow: "publish.yml",
4515
- headSha: mergeResult.commitSha,
4516
- branch: tagName
4517
- }
4518
- ], ciMode, {
4519
- root,
4520
- runId: workflowRun.runId,
4521
- onProgress: (line, stream) => helpers.write(line, stream)
4522
- });
4523
- const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
4524
- assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
4525
- const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
4526
- subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
4527
- version: tagName,
4528
- tagName,
4529
- sourceRef: PRODUCTION_BRANCH,
4530
- targetRef: STAGING_BRANCH,
4531
- commits: packageCommits,
4532
- changelog,
4533
- extraLines: [`Package: ${pkg.name}`]
5196
+ const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
5197
+ subject: `release: ${pkg.name} ${version}`,
5198
+ version,
5199
+ tagName: version,
5200
+ sourceRef: STAGING_BRANCH,
5201
+ targetRef: PRODUCTION_BRANCH,
5202
+ changelog
4534
5203
  }));
4535
- syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
5204
+ pushBranch(pkg.dir, STAGING_BRANCH);
5205
+ const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
5206
+ const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
4536
5207
  return {
4537
- commitSha: mergeResult.commitSha,
4538
- tagName,
4539
- tag,
5208
+ name: pkg.name,
5209
+ path: relative(root, pkg.dir),
5210
+ version,
4540
5211
  changelog,
4541
- adminCommitSummary: {
4542
- commitCount: packageCommits.length,
4543
- notableCommits: packageCommits.slice(0, 12)
4544
- },
4545
- publish,
4546
- workflowGates,
4547
- backMerge
5212
+ commit,
5213
+ promotion,
5214
+ tag
4548
5215
  };
4549
5216
  });
4550
- report.committed = true;
4551
- report.pushed = true;
4552
- report.merged = true;
4553
- report.tagName = String(releasedPackage?.tagName ?? "");
4554
- report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
4555
- report.publishWait = releasedPackage?.publish ?? null;
4556
- report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
4557
- report.backMerge = releasedPackage?.backMerge ?? null;
4558
- report.changelog = releasedPackage?.changelog ?? null;
4559
- report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
4560
- report.branch = STAGING_BRANCH;
4561
- publishWait.push({
4562
- name: report.name,
4563
- ...releasedPackage?.publish ?? {}
4564
- });
5217
+ packageReleases.push(packageRelease);
4565
5218
  }
4566
- assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
4567
5219
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
4568
- setRootPackageJsonVersion(root, rootVersion);
4569
- run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
4570
- const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
4571
- const changelog = updateReleaseChangelog(gitRoot, {
4572
- version: rootVersion,
4573
- commits: rootCommitsBeforeChangelog,
4574
- extraDependencyBullets: [
4575
- `Release @treeseed/market ${rootVersion}.`,
4576
- ...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
4577
- ]
4578
- });
4579
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4580
- subject: `release: ${level} bump`,
4581
- version: rootVersion,
4582
- tagName: rootVersion,
4583
- commits: rootCommitsBeforeChangelog,
4584
- changelog,
4585
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
4586
- }));
4587
- pushBranch(gitRoot, STAGING_BRANCH);
4588
- const stagingCommit = headCommit(gitRoot);
4589
- const rootCommits = releaseHistoryCommits(gitRoot);
4590
- let released;
4591
- let submoduleReconciliation = null;
4592
- const mergeMessage = releaseAdminMessage({
4593
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4594
- version: rootVersion,
4595
- tagName: rootVersion,
4596
- commits: rootCommits,
4597
- changelog,
4598
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
5220
+ const changelog = updateReleaseChangelog(repoRoot(root), {
5221
+ version: plannedRelease.rootVersion,
5222
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
5223
+ targetRef: "HEAD",
5224
+ extraDependencyBullets: versionLines(stableVersions)
4599
5225
  });
4600
- try {
4601
- released = mergeBranchIntoTarget(root, {
4602
- sourceBranch: STAGING_BRANCH,
4603
- targetBranch: PRODUCTION_BRANCH,
4604
- message: mergeMessage,
4605
- pushTarget: false,
4606
- quietMerge: true
4607
- });
4608
- } catch (error) {
4609
- const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
4610
- if (!reconciliation.resolved) {
4611
- throw error;
4612
- }
4613
- helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
4614
- submoduleReconciliation = reconciliation;
4615
- commitAllIfChanged(gitRoot, mergeMessage);
4616
- released = { commitSha: headCommit(gitRoot) };
4617
- }
4618
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4619
- if (effectiveSelectedPackageNames.has(pkg.name)) {
4620
- syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
4621
- }
4622
- }
4623
- const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
4624
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4625
- subject: "release: sync package main heads",
4626
- version: rootVersion,
4627
- tagName: rootVersion,
4628
- sourceRef: "package main heads",
5226
+ const commit = commitAllIfChanged(repoRoot(root), releaseAdminMessage({
5227
+ subject: `release: market ${plannedRelease.rootVersion}`,
5228
+ version: plannedRelease.rootVersion,
5229
+ tagName: plannedRelease.releaseTag,
5230
+ sourceRef: STAGING_BRANCH,
4629
5231
  targetRef: PRODUCTION_BRANCH,
4630
- commits: mainPointerCommits,
4631
- changelog,
4632
- extraLines: versionLines(effectiveVersions).map((line) => `Main package ${line}`)
4633
- }));
4634
- const releasedCommit = headCommit(gitRoot);
4635
- const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit, releaseAdminMessage({
4636
- subject: `release: ${rootVersion}`,
4637
- version: rootVersion,
4638
- tagName: rootVersion,
4639
- commits: rootCommits,
4640
5232
  changelog,
4641
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
5233
+ extraLines: versionLines(stableVersions).map((line) => `Released package ${line}`)
4642
5234
  }));
4643
- run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
4644
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
4645
- syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
5235
+ pushBranch(repoRoot(root), STAGING_BRANCH);
5236
+ const promotion = promoteCommitToProductionBranch(repoRoot(root), commit.commitSha);
5237
+ const tag = ensureReleaseTag(repoRoot(root), plannedRelease.releaseTag, commit.commitSha, `release: market ${plannedRelease.rootVersion}`);
4646
5238
  return {
4647
- rootVersion,
4648
- stagingCommit,
4649
- releasedCommit,
4650
- mergeCommit: released.commitSha,
4651
- tag,
5239
+ name: "@treeseed/market",
5240
+ version: plannedRelease.rootVersion,
5241
+ releaseTag: plannedRelease.releaseTag,
4652
5242
  changelog,
4653
- adminCommitSummary: {
4654
- commitCount: rootCommits.length,
4655
- notableCommits: rootCommits.slice(0, 12)
4656
- },
4657
- submoduleReconciliation
5243
+ commit,
5244
+ promotion,
5245
+ tag
4658
5246
  };
4659
5247
  });
4660
- rootRepo.committed = true;
4661
- rootRepo.pushed = true;
4662
- rootRepo.merged = true;
4663
- rootRepo.branch = PRODUCTION_BRANCH;
4664
- rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
4665
- rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
4666
- const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
5248
+ const publishGates = [
4667
5249
  hostedDeployGate({
4668
- name: rootRepo.name,
4669
- repoPath: rootRepo.path,
4670
- workflow: "deploy.yml",
4671
- branch: rootVersion,
4672
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
4673
- })
4674
- ].filter((gate) => gate.headSha), ciMode, {
5250
+ name: "@treeseed/market",
5251
+ repoPath: repoRoot(root),
5252
+ workflow: "deploy-web.yml",
5253
+ branch: PRODUCTION_BRANCH,
5254
+ headSha: String(rootRelease.commit.commitSha ?? ""),
5255
+ dispatchIfMissing: true,
5256
+ dispatchInputs: {
5257
+ environment: "prod",
5258
+ action_kind: "deploy_web"
5259
+ }
5260
+ }),
5261
+ ...packageReleases.map((entry) => ({
5262
+ name: String(entry.name),
5263
+ repoPath: resolve(root, String(entry.path)),
5264
+ workflow: releaseWorkflowForPackage(root, String(entry.name)),
5265
+ branch: String(entry.version),
5266
+ headSha: String(entry.commit.commitSha ?? "")
5267
+ }))
5268
+ ].filter((gate) => gate.headSha);
5269
+ const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
4675
5270
  root,
4676
5271
  runId: workflowRun.runId,
4677
5272
  onProgress: (line, stream) => helpers.write(line, stream)
4678
5273
  }).then((workflowGates) => ({ workflowGates })));
4679
- const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
4680
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4681
- const applicationSelection = releasePlan.applicationSelection;
4682
- const hostingAudit = await runWorkflowHostedResourceVerification("release", root, helpers, "prod", {
4683
- enabled: true,
4684
- strict: effectiveInput.verifyDeployedResources === true,
4685
- live: effectiveInput.verifyDeployedResources === true,
4686
- appId: singleSelectedWorkflowAppId(applicationSelection)
4687
- });
4688
- const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
4689
- version: rootVersion,
4690
- changelog: rootRelease?.changelog ?? null,
4691
- selectedVersions: effectiveVersions
4692
- }));
4693
- const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
4694
- const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
4695
- const cleanup = collectStaleDevTagCleanupReports(root, {
4696
- includePackages: effectivePackageSelection.selected,
4697
- branchScope: "all"
4698
- }, { execute: true });
4699
- return { ...cleanup, replacedDevReferenceCount: replacedDevReferences.length, releasedPackageDevTagCount: releasedPackageDevTags.size };
5274
+ const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
5275
+ const packageBackMerges = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
5276
+ subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
5277
+ version: selectedVersions.get(pkg.name) ?? null,
5278
+ sourceRef: PRODUCTION_BRANCH,
5279
+ targetRef: STAGING_BRANCH
5280
+ })));
5281
+ const rootBackMerge = backMergeRootProductionIntoStaging(root, true, {
5282
+ version: plannedRelease.rootVersion,
5283
+ selectedVersions: stableVersions
5284
+ });
5285
+ return { packages: packageBackMerges, root: rootBackMerge };
4700
5286
  });
4701
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
4702
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
5287
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
4703
5288
  const payload = {
4704
- mode,
4705
- mergeStrategy: "merge-commit",
4706
- level,
4707
- fresh: input.fresh === true,
4708
- freshArchivedRuns: freshPreparation.archived,
4709
- resumed: workflowRun.resumed,
4710
- resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
4711
- autoResumed: autoResumeRun != null,
4712
- rootVersion: String(rootRelease?.rootVersion ?? ""),
4713
- releaseTag: String(rootRelease?.rootVersion ?? ""),
4714
- releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
4715
- stagingBranch: STAGING_BRANCH,
4716
- productionBranch: PRODUCTION_BRANCH,
4717
- touchedPackages: effectivePackageSelection.selected,
4718
- packageSelection: effectivePackageSelection,
4719
- applicationSelection,
4720
- replacedDevReferences,
4721
- releaseInstalls,
4722
- devTagCleanup,
4723
- publishWait,
4724
- repos: packageReports,
4725
- rootRepo,
4726
- releaseCandidate,
4727
- stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4728
- releaseBackMerge,
4729
- hostedDeploymentState,
4730
- finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
4731
- pushStatus: {
4732
- stagingPushed: true,
4733
- productionPushed: true,
4734
- tagPushed: true
4735
- },
5289
+ ...releaseBasePayload,
5290
+ mode: plannedRelease.mode,
5291
+ runId: workflowRun.runId,
5292
+ releasePlan,
5293
+ releaseGates,
5294
+ workspaceUnlink,
5295
+ releaseMetadata,
5296
+ packageReleases,
5297
+ rootRelease,
5298
+ publishWait: publishWait.workflowGates,
5299
+ backMerge,
4736
5300
  workspaceLinks,
4737
- ciMode,
4738
- workflowGates: [
4739
- ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4740
- ...packageReports.flatMap((report) => report.workflowGates),
4741
- ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
4742
- ],
4743
- hostingAudit,
4744
- ...worktreePayload(root, effectiveInput.worktreeMode)
5301
+ releasedCommit: String(rootRelease.commit.commitSha ?? ""),
5302
+ touchedPackages: selectedPackageNames,
5303
+ finalBranch: STAGING_BRANCH
4745
5304
  };
4746
5305
  completeWorkflowRun(root, workflowRun.runId, payload);
4747
5306
  return buildWorkflowResult("release", root, payload, {
4748
5307
  runId: workflowRun.runId,
5308
+ summary: "Treeseed production release completed successfully.",
4749
5309
  nextSteps: createNextSteps([
4750
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
5310
+ { operation: "status", reason: "Inspect release state after production promotion." }
4751
5311
  ])
4752
5312
  });
4753
5313
  } catch (error) {
4754
- const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
4755
- const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
4756
- const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
4757
- const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
4758
- helpers.write(`[release][recovery] Last release phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`, "stderr");
4759
5314
  try {
4760
- const repair = reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
4761
- onProgress: (line, stream) => helpers.write(line, stream)
4762
- });
4763
- if (repair.blockers.length > 0) {
4764
- helpers.write(`[release][recovery] Package repos need manual review before retrying:
4765
- ${repair.blockers.join("\n")}`, "stderr");
4766
- }
4767
- } catch (repairError) {
4768
- helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
4769
- }
4770
- if (localCleanup.restored.length > 0) {
4771
- helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
4772
- }
4773
- if (localCleanup.manualReview.length > 0) {
4774
- helpers.write(`[release][recovery] Local cleanup needs manual review:
4775
- ${localCleanup.manualReview.map((entry) => `- ${String(entry.repo ?? entry.scope ?? "repo")}: ${String(entry.reason ?? "unknown")}`).join("\n")}`, "stderr");
5315
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
5316
+ } catch {
4776
5317
  }
4777
- helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
4778
5318
  failWorkflowRun(root, workflowRun.runId, error, {
4779
5319
  resumable: true,
4780
5320
  runId: workflowRun.runId,
4781
5321
  command: "release",
4782
- message: `Resume the interrupted release on ${STAGING_BRANCH}. Last phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`,
4783
- recoverCommand: "npx trsd recover --json",
4784
- resumeCommand: `npx trsd release --${level} --json`,
4785
- localCleanup
5322
+ message: "Resume the interrupted production release after fixing the cause.",
5323
+ recoverCommand: "treeseed recover",
5324
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
4786
5325
  });
4787
5326
  throw error;
4788
5327
  }
@@ -4844,6 +5383,8 @@ async function workflowResume(helpers, input) {
4844
5383
  return workflowSwitch(resumedHelpers, journal.input);
4845
5384
  case "save":
4846
5385
  return workflowSave(resumedHelpers, journal.input);
5386
+ case "update":
5387
+ return workflowUpdate(resumedHelpers, journal.input);
4847
5388
  case "close":
4848
5389
  return workflowClose(resumedHelpers, journal.input);
4849
5390
  case "stage":
@@ -4866,7 +5407,7 @@ async function workflowRecover(helpers, input = {}) {
4866
5407
  try {
4867
5408
  return await withContextEnv(helpers.context.env, async () => {
4868
5409
  const root = resolveProjectRootOrThrow("recover", helpers.cwd());
4869
- const lock = inspectWorkflowLock(root);
5410
+ const lock = inspectWorkflowLock(root, { scope: "worktree" });
4870
5411
  const journals = listWorkflowRunJournals(root);
4871
5412
  const session = resolveTreeseedWorkflowSession(root);
4872
5413
  const currentHeads = Object.fromEntries(
@@ -5107,19 +5648,27 @@ async function workflowDestroy(helpers, input) {
5107
5648
  }
5108
5649
  export {
5109
5650
  TreeseedWorkflowError,
5651
+ destroyTreeseedBranchPreview,
5652
+ normalizeReleaseCandidateMode,
5653
+ normalizeSaveCiMode,
5654
+ normalizeSaveLane,
5655
+ reconcileTreeseedBranchPreview,
5656
+ shouldUseHostedSaveCi,
5110
5657
  workflowCi,
5111
5658
  workflowClose,
5112
5659
  workflowConfig,
5113
5660
  workflowDestroy,
5114
5661
  workflowDev,
5115
5662
  workflowExport,
5663
+ workflowProof,
5116
5664
  workflowRecover,
5117
5665
  workflowRelease,
5666
+ workflowReleaseCandidate,
5118
5667
  workflowResume,
5119
5668
  workflowSave,
5120
5669
  workflowStage,
5121
5670
  workflowStatus,
5122
5671
  workflowSwitch,
5123
- workflowTagsCleanup,
5124
- workflowTasks
5672
+ workflowTasks,
5673
+ workflowUpdate
5125
5674
  };