@treeseed/sdk 0.10.28 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (509) hide show
  1. package/README.md +213 -7
  2. package/dist/agent-capacity.d.ts +916 -0
  3. package/dist/agent-capacity.js +1600 -0
  4. package/dist/agent-tools.d.ts +37 -0
  5. package/dist/agent-tools.js +301 -0
  6. package/dist/api/app.d.ts +1 -1
  7. package/dist/api/auth/d1-database.d.ts +2 -2
  8. package/dist/api/auth/d1-provider.d.ts +4 -4
  9. package/dist/api/auth/d1-store.d.ts +2 -2
  10. package/dist/api/auth/memory-provider.d.ts +1 -1
  11. package/dist/api/auth/tokens.d.ts +1 -1
  12. package/dist/api/config.d.ts +3 -1
  13. package/dist/api/config.js +34 -10
  14. package/dist/api/http.d.ts +2 -2
  15. package/dist/api/index.d.ts +10 -10
  16. package/dist/api/index.js +4 -2
  17. package/dist/api/operations-routes.d.ts +3 -3
  18. package/dist/api/operations.d.ts +1 -1
  19. package/dist/api/providers.d.ts +1 -1
  20. package/dist/api/providers.js +21 -4
  21. package/dist/api/railway.d.ts +3 -2
  22. package/dist/api/sdk-dispatch.d.ts +4 -4
  23. package/dist/api/sdk-routes.d.ts +2 -2
  24. package/dist/api/templates.d.ts +2 -2
  25. package/dist/api/types.d.ts +3 -2
  26. package/dist/capacity-provider.d.ts +118 -59
  27. package/dist/capacity-provider.js +235 -43
  28. package/dist/capacity.d.ts +2 -2
  29. package/dist/cli-tools.d.ts +1 -1
  30. package/dist/content-operations.d.ts +97 -0
  31. package/dist/content-operations.js +250 -0
  32. package/dist/content-store.d.ts +4 -4
  33. package/dist/control-plane-client.d.ts +13 -18
  34. package/dist/control-plane-client.js +8 -86
  35. package/dist/control-plane.d.ts +3 -2
  36. package/dist/control-plane.js +38 -13
  37. package/dist/copilot.d.ts +3 -0
  38. package/dist/copilot.js +16 -1
  39. package/dist/d1-http.d.ts +1 -1
  40. package/dist/d1-store.d.ts +32 -57
  41. package/dist/d1-store.js +115 -318
  42. package/dist/db/d1.d.ts +1 -232
  43. package/dist/db/index.d.ts +3 -3
  44. package/dist/db/market-schema.d.ts +50900 -11018
  45. package/dist/db/market-schema.js +2313 -467
  46. package/dist/db/node-sqlite.d.ts +1 -232
  47. package/dist/db/node-sqlite.js +7 -2
  48. package/dist/db/schema.d.ts +0 -462
  49. package/dist/db/schema.js +2 -22
  50. package/dist/dispatch.d.ts +1 -1
  51. package/dist/dispatch.js +0 -11
  52. package/dist/fixture-support.js +52 -4
  53. package/dist/governance.d.ts +133 -0
  54. package/dist/governance.js +285 -0
  55. package/dist/graph/build.d.ts +2 -2
  56. package/dist/graph/context-query-contracts.d.ts +1 -1
  57. package/dist/graph/dsl.d.ts +1 -1
  58. package/dist/graph/query.d.ts +4 -4
  59. package/dist/graph/ranking.d.ts +1 -1
  60. package/dist/graph/schema.d.ts +1 -1
  61. package/dist/graph.d.ts +11 -11
  62. package/dist/guarantees/index.d.ts +425 -0
  63. package/dist/guarantees/index.js +1146 -0
  64. package/dist/hosting/apps.d.ts +12 -0
  65. package/dist/hosting/apps.js +107 -0
  66. package/dist/hosting/builtins.d.ts +25 -0
  67. package/dist/hosting/builtins.js +602 -0
  68. package/dist/hosting/contracts.d.ts +208 -0
  69. package/dist/hosting/contracts.js +0 -0
  70. package/dist/hosting/graph.d.ts +195 -0
  71. package/dist/hosting/graph.js +1092 -0
  72. package/dist/hosting/index.d.ts +4 -0
  73. package/dist/hosting/index.js +4 -0
  74. package/dist/index.d.ts +88 -67
  75. package/dist/index.js +142 -14
  76. package/dist/local-dev/managed-dev.d.ts +85 -0
  77. package/dist/local-dev/managed-dev.js +368 -0
  78. package/dist/managed-dependencies.d.ts +9 -0
  79. package/dist/managed-dependencies.js +10 -9
  80. package/dist/market-client.d.ts +327 -9
  81. package/dist/market-client.js +464 -20
  82. package/dist/model-registry.d.ts +1 -1
  83. package/dist/model-registry.js +19 -19
  84. package/dist/operations/agent-tools.d.ts +2 -11
  85. package/dist/operations/agent-tools.js +2 -14
  86. package/dist/operations/providers/default.d.ts +1 -1
  87. package/dist/operations/providers/default.js +20 -37
  88. package/dist/operations/repository-operations.d.ts +26 -2
  89. package/dist/operations/repository-operations.js +159 -1
  90. package/dist/operations/runtime.d.ts +3 -3
  91. package/dist/operations/services/bootstrap-runner.d.ts +4 -2
  92. package/dist/operations/services/bootstrap-runner.js +22 -2
  93. package/dist/operations/services/build-warning-policy.d.ts +34 -0
  94. package/dist/operations/services/build-warning-policy.js +81 -82
  95. package/dist/operations/services/commit-message-provider.js +4 -4
  96. package/dist/operations/services/config-runtime.d.ts +83 -24
  97. package/dist/operations/services/config-runtime.js +449 -130
  98. package/dist/operations/services/deploy.d.ts +106 -624
  99. package/dist/operations/services/deploy.js +746 -263
  100. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  101. package/dist/operations/services/deployment-readiness.js +206 -0
  102. package/dist/operations/services/git-remote-policy.js +12 -4
  103. package/dist/operations/services/git-runner.d.ts +66 -0
  104. package/dist/operations/services/git-runner.js +479 -0
  105. package/dist/operations/services/git-workflow.d.ts +45 -6
  106. package/dist/operations/services/git-workflow.js +251 -23
  107. package/dist/operations/services/github-actions-verification.d.ts +4 -1
  108. package/dist/operations/services/github-actions-verification.js +12 -3
  109. package/dist/operations/services/github-api.js +22 -6
  110. package/dist/operations/services/github-automation.d.ts +4 -4
  111. package/dist/operations/services/github-automation.js +19 -21
  112. package/dist/operations/services/github-credentials.d.ts +13 -0
  113. package/dist/operations/services/github-credentials.js +59 -0
  114. package/dist/operations/services/hosted-service-checks.d.ts +81 -0
  115. package/dist/operations/services/hosted-service-checks.js +478 -0
  116. package/dist/operations/services/hosting-audit.d.ts +2 -2
  117. package/dist/operations/services/hosting-audit.js +7 -7
  118. package/dist/operations/services/hub-launch.d.ts +1 -1
  119. package/dist/operations/services/hub-provider-launch.d.ts +5 -5
  120. package/dist/operations/services/hub-provider-launch.js +22 -17
  121. package/dist/operations/services/live-hosted-service-checks.d.ts +26 -0
  122. package/dist/operations/services/live-hosted-service-checks.js +463 -0
  123. package/dist/operations/services/local-dev.js +0 -1
  124. package/dist/operations/services/managed-host-security.d.ts +1 -1
  125. package/dist/operations/services/managed-host-security.js +1 -1
  126. package/dist/operations/services/managed-repositories.d.ts +37 -0
  127. package/dist/operations/services/managed-repositories.js +177 -0
  128. package/dist/operations/services/operations-runner-smoke.d.ts +31 -0
  129. package/dist/operations/services/operations-runner-smoke.js +198 -0
  130. package/dist/operations/services/package-adapters.d.ts +170 -0
  131. package/dist/operations/services/package-adapters.js +859 -0
  132. package/dist/operations/services/package-reference-policy.d.ts +13 -88
  133. package/dist/operations/services/package-reference-policy.js +96 -215
  134. package/dist/operations/services/project-host-operations.d.ts +4 -4
  135. package/dist/operations/services/project-platform.d.ts +57 -173
  136. package/dist/operations/services/project-platform.js +80 -111
  137. package/dist/operations/services/project-web-monitor.d.ts +2 -2
  138. package/dist/operations/services/project-web-monitor.js +122 -5
  139. package/dist/operations/services/railway-api.d.ts +117 -5
  140. package/dist/operations/services/railway-api.js +915 -93
  141. package/dist/operations/services/railway-deploy.d.ts +30 -112
  142. package/dist/operations/services/railway-deploy.js +521 -961
  143. package/dist/operations/services/release-history.js +3 -4
  144. package/dist/operations/services/release-proof-ledger.d.ts +13 -0
  145. package/dist/operations/services/release-proof-ledger.js +81 -0
  146. package/dist/operations/services/release-proof-planner.d.ts +48 -0
  147. package/dist/operations/services/release-proof-planner.js +195 -0
  148. package/dist/operations/services/release-proof-runner.d.ts +16 -0
  149. package/dist/operations/services/release-proof-runner.js +169 -0
  150. package/dist/operations/services/release-proof.d.ts +81 -0
  151. package/dist/operations/services/release-proof.js +79 -0
  152. package/dist/operations/services/repository-save-orchestrator.d.ts +8 -7
  153. package/dist/operations/services/repository-save-orchestrator.js +604 -212
  154. package/dist/operations/services/runtime-paths.d.ts +1 -3
  155. package/dist/operations/services/runtime-paths.js +0 -4
  156. package/dist/operations/services/runtime-tools.d.ts +1 -191
  157. package/dist/operations/services/runtime-tools.js +4 -443
  158. package/dist/operations/services/save-deploy-preflight.js +9 -1
  159. package/dist/operations/services/template-host-bindings.d.ts +2 -2
  160. package/dist/operations/services/template-host-bindings.js +3 -2
  161. package/dist/operations/services/template-registry.d.ts +6 -6
  162. package/dist/operations/services/template-registry.js +33 -14
  163. package/dist/operations/services/template-secret-sync.d.ts +3 -3
  164. package/dist/operations/services/verification-cache.d.ts +25 -0
  165. package/dist/operations/services/verification-cache.js +71 -0
  166. package/dist/operations/services/workflow-timing.d.ts +25 -0
  167. package/dist/operations/services/workflow-timing.js +79 -0
  168. package/dist/operations/services/workspace-dependency-mode.js +50 -5
  169. package/dist/operations/services/workspace-preflight.js +8 -8
  170. package/dist/operations/services/workspace-save-script.d.ts +4 -4
  171. package/dist/operations/services/workspace-save.d.ts +1 -0
  172. package/dist/operations/services/workspace-save.js +42 -19
  173. package/dist/operations/services/workspace-tools.js +42 -7
  174. package/dist/operations-registry.d.ts +1 -1
  175. package/dist/operations-registry.js +5 -6
  176. package/dist/operations-types.d.ts +1 -1
  177. package/dist/operations.d.ts +17 -11
  178. package/dist/operations.js +57 -3
  179. package/dist/platform/book-export.js +2 -0
  180. package/dist/platform/books-data.d.ts +1 -1
  181. package/dist/platform/config.d.ts +27 -0
  182. package/dist/platform/config.js +100 -0
  183. package/dist/platform/content-runtime-source.d.ts +54 -0
  184. package/dist/platform/content-runtime-source.js +169 -0
  185. package/dist/platform/contracts.d.ts +38 -2
  186. package/dist/platform/deploy-config.d.ts +1 -1
  187. package/dist/platform/deploy-config.js +80 -9
  188. package/dist/platform/deploy-runtime.d.ts +1 -1
  189. package/dist/platform/desired-state.d.ts +63 -0
  190. package/dist/platform/desired-state.js +1021 -0
  191. package/dist/platform/env.yaml +568 -45
  192. package/dist/platform/environment.d.ts +3 -2
  193. package/dist/platform/environment.js +122 -21
  194. package/dist/platform/local-content-materialization.d.ts +9 -0
  195. package/dist/platform/local-content-materialization.js +212 -0
  196. package/dist/platform/plugin.d.ts +13 -4
  197. package/dist/platform/plugins/runtime.d.ts +3 -3
  198. package/dist/platform/plugins/runtime.js +62 -1
  199. package/dist/platform/plugins.d.ts +6 -6
  200. package/dist/platform/published-content-pipeline.d.ts +3 -3
  201. package/dist/platform/published-content-pipeline.js +5 -1
  202. package/dist/platform/published-content.d.ts +4 -3
  203. package/dist/platform/site-config-schema.d.ts +1 -0
  204. package/dist/platform/site-config-schema.js +4 -1
  205. package/dist/platform/tenant/runtime-config.d.ts +152 -2
  206. package/dist/platform/tenant-config.d.ts +1 -1
  207. package/dist/platform/utils/site-config-schema.d.ts +154 -0
  208. package/dist/platform/utils/site-config-schema.js +400 -461
  209. package/dist/platform-operation-store.d.ts +1 -1
  210. package/dist/platform-operation-store.js +2 -2
  211. package/dist/platform-operations.js +1 -1
  212. package/dist/plugin-default.d.ts +1 -1
  213. package/dist/plugin-default.js +27 -12
  214. package/dist/project-import.d.ts +91 -0
  215. package/dist/project-import.js +257 -0
  216. package/dist/project-workflow.d.ts +1 -1
  217. package/dist/reconcile/bootstrap-systems.d.ts +2 -2
  218. package/dist/reconcile/bootstrap-systems.js +5 -5
  219. package/dist/reconcile/builtin-adapters.d.ts +7 -1
  220. package/dist/reconcile/builtin-adapters.js +3190 -429
  221. package/dist/reconcile/contracts.d.ts +23 -6
  222. package/dist/reconcile/desired-state.d.ts +2 -192
  223. package/dist/reconcile/desired-state.js +29 -38
  224. package/dist/reconcile/engine.d.ts +61 -395
  225. package/dist/reconcile/engine.js +229 -44
  226. package/dist/reconcile/index.d.ts +10 -8
  227. package/dist/reconcile/index.js +2 -0
  228. package/dist/reconcile/live-acceptance.d.ts +100 -0
  229. package/dist/reconcile/live-acceptance.js +2027 -0
  230. package/dist/reconcile/platform.d.ts +104 -0
  231. package/dist/reconcile/platform.js +100 -0
  232. package/dist/reconcile/providers/docker-private.d.ts +54 -0
  233. package/dist/reconcile/providers/docker-private.js +101 -0
  234. package/dist/reconcile/providers/github-private.d.ts +65 -0
  235. package/dist/reconcile/providers/github-private.js +119 -0
  236. package/dist/reconcile/providers/local-private.d.ts +28 -0
  237. package/dist/reconcile/providers/local-private.js +75 -0
  238. package/dist/reconcile/providers/railway-iac.d.ts +66 -0
  239. package/dist/reconcile/providers/railway-iac.js +275 -0
  240. package/dist/reconcile/providers/release-private.d.ts +92 -0
  241. package/dist/reconcile/providers/release-private.js +152 -0
  242. package/dist/reconcile/registry.d.ts +2 -2
  243. package/dist/reconcile/registry.js +16 -1
  244. package/dist/reconcile/state.d.ts +1 -1
  245. package/dist/reconcile/state.js +7 -44
  246. package/dist/reconcile/units.d.ts +1 -1
  247. package/dist/reconcile/units.js +9 -3
  248. package/dist/remote.d.ts +6 -6
  249. package/dist/scenes/artifacts.d.ts +16 -0
  250. package/dist/scenes/artifacts.js +61 -0
  251. package/dist/scenes/auth.d.ts +2 -0
  252. package/dist/scenes/auth.js +28 -0
  253. package/dist/scenes/base-url.d.ts +12 -0
  254. package/dist/scenes/base-url.js +123 -0
  255. package/dist/scenes/builtin-plugins.d.ts +2 -0
  256. package/dist/scenes/builtin-plugins.js +447 -0
  257. package/dist/scenes/checkpoints.d.ts +14 -0
  258. package/dist/scenes/checkpoints.js +33 -0
  259. package/dist/scenes/device-matrix.d.ts +2 -0
  260. package/dist/scenes/device-matrix.js +97 -0
  261. package/dist/scenes/devices.d.ts +11 -0
  262. package/dist/scenes/devices.js +22 -0
  263. package/dist/scenes/diagnostics.d.ts +5 -0
  264. package/dist/scenes/diagnostics.js +22 -0
  265. package/dist/scenes/diagram-providers.d.ts +2 -0
  266. package/dist/scenes/diagram-providers.js +181 -0
  267. package/dist/scenes/diagram-validation.d.ts +22 -0
  268. package/dist/scenes/diagram-validation.js +74 -0
  269. package/dist/scenes/environment.d.ts +2 -0
  270. package/dist/scenes/environment.js +65 -0
  271. package/dist/scenes/evidence-bundle.d.ts +5 -0
  272. package/dist/scenes/evidence-bundle.js +42 -0
  273. package/dist/scenes/evidence-report.d.ts +10 -0
  274. package/dist/scenes/evidence-report.js +73 -0
  275. package/dist/scenes/evidence.d.ts +16 -0
  276. package/dist/scenes/evidence.js +277 -0
  277. package/dist/scenes/index.d.ts +48 -0
  278. package/dist/scenes/index.js +312 -0
  279. package/dist/scenes/inspect.d.ts +9 -0
  280. package/dist/scenes/inspect.js +64 -0
  281. package/dist/scenes/loader.d.ts +3 -0
  282. package/dist/scenes/loader.js +38 -0
  283. package/dist/scenes/logs.d.ts +2 -0
  284. package/dist/scenes/logs.js +47 -0
  285. package/dist/scenes/operations.d.ts +3 -0
  286. package/dist/scenes/operations.js +128 -0
  287. package/dist/scenes/phase0.d.ts +9 -0
  288. package/dist/scenes/phase0.js +637 -0
  289. package/dist/scenes/planner.d.ts +12 -0
  290. package/dist/scenes/planner.js +166 -0
  291. package/dist/scenes/playwright-adapter.d.ts +2 -0
  292. package/dist/scenes/playwright-adapter.js +63 -0
  293. package/dist/scenes/plugins.d.ts +5 -0
  294. package/dist/scenes/plugins.js +115 -0
  295. package/dist/scenes/progress.d.ts +17 -0
  296. package/dist/scenes/progress.js +24 -0
  297. package/dist/scenes/publish-destinations.d.ts +12 -0
  298. package/dist/scenes/publish-destinations.js +99 -0
  299. package/dist/scenes/publish-plan-report.d.ts +10 -0
  300. package/dist/scenes/publish-plan-report.js +70 -0
  301. package/dist/scenes/publish-plan.d.ts +15 -0
  302. package/dist/scenes/publish-plan.js +248 -0
  303. package/dist/scenes/publish-redaction.d.ts +16 -0
  304. package/dist/scenes/publish-redaction.js +148 -0
  305. package/dist/scenes/publish-release-record.d.ts +13 -0
  306. package/dist/scenes/publish-release-record.js +34 -0
  307. package/dist/scenes/publish-report.d.ts +10 -0
  308. package/dist/scenes/publish-report.js +67 -0
  309. package/dist/scenes/publish.d.ts +16 -0
  310. package/dist/scenes/publish.js +298 -0
  311. package/dist/scenes/registry.d.ts +12 -0
  312. package/dist/scenes/registry.js +65 -0
  313. package/dist/scenes/remotion-adapter.d.ts +3 -0
  314. package/dist/scenes/remotion-adapter.js +94 -0
  315. package/dist/scenes/remotion-composition-registry.d.ts +2 -0
  316. package/dist/scenes/remotion-composition-registry.js +12 -0
  317. package/dist/scenes/remotion-compositions.d.ts +15 -0
  318. package/dist/scenes/remotion-compositions.js +464 -0
  319. package/dist/scenes/remotion-diagrams.d.ts +9 -0
  320. package/dist/scenes/remotion-diagrams.js +188 -0
  321. package/dist/scenes/remotion-input.d.ts +13 -0
  322. package/dist/scenes/remotion-input.js +291 -0
  323. package/dist/scenes/remotion-visuals.d.ts +29 -0
  324. package/dist/scenes/remotion-visuals.js +198 -0
  325. package/dist/scenes/render-media-assets.d.ts +9 -0
  326. package/dist/scenes/render-media-assets.js +117 -0
  327. package/dist/scenes/render-report.d.ts +10 -0
  328. package/dist/scenes/render-report.js +27 -0
  329. package/dist/scenes/render.d.ts +2 -0
  330. package/dist/scenes/render.js +218 -0
  331. package/dist/scenes/reporter.d.ts +3 -0
  332. package/dist/scenes/reporter.js +101 -0
  333. package/dist/scenes/resume.d.ts +2 -0
  334. package/dist/scenes/resume.js +79 -0
  335. package/dist/scenes/runner.d.ts +2 -0
  336. package/dist/scenes/runner.js +674 -0
  337. package/dist/scenes/schema.d.ts +6 -0
  338. package/dist/scenes/schema.js +994 -0
  339. package/dist/scenes/seed.d.ts +2 -0
  340. package/dist/scenes/seed.js +83 -0
  341. package/dist/scenes/segments.d.ts +15 -0
  342. package/dist/scenes/segments.js +79 -0
  343. package/dist/scenes/selectors.d.ts +3 -0
  344. package/dist/scenes/selectors.js +21 -0
  345. package/dist/scenes/timeline.d.ts +11 -0
  346. package/dist/scenes/timeline.js +26 -0
  347. package/dist/scenes/timeouts.d.ts +6 -0
  348. package/dist/scenes/timeouts.js +17 -0
  349. package/dist/scenes/training-captions.d.ts +14 -0
  350. package/dist/scenes/training-captions.js +98 -0
  351. package/dist/scenes/training-report.d.ts +10 -0
  352. package/dist/scenes/training-report.js +78 -0
  353. package/dist/scenes/training-transcript.d.ts +24 -0
  354. package/dist/scenes/training-transcript.js +204 -0
  355. package/dist/scenes/training.d.ts +10 -0
  356. package/dist/scenes/training.js +134 -0
  357. package/dist/scenes/types.d.ts +1872 -0
  358. package/dist/scenes/types.js +8 -0
  359. package/dist/scenes/visual-audit-contact-sheets.d.ts +6 -0
  360. package/dist/scenes/visual-audit-contact-sheets.js +119 -0
  361. package/dist/scenes/visual-audit-fixtures.d.ts +25 -0
  362. package/dist/scenes/visual-audit-fixtures.js +210 -0
  363. package/dist/scenes/visual-audit-report.d.ts +11 -0
  364. package/dist/scenes/visual-audit-report.js +85 -0
  365. package/dist/scenes/visual-audit-review.d.ts +23 -0
  366. package/dist/scenes/visual-audit-review.js +829 -0
  367. package/dist/scenes/visual-audit-routes.d.ts +13 -0
  368. package/dist/scenes/visual-audit-routes.js +308 -0
  369. package/dist/scenes/visual-audit.d.ts +3 -0
  370. package/dist/scenes/visual-audit.js +596 -0
  371. package/dist/scripts/build-dist.js +154 -40
  372. package/dist/scripts/check-build-warnings.js +42 -53
  373. package/dist/scripts/cleanup-markdown.js +1 -1
  374. package/dist/scripts/cli-test-fixtures.js +24 -28
  375. package/dist/scripts/deployment-readiness.js +20 -0
  376. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  377. package/dist/scripts/migrate-journey-csv-to-guarantees.js +140 -0
  378. package/dist/scripts/operations-runner-smoke.js +16 -0
  379. package/dist/scripts/package-tools.js +12 -1
  380. package/dist/scripts/prepare.js +3 -7
  381. package/dist/scripts/release-verify.js +6 -6
  382. package/dist/scripts/run-fixture-astro-command.js +1 -1
  383. package/dist/scripts/template-catalog.test.js +68 -82
  384. package/dist/scripts/tenant-dev.js +0 -10
  385. package/dist/scripts/tenant-workflow-action.js +10 -1
  386. package/dist/scripts/test-cloudflare-local.js +1 -1
  387. package/dist/scripts/test-scaffold.js +0 -1
  388. package/dist/scripts/test-smoke.js +3 -3
  389. package/dist/scripts/treeseed-build-dist.js +4 -7
  390. package/dist/scripts/treeseed-release-verify.js +1 -1
  391. package/dist/scripts/verify-driver.js +30 -21
  392. package/dist/scripts/workflow-commands.test.js +18 -27
  393. package/dist/scripts/workspace-command-e2e.js +1 -1
  394. package/dist/scripts/workspace-lint.js +6 -0
  395. package/dist/scripts/workspace-release-test.js +90 -1
  396. package/dist/scripts/workspace-test.js +6 -0
  397. package/dist/sdk-dispatch.d.ts +1 -1
  398. package/dist/sdk-dispatch.js +0 -16
  399. package/dist/sdk-fields.d.ts +1 -1
  400. package/dist/sdk-filters.d.ts +1 -1
  401. package/dist/sdk-types.d.ts +1352 -116
  402. package/dist/sdk-types.js +413 -2
  403. package/dist/sdk.d.ts +106 -100
  404. package/dist/sdk.js +211 -51
  405. package/dist/secrets-capability.d.ts +421 -0
  406. package/dist/secrets-capability.js +530 -0
  407. package/dist/seeds/normalize.js +1 -0
  408. package/dist/seeds/planner.js +38 -0
  409. package/dist/seeds/schema.js +293 -4
  410. package/dist/seeds/types.d.ts +60 -0
  411. package/dist/seeds/types.js +9 -1
  412. package/dist/service-credentials.d.ts +54 -0
  413. package/dist/service-credentials.js +112 -0
  414. package/dist/stores/cursor-store.d.ts +2 -2
  415. package/dist/stores/envelopes.d.ts +1 -1
  416. package/dist/stores/helpers.d.ts +1 -1
  417. package/dist/stores/lease-store.d.ts +2 -2
  418. package/dist/stores/message-store.d.ts +2 -2
  419. package/dist/stores/operational-store.d.ts +3 -26
  420. package/dist/stores/operational-store.js +0 -225
  421. package/dist/stores/project-workflow-store.d.ts +2 -2
  422. package/dist/stores/run-store.d.ts +2 -3
  423. package/dist/stores/run-store.js +0 -98
  424. package/dist/stores/subscription-store.d.ts +2 -3
  425. package/dist/stores/subscription-store.js +0 -100
  426. package/dist/template-catalog.d.ts +1 -1
  427. package/dist/template-launch-requirements.d.ts +2 -2
  428. package/dist/template-launch-requirements.js +9 -0
  429. package/dist/template-launch-ui.d.ts +2 -2
  430. package/dist/treedx/adapters.d.ts +6 -0
  431. package/dist/treedx/adapters.js +36 -0
  432. package/dist/treedx/client.d.ts +222 -0
  433. package/dist/treedx/client.js +871 -0
  434. package/dist/treedx/errors.d.ts +13 -0
  435. package/dist/treedx/errors.js +17 -0
  436. package/dist/treedx/federated-client.d.ts +27 -0
  437. package/dist/treedx/federated-client.js +158 -0
  438. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  439. package/dist/treedx/generated/openapi-types.js +0 -0
  440. package/dist/treedx/graph-adapter.d.ts +33 -0
  441. package/dist/treedx/graph-adapter.js +156 -0
  442. package/dist/treedx/index.d.ts +14 -0
  443. package/dist/treedx/index.js +48 -0
  444. package/dist/treedx/market-integration.d.ts +27 -0
  445. package/dist/treedx/market-integration.js +131 -0
  446. package/dist/treedx/ports.d.ts +166 -0
  447. package/dist/treedx/ports.js +231 -0
  448. package/dist/treedx/query-adapter.d.ts +19 -0
  449. package/dist/treedx/query-adapter.js +62 -0
  450. package/dist/treedx/registry-client.d.ts +11 -0
  451. package/dist/treedx/registry-client.js +19 -0
  452. package/dist/treedx/repository-adapter.d.ts +45 -0
  453. package/dist/treedx/repository-adapter.js +319 -0
  454. package/dist/treedx/sdk-integration.d.ts +27 -0
  455. package/dist/treedx/sdk-integration.js +63 -0
  456. package/dist/treedx/types.d.ts +1084 -0
  457. package/dist/treedx/types.js +8 -0
  458. package/dist/treedx/workspace-adapter.d.ts +27 -0
  459. package/dist/treedx/workspace-adapter.js +65 -0
  460. package/dist/treedx-backends.d.ts +220 -0
  461. package/dist/treedx-backends.js +634 -0
  462. package/dist/treedx-client.d.ts +89 -0
  463. package/dist/treedx-client.js +184 -0
  464. package/dist/treeseed/services/compose.yml +2 -3
  465. package/dist/treeseed/template-catalog/catalog.fixture.json +27 -27
  466. package/dist/types/agents.d.ts +256 -0
  467. package/dist/types/agents.js +42 -1
  468. package/dist/verification.d.ts +1 -0
  469. package/dist/verification.js +143 -14
  470. package/dist/workflow/operations.d.ts +580 -523
  471. package/dist/workflow/operations.js +2084 -1279
  472. package/dist/workflow/policy.js +19 -7
  473. package/dist/workflow/runs.d.ts +9 -3
  474. package/dist/workflow/runs.js +66 -26
  475. package/dist/workflow/session.d.ts +4 -1
  476. package/dist/workflow/session.js +50 -10
  477. package/dist/workflow/worktrees.d.ts +6 -2
  478. package/dist/workflow/worktrees.js +98 -15
  479. package/dist/workflow-state.d.ts +17 -4
  480. package/dist/workflow-state.js +68 -41
  481. package/dist/workflow-support.d.ts +26 -17
  482. package/dist/workflow-support.js +84 -24
  483. package/dist/workflow.d.ts +51 -16
  484. package/dist/workflow.js +19 -7
  485. package/drizzle/d1/0000_treeseed_d1.sql +0 -18
  486. package/drizzle/market/0000_market_control_plane.sql +1793 -1499
  487. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  488. package/drizzle/market/0005_agent_capacity_coordination.sql +145 -0
  489. package/drizzle/market/0006_provider_assignment_lifecycle.sql +9 -0
  490. package/drizzle/market/0007_agent_architecture_gap_closure.sql +157 -0
  491. package/drizzle/market/0008_agent_capacity_operational_closure.sql +27 -0
  492. package/drizzle/market/0009_agent_capacity_runtime_hardening.sql +29 -0
  493. package/drizzle/market/0010_secrets_capability_persistence.sql +130 -0
  494. package/drizzle/market/0011_github_app_adapter.sql +47 -0
  495. package/drizzle/market/0012_workday_test_runs.sql +44 -0
  496. package/drizzle/market/0013_runtime_task_lifecycle.sql +39 -0
  497. package/drizzle/market/0014_governance_pipeline.sql +189 -0
  498. package/package.json +106 -18
  499. package/scripts/{verify-driver.mjs → verify-driver.ts} +19 -4
  500. package/templates/github/deploy-web.workflow.yml +49 -12
  501. package/dist/operations/services/mailpit-runtime.d.ts +0 -20
  502. package/dist/operations/services/mailpit-runtime.js +0 -67
  503. package/dist/operations/services/release-candidate.d.ts +0 -41
  504. package/dist/operations/services/release-candidate.js +0 -616
  505. package/dist/scripts/ensure-mailpit.js +0 -48
  506. package/dist/scripts/logs-mailpit.js +0 -2
  507. package/dist/scripts/run-ts.js +0 -45
  508. package/dist/scripts/stop-mailpit.js +0 -5
  509. package/dist/scripts/treeseed-run-ts.js +0 -45
@@ -1,6 +1,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,
@@ -18,28 +28,24 @@ import {
18
28
  inspectTreeseedKeyAgentStatus,
19
29
  loadTreeseedMachineConfig,
20
30
  resolveTreeseedLaunchEnvironment,
31
+ resolveTreeseedMachineEnvironmentValues,
21
32
  resolveTreeseedRemoteSession,
22
33
  rotateTreeseedMachineKey,
23
34
  setTreeseedRemoteSession,
24
35
  writeTreeseedMachineConfig
25
36
  } from "../operations/services/config-runtime.js";
26
- import { collectTreeseedToolStatus, formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
37
+ import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
27
38
  import { ControlPlaneClient } from "../control-plane-client.js";
28
39
  import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
29
40
  import {
30
- assertDeploymentInitialized,
31
41
  buildProvisioningSummary,
32
42
  cleanupDestroyedState,
33
43
  createBranchPreviewDeployTarget,
34
44
  createPersistentDeployTarget,
35
- destroyCloudflareResources,
36
45
  destroyTreeseedEnvironmentResources,
37
- ensureGeneratedWranglerConfig,
38
- finalizeDeploymentState,
39
46
  loadDeployState,
40
47
  recordHostedDeploymentState,
41
- runRemoteD1Migrations,
42
- validateDeployPrerequisites,
48
+ resolveConfiguredSurfaceDomain,
43
49
  validateDestroyPrerequisites
44
50
  } from "../operations/services/deploy.js";
45
51
  import {
@@ -48,7 +54,6 @@ import {
48
54
  assertFeatureBranch,
49
55
  branchExists,
50
56
  checkoutBranch,
51
- checkoutDetachedOriginBranch,
52
57
  checkoutTaskBranchFromStaging,
53
58
  createDeprecatedTaskTag,
54
59
  deleteLocalBranch,
@@ -57,35 +62,36 @@ import {
57
62
  gitWorkflowRoot,
58
63
  headCommit,
59
64
  listTaskBranches,
60
- mergeBranchIntoTarget,
61
- prepareReleaseBranches,
65
+ mergeBranchDownIntoFeature,
62
66
  PRODUCTION_BRANCH,
67
+ promoteCommitToBranchWithExpectedHead,
63
68
  pushBranch,
64
- pushHeadToBranch,
65
69
  reattachDetachedHeadIfSafe,
70
+ remoteHeadCommit,
66
71
  remoteBranchExists,
67
72
  STAGING_BRANCH,
68
- squashMergeBranchIntoStaging,
69
- syncBranchWithOrigin
73
+ syncBranchWithOrigin,
74
+ waitForStagingAutomation
70
75
  } from "../operations/services/git-workflow.js";
71
76
  import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
72
- import { dispatchGitHubWorkflowRun } from "../operations/services/github-api.js";
77
+ import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
73
78
  import {
74
79
  formatGitHubActionsGateFailure,
75
80
  inspectGitHubActionsVerification,
76
81
  skippedGitHubActionsGate,
77
82
  waitForGitHubActionsGate
78
83
  } from "../operations/services/github-actions-verification.js";
79
- import {
80
- runReleaseCandidateGate
81
- } 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";
82
88
  import {
83
89
  collectReleaseHistoryCommits,
84
90
  renderAdministrativeCommitMessage,
85
91
  upsertReleaseChangelog
86
92
  } from "../operations/services/release-history.js";
87
- import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
88
- 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";
89
95
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
90
96
  import {
91
97
  collectMergeConflictReport,
@@ -102,18 +108,14 @@ import {
102
108
  } from "../operations/services/workspace-save.js";
103
109
  import {
104
110
  planRepositorySave,
105
- refreshAndValidateRootWorkspaceLockfileForSave,
106
111
  repositorySaveErrorDetails,
107
112
  runRepositorySaveOrchestrator
108
113
  } from "../operations/services/repository-save-orchestrator.js";
114
+ import { discoverTreeseedPackageAdapters } from "../operations/services/package-adapters.js";
109
115
  import {
110
- assertNoInternalDevReferences,
111
- cleanupStaleTreeseedDevTags,
112
- collectTreeseedDevTagCleanupPlan,
113
116
  collectInternalDevReferenceIssues,
114
- devTagFromDependencySpec,
115
- normalizeGitRemoteForManifest,
116
- rewriteProjectInternalDependenciesToStableVersions
117
+ installableInternalDependencyVersions,
118
+ normalizeGitRemoteForManifest
117
119
  } from "../operations/services/package-reference-policy.js";
118
120
  import {
119
121
  ensureLocalWorkspaceLinks,
@@ -121,7 +123,6 @@ import {
121
123
  unlinkLocalWorkspaceLinks
122
124
  } from "../operations/services/workspace-dependency-mode.js";
123
125
  import {
124
- hasCompleteTreeseedPackageCheckout,
125
126
  changedWorkspacePackages,
126
127
  publishableWorkspacePackages,
127
128
  run,
@@ -129,9 +130,13 @@ import {
129
130
  workspacePackages,
130
131
  workspaceRoot
131
132
  } from "../operations/services/workspace-tools.js";
132
- import { runTreeseedHostingAudit } from "../operations/services/hosting-audit.js";
133
+ import { classifyTreeseedGitMode, runTreeseedGit, runTreeseedGitOk, runTreeseedGitText } from "../operations/services/git-runner.js";
134
+ import { collectTreeseedDeploymentReadiness } from "../operations/services/deployment-readiness.js";
135
+ import { collectTreeseedLiveHostedServiceChecks } from "../operations/services/live-hosted-service-checks.js";
136
+ import { discoverTreeseedApplications } from "../hosting/apps.js";
137
+ import { compileTreeseedHostingGraph } from "../hosting/graph.js";
133
138
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
134
- import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, filterTreeseedDesiredUnitsByBootstrapSystems, planTreeseedReconciliation, resolveTreeseedBootstrapSelection, reconcileTreeseedTarget } from "../reconcile/index.js";
139
+ import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, destroyTreeseedTargetUnits, filterTreeseedDesiredUnitsByBootstrapSystems, resolveTreeseedBootstrapSelection } from "../reconcile/index.js";
135
140
  import {
136
141
  acquireWorkflowLock,
137
142
  archiveWorkflowRun,
@@ -153,6 +158,7 @@ import {
153
158
  checkedOutWorkspacePackageRepos,
154
159
  resolveTreeseedWorkflowSession
155
160
  } from "./session.js";
161
+ import { checkedOutManagedWorkflowRepos } from "../operations/services/managed-repositories.js";
156
162
  import {
157
163
  classifyTreeseedBranchRole,
158
164
  resolveTreeseedWorkflowPaths
@@ -184,6 +190,14 @@ function defaultWrite(output, stream = "stdout") {
184
190
  (stream === "stderr" ? process.stderr : process.stdout).write(`${output}
185
191
  `);
186
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
+ }
187
201
  function shouldManageWorkspaceLinks(mode, env = process.env) {
188
202
  if (mode === "off") return false;
189
203
  const envMode = String(env?.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
@@ -214,8 +228,10 @@ function readPackageScript(root, packageDir, scriptName) {
214
228
  function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
215
229
  const packages = [
216
230
  { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
217
- { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/worker.js"] },
231
+ { name: "@treeseed/ui", dir: "packages/ui", artifacts: ["dist/index.js"] },
232
+ { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/manager.js", "dist/provider/runner.js"] },
218
233
  { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
234
+ { name: "@treeseed/admin", dir: "packages/admin", artifacts: ["dist/plugin.js"] },
219
235
  { name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
220
236
  ];
221
237
  for (const entry of packages) {
@@ -257,7 +273,7 @@ function ensureWorkflowCommandBins(root, helpers) {
257
273
  }
258
274
  }
259
275
  function unresolvedMergePaths(repoDir) {
260
- 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);
261
277
  }
262
278
  function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
263
279
  const gitRoot = repoRoot(root);
@@ -278,7 +294,7 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
278
294
  const entries = [];
279
295
  for (const pkg of packages) {
280
296
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
281
- run("git", ["add", pkg.repoPath], { cwd: gitRoot });
297
+ runGit(["add", pkg.repoPath], { cwd: gitRoot });
282
298
  entries.push({
283
299
  packageName: pkg.name,
284
300
  path: pkg.repoPath,
@@ -307,9 +323,14 @@ function normalizeCiMode(mode, operation) {
307
323
  if (mode === "hosted" || mode === "off") return mode;
308
324
  return operation === "save" ? "off" : "hosted";
309
325
  }
310
- function normalizeSaveCiMode(mode, branch) {
326
+ function normalizeSaveLane(lane) {
327
+ const value = lane ?? process.env.TREESEED_SAVE_LANE;
328
+ return value === "promotion" ? "promotion" : "fast";
329
+ }
330
+ function normalizeSaveCiMode(mode, branch, lane = "fast") {
311
331
  if (mode === "hosted" || mode === "off") return mode;
312
- return branch === STAGING_BRANCH ? "hosted" : "off";
332
+ if (lane === "promotion") return branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH ? "hosted" : "off";
333
+ return "off";
313
334
  }
314
335
  function normalizeSaveVerifyMode(mode) {
315
336
  switch (mode) {
@@ -329,8 +350,16 @@ function normalizeSaveVerifyMode(mode) {
329
350
  return "skip";
330
351
  }
331
352
  }
332
- function shouldUseHostedSaveCi(input, branch) {
333
- return normalizeSaveCiMode(input.ciMode, branch) === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
353
+ function normalizeReleaseCandidateMode(mode, operation, lane = "fast") {
354
+ const value = mode ?? process.env.TREESEED_RELEASE_CANDIDATE_MODE;
355
+ if (value === "hybrid" || value === "strict" || value === "skip") {
356
+ return value;
357
+ }
358
+ if (operation === "save" && lane === "promotion") return "strict";
359
+ return operation === "save" ? "hybrid" : "strict";
360
+ }
361
+ function shouldUseHostedSaveCi(input, branch, lane = normalizeSaveLane(input.lane)) {
362
+ return normalizeSaveCiMode(input.ciMode, branch, lane) === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
334
363
  }
335
364
  function worktreePayload(root, requestedMode) {
336
365
  const metadata = managedWorkflowWorktreeMetadata(root);
@@ -354,35 +383,27 @@ function helpersForCwd(helpers, cwd) {
354
383
  function shouldDispatchSwitchToManagedWorktree(root, input, env) {
355
384
  return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
356
385
  }
357
- function assertHostedGitHubWorkflowAuthReady(operation, root) {
358
- const tools = collectTreeseedToolStatus({
359
- tenantRoot: root,
360
- env: process.env
361
- });
362
- const github = tools.auth.github;
363
- if (!github.authenticated) {
364
- const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
365
- workflowError(
366
- operation,
367
- "github_auth_unavailable",
368
- [
369
- "Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
370
- github.detail,
371
- `Managed gh check: ${command}`,
372
- "Remediation:",
373
- ...github.remediation.map((item) => `- ${item}`)
374
- ].join("\n"),
375
- {
376
- details: {
377
- toolsHome: tools.toolsHome,
378
- ghConfigDir: tools.ghConfigDir,
379
- github,
380
- tools: tools.tools
381
- }
382
- }
383
- );
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
+ }
384
396
  }
385
- 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
+ );
386
407
  }
387
408
  async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
388
409
  if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
@@ -391,41 +412,49 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
391
412
  if (gates.length === 0) {
392
413
  return [];
393
414
  }
394
- 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);
395
419
  const results = [];
396
420
  for (const gate of gates) {
421
+ const gateWithTimeout = {
422
+ ...gate,
423
+ timeoutSeconds: gate.timeoutSeconds ?? HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS
424
+ };
397
425
  if (options.root && options.runId) {
398
426
  const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
399
- repository: gate.repository ?? null,
400
- workflow: gate.workflow,
401
- headSha: gate.headSha,
402
- branch: gate.branch
427
+ repository: gateWithTimeout.repository ?? null,
428
+ workflow: gateWithTimeout.workflow,
429
+ headSha: gateWithTimeout.headSha,
430
+ branch: gateWithTimeout.branch
403
431
  });
404
432
  if (cached) {
405
433
  results.push({
406
434
  ...cached.result,
407
- name: gate.name,
435
+ name: gateWithTimeout.name,
408
436
  cached: true
409
437
  });
410
438
  continue;
411
439
  }
412
440
  }
413
- const result = await waitForGitHubActionsGate(gate, {
441
+ const result = await waitForGitHubActionsGate(gateWithTimeout, {
414
442
  operation,
443
+ env: githubWorkflowGateEnv(options.root, gateWithTimeout),
415
444
  onProgress: options.onProgress
416
445
  });
417
446
  const normalized = {
418
- name: gate.name,
447
+ name: gateWithTimeout.name,
419
448
  ...result,
420
- workflow: String(result.workflow ?? gate.workflow),
421
- branch: String(result.branch ?? gate.branch),
422
- headSha: String(result.headSha ?? gate.headSha),
423
- 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,
424
453
  cached: false
425
454
  };
426
455
  if (normalized.status === "completed" && normalized.conclusion !== "success") {
427
- workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gate, normalized), {
428
- details: { gate, workflow: normalized }
456
+ workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gateWithTimeout, normalized), {
457
+ details: { gate: gateWithTimeout, workflow: normalized }
429
458
  });
430
459
  }
431
460
  if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
@@ -435,13 +464,146 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
435
464
  }
436
465
  return results;
437
466
  }
467
+ function githubWorkflowGateEnv(root, gate) {
468
+ if (!root) return process.env;
469
+ try {
470
+ const repository = gate.repository ?? resolveGitHubRepositorySlug(gate.repoPath);
471
+ const scope = gate.branch === PRODUCTION_BRANCH ? "prod" : "staging";
472
+ const values = resolveTreeseedMachineEnvironmentValues(root, scope);
473
+ const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
474
+ if (!credential.token) return process.env;
475
+ return {
476
+ ...process.env,
477
+ TREESEED_GITHUB_TOKEN: credential.token,
478
+ GH_TOKEN: credential.token,
479
+ GITHUB_TOKEN: credential.token
480
+ };
481
+ } catch {
482
+ return process.env;
483
+ }
484
+ }
438
485
  const HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
486
+ const HOSTED_WORKFLOW_GATE_TIMEOUT_SECONDS = 45 * 60;
439
487
  function hostedDeployGate(gate) {
440
488
  return {
441
489
  ...gate,
442
490
  timeoutSeconds: gate.timeoutSeconds ?? HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS
443
491
  };
444
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
+ }
445
607
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
446
608
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
447
609
  const releaseRecord = stringRecord(rootRelease) ?? {};
@@ -507,6 +669,22 @@ ${missing.map((check) => `${check.id}: ${check.path}`).join("\n")}`, {
507
669
  }
508
670
  return report;
509
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
+ }
510
688
  function workflowError(operation, code, message, options = {}) {
511
689
  throw new TreeseedWorkflowError(operation, code, message, options);
512
690
  }
@@ -609,6 +787,9 @@ function createWorkspaceRootRepoReport(root) {
609
787
  function createWorkspacePackageReports(root) {
610
788
  return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
611
789
  }
790
+ function createManagedWorkflowRepoReports(root) {
791
+ return checkedOutManagedWorkflowRepos(root).map((repo) => createRepoReport(repo.name, repo.dir, currentBranch(repo.dir) || null, hasMeaningfulChanges(repo.dir)));
792
+ }
612
793
  function findReportByName(reports, name) {
613
794
  return reports.find((report) => report.name === name) ?? null;
614
795
  }
@@ -616,15 +797,20 @@ function findReportByPath(reports, path) {
616
797
  return reports.find((report) => report.path === path) ?? null;
617
798
  }
618
799
  function assertWorkspaceClean(root) {
619
- const repoDirs = [repoRoot(root), ...checkedOutWorkspacePackageRepos(root).map((pkg) => pkg.dir)];
800
+ const repoDirs = [repoRoot(root), ...checkedOutManagedWorkflowRepos(root).map((repo) => repo.dir)];
620
801
  assertCleanWorktrees(repoDirs);
621
802
  return repoDirs;
622
803
  }
623
804
  function buildWorkflowResult(operation, cwd, payload, options = {}) {
805
+ const timing = options.timing ?? createTreeseedWorkflowTimer().finish();
624
806
  const resolvedPayload = options.includeFinalState ?? true ? {
625
807
  ...payload,
808
+ timing,
626
809
  finalState: resolveWorkflowStateSnapshot(cwd)
627
- } : payload;
810
+ } : {
811
+ ...payload,
812
+ timing
813
+ };
628
814
  return {
629
815
  schemaVersion: 1,
630
816
  kind: "treeseed.workflow.result",
@@ -642,31 +828,108 @@ function buildWorkflowResult(operation, cwd, payload, options = {}) {
642
828
  errors: options.errors ?? []
643
829
  };
644
830
  }
645
- async function runReadOnlyHostingAuditForWorkflow(operation, root, helpers, environment, options = { enabled: true }) {
646
- if (!options.enabled) {
647
- return null;
831
+ function parseGitStatusChangedPaths(status) {
832
+ return status.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => {
833
+ const value = line.slice(3).trim();
834
+ return value.includes(" -> ") ? value.split(" -> ").at(-1).trim() : value;
835
+ }).filter(Boolean);
836
+ }
837
+ function availableWorkflowAppIds(root) {
838
+ try {
839
+ const ids = discoverTreeseedApplications(root).map((app) => app.id);
840
+ return ids.length > 0 ? ids : ["web"];
841
+ } catch {
842
+ return ["web"];
843
+ }
844
+ }
845
+ function selectWorkflowApplications(root, input = {}) {
846
+ const available = availableWorkflowAppIds(root);
847
+ const availableSet = new Set(available);
848
+ const selected = /* @__PURE__ */ new Set();
849
+ const reasons = [];
850
+ const add = (appId, reason) => {
851
+ if (!availableSet.has(appId)) return;
852
+ selected.add(appId);
853
+ reasons.push({ appId, reason });
854
+ };
855
+ const packages = [
856
+ ...input.packageSelection?.selected ?? [],
857
+ ...input.packageSelection?.changed ?? [],
858
+ ...input.packageSelection?.dependents ?? []
859
+ ];
860
+ const appByPackage = new Map(discoverTreeseedApplications(root).filter((app) => app.relativeRoot.startsWith("packages/")).map((app) => [`@treeseed/${app.relativeRoot.slice("packages/".length).split("/")[0]}`, app.id]));
861
+ for (const packageName of packages) {
862
+ if (packageName === "@treeseed/api" || packageName === "@treeseed/treedx" || packageName === "@treeseed/agent") {
863
+ add("api", `${packageName} changed`);
864
+ } else if (packageName === "@treeseed/core" || packageName === "@treeseed/ui" || packageName === "@treeseed/admin") {
865
+ add("web", `${packageName} changed`);
866
+ } else if (packageName === "@treeseed/sdk" || packageName === "@treeseed/cli") {
867
+ add("web", `${packageName} is shared`);
868
+ add("api", `${packageName} is shared`);
869
+ }
870
+ const packageAppId = appByPackage.get(packageName);
871
+ if (packageAppId) add(packageAppId, `${packageName} owns ${packageAppId}`);
872
+ }
873
+ const changedPaths = input.changedPaths ?? parseGitStatusChangedPaths(gitStatusPorcelain(root));
874
+ const appByPackagePath = new Map(discoverTreeseedApplications(root).filter((app) => app.relativeRoot.startsWith("packages/")).map((app) => [app.relativeRoot, app.id]));
875
+ for (const file of changedPaths) {
876
+ if (file.startsWith("packages/api/") || file === "packages/api") {
877
+ add("api", `${file} is API-owned`);
878
+ } else if (file.startsWith("packages/treedx/") || file === "packages/treedx") {
879
+ add("api", `${file} is TreeDX implementation`);
880
+ } else if (file.startsWith("packages/core/") || file.startsWith("packages/ui/") || file.startsWith("packages/admin/") || file.startsWith("src/") || file.startsWith("content/") || file.startsWith("public/") || file === "treeseed.site.yaml") {
881
+ add("web", `${file} is web-owned`);
882
+ } else if (file.startsWith("packages/sdk/") || file.startsWith("packages/cli/") || file === "package.json" || file === "package-lock.json" || file.startsWith(".github/")) {
883
+ add("web", `${file} is shared workflow/config`);
884
+ add("api", `${file} is shared workflow/config`);
885
+ }
886
+ for (const [packageRoot, appId] of appByPackagePath) {
887
+ if (file === packageRoot || file.startsWith(`${packageRoot}/`)) {
888
+ add(appId, `${file} is ${appId}-owned`);
889
+ }
890
+ }
648
891
  }
649
- helpers.write(`[workflow][hosting-audit] Running read-only hosting audit for ${environment}.`);
650
- const report = await runTreeseedHostingAudit({
892
+ const source = packages.length > 0 ? "package-selection" : changedPaths.length > 0 ? "changed-paths" : "default";
893
+ const finalSelected = selected.size > 0 ? available.filter((appId) => selected.has(appId)) : available;
894
+ return {
895
+ selected: finalSelected,
896
+ skipped: available.filter((appId) => !finalSelected.includes(appId)).map((appId) => ({ appId, reason: "No changed files or selected packages target this application." })),
897
+ reasons,
898
+ source
899
+ };
900
+ }
901
+ function singleSelectedWorkflowAppId(selection) {
902
+ return selection.selected.length === 1 ? selection.selected[0] : void 0;
903
+ }
904
+ async function workflowHostedVerificationGateRequired(operation, root, helpers, environment, options = { enabled: true }) {
905
+ if (!options.enabled) return null;
906
+ const target = environment === "prod" ? "prod" : environment === "local" ? "local" : "staging";
907
+ const readiness = collectTreeseedDeploymentReadiness({
651
908
  tenantRoot: root,
652
- environment,
653
- repair: false,
654
- env: helpers.context.env,
655
- write: (line) => helpers.write(line)
909
+ environment: target,
910
+ appId: options.appId
656
911
  });
657
- if (options.strict && !report.ok) {
658
- workflowError(operation, "validation_failed", `Hosting audit failed for ${report.environment}: ${report.blockers.join("\n")}`, {
659
- details: { hostingAudit: report }
912
+ if (options.strict && !readiness.ok) {
913
+ const failures = readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
914
+ workflowError(operation, "validation_failed", `Deployment readiness failed for ${target}:
915
+ ${failures.join("\n")}`, {
916
+ details: { readiness }
660
917
  });
661
918
  }
662
- return report;
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
+ }
925
+ });
663
926
  }
664
927
  function normalizeExecutionMode(input) {
665
928
  return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
666
929
  }
667
930
  function submodulePointerForRef(repoDir, ref, relativeDir) {
668
931
  try {
669
- 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();
670
933
  if (!output) {
671
934
  return null;
672
935
  }
@@ -710,7 +973,8 @@ function writeJsonFile(path, value) {
710
973
  `, "utf8");
711
974
  }
712
975
  function applyStableWorkspaceVersionChanges(root, versions) {
713
- const stableGitReferences = stablePackageGitReferences(root, versions);
976
+ const dependencyVersions = installableInternalDependencyVersions(root, versions);
977
+ const stableGitReferences = stablePackageGitReferences(root, dependencyVersions);
714
978
  for (const target of [{ name: "@treeseed/market", dir: root }, ...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))]) {
715
979
  const packageJsonPath = resolve(target.dir, "package.json");
716
980
  if (!existsSync(packageJsonPath)) continue;
@@ -724,7 +988,7 @@ function applyStableWorkspaceVersionChanges(root, versions) {
724
988
  for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
725
989
  const values = packageJson[field];
726
990
  if (!values || typeof values !== "object" || Array.isArray(values)) continue;
727
- for (const [dependencyName, version] of versions.entries()) {
991
+ for (const [dependencyName, version] of dependencyVersions.entries()) {
728
992
  if (!(dependencyName in values)) continue;
729
993
  const dependencySpec = stableGitReferences.get(dependencyName) ?? version;
730
994
  if (String(values[dependencyName]) === dependencySpec) continue;
@@ -753,13 +1017,13 @@ function stablePackageGitReferences(root, versions) {
753
1017
  }
754
1018
  function gitObjectCommit(repoDir, ref) {
755
1019
  try {
756
- 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;
757
1021
  } catch {
758
1022
  return null;
759
1023
  }
760
1024
  }
761
1025
  function remoteTagCommit(repoDir, tagName) {
762
- 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();
763
1027
  if (!output) return null;
764
1028
  const peeled = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
765
1029
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
@@ -771,14 +1035,14 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
771
1035
  throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
772
1036
  }
773
1037
  if (!localCommit) {
774
- run("git", ["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
1038
+ runGit(["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
775
1039
  }
776
1040
  const remoteCommit = remoteTagCommit(repoDir, tagName);
777
1041
  if (remoteCommit && remoteCommit !== commitSha) {
778
1042
  throw new Error(`Release tag ${tagName} already exists on origin at ${remoteCommit}, expected ${commitSha}.`);
779
1043
  }
780
1044
  if (!remoteCommit) {
781
- run("git", ["push", "origin", tagName], { cwd: repoDir });
1045
+ runGit(["push", "origin", tagName], { cwd: repoDir });
782
1046
  }
783
1047
  return {
784
1048
  tagName,
@@ -786,12 +1050,28 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
786
1050
  remote: remoteCommit ? "existing" : "pushed"
787
1051
  };
788
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
+ }
789
1069
  function commitAllIfChanged(repoDir, message) {
790
- run("git", ["add", "-A"], { cwd: repoDir });
1070
+ runGit(["add", "-A"], { cwd: repoDir });
791
1071
  if (!hasMeaningfulChanges(repoDir)) {
792
1072
  return { committed: false, commitSha: headCommit(repoDir) };
793
1073
  }
794
- run("git", ["commit", "-m", message], { cwd: repoDir });
1074
+ runGit(["commit", "-m", message], { cwd: repoDir });
795
1075
  return { committed: true, commitSha: headCommit(repoDir) };
796
1076
  }
797
1077
  function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
@@ -869,6 +1149,11 @@ function defaultCiWorkflows(kind, branch) {
869
1149
  }
870
1150
  return ["verify.yml"];
871
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
+ }
872
1157
  function githubRepositoryForRepo(repoDir) {
873
1158
  try {
874
1159
  return resolveGitHubRepositorySlug(repoDir);
@@ -878,7 +1163,7 @@ function githubRepositoryForRepo(repoDir) {
878
1163
  }
879
1164
  function ciTargetForRepo(repo, kind, input, workflowOverrides) {
880
1165
  const branch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : repo.branchName;
881
- 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);
882
1167
  return {
883
1168
  name: repo.name,
884
1169
  repoPath: repo.path,
@@ -931,7 +1216,7 @@ async function createCiResult(cwd, input) {
931
1216
  function createTasksResult(cwd) {
932
1217
  const tenantRoot = cwd;
933
1218
  const repoDir = gitWorkflowRoot(tenantRoot);
934
- const deployConfig = loadCliDeployConfig(tenantRoot);
1219
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
935
1220
  const dirty = gitStatusPorcelain(repoDir).length > 0;
936
1221
  const tasks = listTaskBranches(repoDir).map((branch) => {
937
1222
  const previewState = loadDeployState(tenantRoot, deployConfig, {
@@ -1064,7 +1349,7 @@ async function connectTreeseedMarketProject(helpers, tenantRoot, input, context)
1064
1349
  principal: activeRemoteSession?.principal ?? null
1065
1350
  });
1066
1351
  }
1067
- const runnerHostId = `market-runner:${projectId}`;
1352
+ const runnerHostId = `operations-runner:${projectId}`;
1068
1353
  if (connectionResult.runnerToken) {
1069
1354
  setTreeseedRemoteSession(tenantRoot, {
1070
1355
  hostId: runnerHostId,
@@ -1180,16 +1465,61 @@ function findAutoResumableSaveRun(root, branch) {
1180
1465
  if (branch === STAGING_BRANCH && (hasMeaningfulChanges(repoRoot(root)) || checkedOutWorkspacePackageRepos(root).some((repo) => hasMeaningfulChanges(repo.dir)))) {
1181
1466
  return null;
1182
1467
  }
1183
- 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;
1494
+ }
1495
+ function workflowFileExists(repoPath, workflow) {
1496
+ return existsSync(resolve(repoPath, ".github", "workflows", workflow));
1497
+ }
1498
+ function hostedWorkflowForSavedRepository(root, repo) {
1499
+ const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
1500
+ const adapterWorkflow = packageHostedVerifyWorkflow(adapterByPath.get(resolve(repo.path)));
1501
+ if (adapterWorkflow) return adapterWorkflow;
1502
+ if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
1503
+ return "deploy.yml";
1504
+ }
1505
+ return "verify.yml";
1184
1506
  }
1185
- function gatesForSavedPackageReports(reports) {
1186
- return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
1187
- name: repo.name,
1188
- repoPath: repo.path,
1189
- workflow: "verify.yml",
1190
- branch: String(repo.branch),
1191
- headSha: String(repo.commitSha)
1192
- }));
1507
+ function gatesForSavedRepositoryReports(root, reports) {
1508
+ return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch && (repo.committed || repo.tagName)).map((repo) => {
1509
+ const workflow = hostedWorkflowForSavedRepository(root, repo);
1510
+ const gate = {
1511
+ name: repo.name,
1512
+ repoPath: repo.path,
1513
+ workflow,
1514
+ branch: String(repo.branch),
1515
+ headSha: String(repo.commitSha)
1516
+ };
1517
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
1518
+ });
1519
+ }
1520
+ function packageHostedVerifyWorkflow(adapter) {
1521
+ const workflow = adapter?.metadata?.hostedVerifyWorkflow;
1522
+ return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
1193
1523
  }
1194
1524
  function gateForSavedRootReport(report, branch, scope) {
1195
1525
  if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
@@ -1254,7 +1584,7 @@ function generatedReleaseMetadataFiles(repoDir) {
1254
1584
  return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
1255
1585
  if (existsSync(resolve(repoDir, filePath))) return true;
1256
1586
  try {
1257
- run("git", ["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
1587
+ runGit(["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
1258
1588
  return true;
1259
1589
  } catch {
1260
1590
  return false;
@@ -1283,12 +1613,12 @@ function restoreReleaseGeneratedMetadata(repo) {
1283
1613
  const restored = [];
1284
1614
  const skipped = [];
1285
1615
  for (const filePath of repo.files) {
1286
- const status = run("git", ["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
1616
+ const status = runGit(["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
1287
1617
  if (!status.trim()) {
1288
1618
  skipped.push(filePath);
1289
1619
  continue;
1290
1620
  }
1291
- run("git", ["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
1621
+ runGit(["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
1292
1622
  restored.push(filePath);
1293
1623
  }
1294
1624
  return { restored, skipped };
@@ -1525,7 +1855,12 @@ function failWorkflowRun(root, runId, error, recovery) {
1525
1855
  releaseWorkflowLock(root, runId);
1526
1856
  }
1527
1857
  function validatePackageReleaseWorkflows(root, packageNames) {
1528
- 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);
1529
1864
  if (missing.length > 0) {
1530
1865
  workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
1531
1866
  details: {
@@ -1534,6 +1869,26 @@ function validatePackageReleaseWorkflows(root, packageNames) {
1534
1869
  });
1535
1870
  }
1536
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
+ }
1537
1892
  function validateStagingWorkflowContracts(root) {
1538
1893
  if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
1539
1894
  return;
@@ -1553,17 +1908,38 @@ function validateStagingWorkflowContracts(root) {
1553
1908
  function shouldSkipReleaseInstall() {
1554
1909
  return process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
1555
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
+ }
1556
1925
  function runReleaseNpmInstall(repoDir, options = {}) {
1557
1926
  if (shouldSkipReleaseInstall()) {
1558
1927
  return { status: "skipped", reason: "disabled" };
1559
1928
  }
1560
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"];
1561
- const result = spawnSync("npm", args, {
1930
+ const spawnCommand = npmCommandForWorkflowSpawn(args);
1931
+ const result = spawnSync(spawnCommand.command, spawnCommand.args, {
1562
1932
  cwd: repoDir,
1563
1933
  env: {
1564
1934
  ...process.env,
1565
1935
  npm_config_audit: "false",
1566
- 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"
1567
1943
  },
1568
1944
  stdio: "pipe",
1569
1945
  encoding: "utf8"
@@ -1598,7 +1974,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1598
1974
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1599
1975
  checkoutBranch(repoDir, STAGING_BRANCH);
1600
1976
  try {
1601
- 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 });
1602
1978
  return {
1603
1979
  status: "up-to-date",
1604
1980
  merged: false,
@@ -1610,7 +1986,7 @@ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1610
1986
  } catch {
1611
1987
  }
1612
1988
  try {
1613
- 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 });
1614
1990
  } catch (error) {
1615
1991
  const report = collectMergeConflictReport(repoDir);
1616
1992
  throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
@@ -1663,69 +2039,6 @@ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, optio
1663
2039
  packageStagingPointerCommit: pointerSync.commitSha
1664
2040
  };
1665
2041
  }
1666
- function collectActiveDevTagReferences(root) {
1667
- return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
1668
- }
1669
- function normalizeIncludePackages(value) {
1670
- if (!value) return null;
1671
- const values = Array.isArray(value) ? value : String(value).split(",");
1672
- const normalized = values.map((entry) => String(entry).trim()).filter(Boolean);
1673
- return normalized.length > 0 ? new Set(normalized) : null;
1674
- }
1675
- function normalizeDevTagBranchScope(value) {
1676
- return value === "staging" || value === "preview" || value === "all" ? value : "all";
1677
- }
1678
- function packageJsonVersion(repoDir) {
1679
- const packageJson = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
1680
- return String(packageJson.version ?? "");
1681
- }
1682
- function collectStaleDevTagCleanupReports(root, input, options) {
1683
- const includePackages = normalizeIncludePackages(input.includePackages);
1684
- const branchScope = normalizeDevTagBranchScope(input.branchScope);
1685
- const activeDevTags = collectActiveDevTagReferences(root);
1686
- const repos = checkedOutWorkspacePackageRepos(root).filter((pkg) => !includePackages || includePackages.has(pkg.name)).map((pkg) => {
1687
- const currentVersion = packageJsonVersion(pkg.dir);
1688
- const report = options.execute ? cleanupStaleTreeseedDevTags({
1689
- repoDir: pkg.dir,
1690
- packageName: pkg.name,
1691
- currentVersion,
1692
- activeReferences: activeDevTags,
1693
- branchScope
1694
- }) : {
1695
- ...collectTreeseedDevTagCleanupPlan({
1696
- repoDir: pkg.dir,
1697
- packageName: pkg.name,
1698
- currentVersion,
1699
- activeReferences: activeDevTags,
1700
- branchScope
1701
- }),
1702
- status: "planned",
1703
- candidateCount: 0,
1704
- cleaned: [],
1705
- cleanedCount: 0,
1706
- skippedCount: 0
1707
- };
1708
- if (!options.execute) {
1709
- const planned = report;
1710
- planned.candidateCount = planned.candidates.length;
1711
- planned.skippedCount = planned.skipped.length;
1712
- }
1713
- return {
1714
- name: pkg.name,
1715
- path: relative(root, pkg.dir),
1716
- ...report
1717
- };
1718
- });
1719
- return {
1720
- status: options.execute ? "completed" : "planned",
1721
- branchScope,
1722
- includePackages: includePackages ? [...includePackages].sort() : [],
1723
- repos,
1724
- candidateCount: repos.reduce((total, repo) => total + Number(repo.candidateCount ?? 0), 0),
1725
- cleanedCount: repos.reduce((total, repo) => total + Number(repo.cleanedCount ?? 0), 0),
1726
- skippedCount: repos.reduce((total, repo) => total + Number(repo.skippedCount ?? 0), 0)
1727
- };
1728
- }
1729
2042
  function releasePlanVersionMap(plannedVersions) {
1730
2043
  return new Map(
1731
2044
  Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
@@ -1757,36 +2070,233 @@ function stableDependencyVersionsForReleaseLine(root, options) {
1757
2070
  }
1758
2071
  return versions;
1759
2072
  }
1760
- function assertReleaseCandidatePassed(operation, report) {
1761
- if (report.status === "passed") {
1762
- return;
1763
- }
1764
- const rendered = report.failures.map((failure) => `- ${failure.scope}${failure.provider ? ` ${failure.provider}` : ""}: ${failure.message}`);
1765
- workflowError(operation, "validation_failed", [
1766
- "Treeseed release-candidate readiness failed.",
1767
- ...rendered
1768
- ].join("\n"), {
1769
- details: {
1770
- releaseCandidate: report
1771
- }
1772
- });
2073
+ function releaseCandidateProofDriver(mode, lane = "fast") {
2074
+ if (lane === "promotion" || mode === "strict") return "github-hosted";
2075
+ return "local";
1773
2076
  }
1774
- async function runReleaseCandidateForPlan(operation, root, plannedRelease, options = {}) {
1775
- const plannedVersions = plannedRelease.plannedVersions && typeof plannedRelease.plannedVersions === "object" && !Array.isArray(plannedRelease.plannedVersions) ? plannedRelease.plannedVersions : {};
1776
- const stableDependencyVersions = plannedRelease.stableDependencyVersions && typeof plannedRelease.stableDependencyVersions === "object" && !Array.isArray(plannedRelease.stableDependencyVersions) ? plannedRelease.stableDependencyVersions : {};
2077
+ async function runReleaseCandidateProofForPlan(operation, root, plannedRelease, options = {}) {
1777
2078
  const packageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
1778
- 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({
1779
2082
  root,
1780
- plannedVersions: { ...stableDependencyVersions, ...plannedVersions },
1781
- selectedPackageNames: packageSelection.selected,
1782
- allowReuse: options.allowReuse
2083
+ target: operation === "release" ? "prod" : "staging",
2084
+ driver,
2085
+ write: options.write
1783
2086
  });
1784
- assertReleaseCandidatePassed(operation, report);
1785
- 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
+ }
1786
2288
  }
1787
2289
  function buildReleasePlanSnapshot(input) {
1788
2290
  const selectedPackageNames = new Set(input.packageSelection.selected);
2291
+ const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
1789
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
+ }
1790
2300
  const plannedSelected = [...versionPlan.selected].filter((name) => versionPlan.versions.has(name));
1791
2301
  const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(input.packageSelection.changed.filter((name) => plannedSelected.includes(name))));
1792
2302
  const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
@@ -1821,10 +2331,11 @@ function buildReleasePlanSnapshot(input) {
1821
2331
  packageSelection: plannedPackageSelection,
1822
2332
  plannedVersions,
1823
2333
  stableDependencyVersions,
2334
+ applicationSelection,
1824
2335
  plannedDevReferenceRewrites,
1825
2336
  plannedPublishWaits: plannedPackageSelection.selected.map((name) => ({
1826
2337
  name,
1827
- workflow: "publish.yml",
2338
+ workflow: releaseWorkflowForPackage(input.root, name),
1828
2339
  branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
1829
2340
  status: "planned"
1830
2341
  })),
@@ -1843,7 +2354,6 @@ function buildReleasePlanSnapshot(input) {
1843
2354
  })),
1844
2355
  { id: "release-root", description: `Release market ${rootVersion}` },
1845
2356
  { id: "release-back-merge", description: "Back-merge production release history into staging" },
1846
- { id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
1847
2357
  { id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
1848
2358
  ],
1849
2359
  blockers: input.blockers
@@ -1884,10 +2394,26 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames, ciMode)
1884
2394
  if (ciMode === "off") {
1885
2395
  return;
1886
2396
  }
1887
- assertHostedGitHubWorkflowAuthReady("release", root);
2397
+ const values = resolveTreeseedMachineEnvironmentValues(root, "prod");
2398
+ const missing = [];
1888
2399
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1889
2400
  if (!selectedPackageNames.has(pkg.name)) continue;
1890
- 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
+ );
1891
2417
  }
1892
2418
  }
1893
2419
  function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
@@ -1914,31 +2440,31 @@ function assertSessionBranchSafety(operation, session, {
1914
2440
  requireCurrentBranch = false,
1915
2441
  allowPackageReposWithoutOrigin = false
1916
2442
  } = {}) {
1917
- 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);
1918
2444
  if (detached.length > 0) {
1919
- workflowError(operation, "validation_failed", `Detached package heads detected: ${detached.join(", ")}.`, {
2445
+ workflowError(operation, "validation_failed", `Detached managed repository heads detected: ${detached.join(", ")}.`, {
1920
2446
  details: { detached }
1921
2447
  });
1922
2448
  }
1923
2449
  if (requireCleanPackages) {
1924
- 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);
1925
2451
  if (dirty.length > 0) {
1926
- workflowError(operation, "validation_failed", `Dirty package repos block ${operation}: ${dirty.join(", ")}.`, {
2452
+ workflowError(operation, "validation_failed", `Dirty managed repos block ${operation}: ${dirty.join(", ")}.`, {
1927
2453
  details: { dirty }
1928
2454
  });
1929
2455
  }
1930
2456
  }
1931
2457
  if (requireCurrentBranch && session.branchName) {
1932
- 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 }));
1933
2459
  if (missing.length > 0) {
1934
- workflowError(operation, "validation_failed", `Package branch alignment is required for ${operation}.`, {
2460
+ workflowError(operation, "validation_failed", `Managed repository branch alignment is required for ${operation}.`, {
1935
2461
  details: { expectedBranch: session.branchName, repos: missing }
1936
2462
  });
1937
2463
  }
1938
2464
  }
1939
2465
  const missingOriginRepos = [
1940
2466
  session.rootRepo,
1941
- ...allowPackageReposWithoutOrigin ? [] : session.packageRepos
2467
+ ...allowPackageReposWithoutOrigin ? [] : session.managedRepos
1942
2468
  ].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
1943
2469
  if (missingOriginRepos.length > 0 && operation !== "destroy") {
1944
2470
  workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
@@ -1947,7 +2473,7 @@ function assertSessionBranchSafety(operation, session, {
1947
2473
  }
1948
2474
  }
1949
2475
  function previewStateFor(tenantRoot, branchName) {
1950
- const deployConfig = loadCliDeployConfig(tenantRoot);
2476
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: "staging", env: process.env }).deployConfig;
1951
2477
  return loadDeployState(tenantRoot, deployConfig, {
1952
2478
  target: createBranchPreviewDeployTarget(branchName)
1953
2479
  });
@@ -1960,59 +2486,71 @@ function branchPreviewInitialized(tenantRoot, branchName) {
1960
2486
  return false;
1961
2487
  }
1962
2488
  }
1963
- async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
1964
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
1965
- assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
1966
- const target = createBranchPreviewDeployTarget(branchName);
1967
- const existingState = previewStateFor(tenantRoot, branchName);
1968
- if (initialize && !existingState.readiness?.initialized) {
1969
- validateDeployPrerequisites(tenantRoot, { requireRemote: true });
1970
- await reconcileTreeseedTarget({
1971
- tenantRoot,
1972
- target,
1973
- env: { ...process.env, ...context.env ?? {} },
1974
- write: (line) => context.write?.(line)
1975
- });
1976
- runRemoteD1Migrations(tenantRoot, { target });
1977
- } else {
1978
- assertDeploymentInitialized(tenantRoot, { target });
1979
- runRemoteD1Migrations(tenantRoot, { target });
1980
- }
1981
- runTenantDeployPreflight({ cwd: tenantRoot, scope: "staging" });
1982
- const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
1983
- runNodeScript("tenant-build", context, tenantRoot, "tenant build");
1984
- const deployResult = spawnSync(process.execPath, [resolveWranglerBin(), "deploy", "--config", wranglerPath], {
1985
- cwd: tenantRoot,
1986
- env: { ...process.env, ...context.env ?? {} },
1987
- 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
1988
2516
  });
1989
- if ((deployResult.status ?? 1) !== 0) {
1990
- workflowError("switch", "unsupported_state", "Preview deployment failed.", {
1991
- exitCode: deployResult.status ?? 1,
1992
- details: { branchName, wranglerPath }
1993
- });
1994
- }
1995
- const state = finalizeDeploymentState(tenantRoot, { target });
1996
2517
  return {
1997
- initialized: existingState.readiness?.initialized !== true,
1998
- previewUrl: state.lastDeployedUrl ?? null,
1999
- lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null,
2000
- wranglerPath
2518
+ status: input.dryRun || !input.execute ? "planned" : "reconciled",
2519
+ branch: input.branch,
2520
+ initialize: input.initialize === true,
2521
+ reconcile
2001
2522
  };
2002
2523
  }
2003
- function destroyPreviewIfPresent(tenantRoot, branchName) {
2004
- const previewTarget = createBranchPreviewDeployTarget(branchName);
2005
- const deployConfig = loadCliDeployConfig(tenantRoot);
2006
- const previewState = loadDeployState(tenantRoot, deployConfig, { target: previewTarget });
2007
- if (previewState.readiness?.initialized) {
2008
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
2009
- validateDestroyPrerequisites(tenantRoot, { requireRemote: true });
2010
- destroyCloudflareResources(tenantRoot, { target: previewTarget });
2011
- }
2012
- 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
+ });
2013
2549
  return {
2014
- performed: previewState.readiness?.initialized === true,
2015
- state: previewState
2550
+ status: input.dryRun || !input.execute ? "planned" : "destroyed",
2551
+ branch: input.branch,
2552
+ reason: input.reason,
2553
+ reconcile
2016
2554
  };
2017
2555
  }
2018
2556
  function resolveDestroyConfirmation(context, expected, input) {
@@ -2036,8 +2574,8 @@ function resolveDestroyConfirmation(context, expected, input) {
2036
2574
  function syncCurrentBranchToOrigin(operation, repoDir, branch) {
2037
2575
  try {
2038
2576
  if (remoteBranchExists(repoDir, branch)) {
2039
- run("git", ["pull", "--rebase", "origin", branch], { cwd: repoDir });
2040
- run("git", ["push", "origin", branch], { cwd: repoDir });
2577
+ runGit(["pull", "--rebase", "origin", branch], { cwd: repoDir });
2578
+ runGit(["push", "origin", branch], { cwd: repoDir });
2041
2579
  return {
2042
2580
  remoteBranchExisted: true,
2043
2581
  pulledRebase: true,
@@ -2046,7 +2584,7 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
2046
2584
  conflicts: false
2047
2585
  };
2048
2586
  }
2049
- run("git", ["push", "-u", "origin", branch], { cwd: repoDir });
2587
+ runGit(["push", "-u", "origin", branch], { cwd: repoDir });
2050
2588
  return {
2051
2589
  remoteBranchExisted: false,
2052
2590
  pulledRebase: false,
@@ -2095,10 +2633,10 @@ function checkoutOrCreateSaveBranch(repoDir, branch) {
2095
2633
  return branch;
2096
2634
  }
2097
2635
  if (remoteBranchExists(repoDir, branch)) {
2098
- run("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
2636
+ runGit(["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
2099
2637
  return branch;
2100
2638
  }
2101
- run("git", ["checkout", "-b", branch], { cwd: repoDir });
2639
+ runGit(["checkout", "-b", branch], { cwd: repoDir });
2102
2640
  return branch;
2103
2641
  }
2104
2642
  function runPackageVerifyLocal(pkgDir) {
@@ -2108,8 +2646,8 @@ function branchNeedsSync(repoDir, branch) {
2108
2646
  if (!remoteBranchExists(repoDir, branch)) {
2109
2647
  return true;
2110
2648
  }
2111
- const localHead = run("git", ["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
2112
- 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();
2113
2651
  return localHead !== remoteHead;
2114
2652
  }
2115
2653
  function savePackageRepo(report, message, branch, shouldVerify) {
@@ -2119,7 +2657,7 @@ function savePackageRepo(report, message, branch, shouldVerify) {
2119
2657
  const needsSync = branchNeedsSync(report.path, branch);
2120
2658
  if (!report.dirty && !needsSync) {
2121
2659
  report.skippedReason = "clean";
2122
- 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();
2123
2661
  return report;
2124
2662
  }
2125
2663
  if (shouldVerify && report.dirty) {
@@ -2127,11 +2665,11 @@ function savePackageRepo(report, message, branch, shouldVerify) {
2127
2665
  report.verified = true;
2128
2666
  }
2129
2667
  if (report.dirty) {
2130
- run("git", ["add", "-A"], { cwd: report.path });
2131
- run("git", ["commit", "-m", message], { cwd: report.path });
2668
+ runGit(["add", "-A"], { cwd: report.path });
2669
+ runGit(["commit", "-m", message], { cwd: report.path });
2132
2670
  report.committed = true;
2133
2671
  }
2134
- 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();
2135
2673
  const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
2136
2674
  report.pushed = branchSync.pushed === true;
2137
2675
  if (!report.dirty && needsSync) {
@@ -2169,14 +2707,14 @@ function ensureLocalTaskBranch(repoDir, branchName) {
2169
2707
  }
2170
2708
  return true;
2171
2709
  }
2172
- function cleanupTaskBranchReport(report, branchName, message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
2710
+ function cleanupTaskBranchReport(report, branchName, _message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
2173
2711
  if (!ensureLocalTaskBranch(report.path, branchName)) {
2174
2712
  report.skippedReason = "branch-missing";
2175
2713
  return report;
2176
2714
  }
2177
- const deprecatedTag = createDeprecatedTaskTag(report.path, branchName, message);
2178
- report.tagName = deprecatedTag.tagName;
2179
- report.commitSha = deprecatedTag.head;
2715
+ const tag = createDeprecatedTaskTag(report.path, branchName, _message);
2716
+ report.tagName = tag.tagName;
2717
+ report.commitSha = updateHead(report.path);
2180
2718
  report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
2181
2719
  syncBranchWithOrigin(report.path, targetBranch);
2182
2720
  if (deleteBranch) {
@@ -2240,7 +2778,7 @@ function collectReleasePackageSelection(root) {
2240
2778
  };
2241
2779
  }
2242
2780
  function hasStagedChanges(repoDir) {
2243
- 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;
2244
2782
  }
2245
2783
  async function workflowStatus(helpers, input = {}) {
2246
2784
  return withContextEnv(helpers.context.env, async () => {
@@ -2554,6 +3092,7 @@ async function workflowConfig(helpers, input = {}) {
2554
3092
  sync,
2555
3093
  env: helpers.context.env,
2556
3094
  checkConnections: bootstrapOnly || sync !== "none" || scopes.some((scope) => scope !== "local"),
3095
+ initializePersistent: bootstrapOnly,
2557
3096
  systems: bootstrapSystemsInput,
2558
3097
  skipUnavailable,
2559
3098
  bootstrapExecution,
@@ -2661,7 +3200,7 @@ async function workflowSwitch(helpers, input) {
2661
3200
  const mode = session.mode;
2662
3201
  const repoDir = session.gitRoot;
2663
3202
  const rootRepo = createWorkspaceRootRepoReport(root);
2664
- const packageReports = createWorkspacePackageReports(root);
3203
+ const packageReports = createManagedWorkflowRepoReports(root);
2665
3204
  let previewResult = null;
2666
3205
  const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
2667
3206
  if (executionMode === "plan") {
@@ -2671,6 +3210,14 @@ async function workflowSwitch(helpers, input) {
2671
3210
  report.created = !local && !remote;
2672
3211
  report.resumed = local || remote;
2673
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;
2674
3221
  return buildWorkflowResult(
2675
3222
  "switch",
2676
3223
  root,
@@ -2688,7 +3235,8 @@ async function workflowSwitch(helpers, input) {
2688
3235
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
2689
3236
  { id: "workspace-link", description: "Apply local workspace links for integrated development" },
2690
3237
  ...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
2691
- ]
3238
+ ],
3239
+ previewPlan
2692
3240
  },
2693
3241
  {
2694
3242
  executionMode,
@@ -2738,8 +3286,8 @@ async function workflowSwitch(helpers, input) {
2738
3286
  rootRepo.resumed = rootSwitch.resumed;
2739
3287
  rootRepo.commitSha = headCommit(repoDir);
2740
3288
  rootRepo.pushed = rootSwitch.created;
2741
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2742
- const report = findReportByName(packageReports, pkg.name);
3289
+ for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
3290
+ const report = findReportByName(packageReports, managedRepo.name);
2743
3291
  if (!report) {
2744
3292
  continue;
2745
3293
  }
@@ -2747,16 +3295,16 @@ async function workflowSwitch(helpers, input) {
2747
3295
  root,
2748
3296
  workflowRun.runId,
2749
3297
  `switch-${report.name}`,
2750
- () => checkoutTaskBranchFromStaging(pkg.dir, branchName, {
3298
+ () => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
2751
3299
  createIfMissing: input.createIfMissing !== false,
2752
3300
  pushIfCreated: false
2753
3301
  })
2754
3302
  );
2755
- report.branch = currentBranch(pkg.dir) || branchName;
3303
+ report.branch = currentBranch(managedRepo.dir) || branchName;
2756
3304
  report.created = packageSwitch.created;
2757
3305
  report.resumed = packageSwitch.resumed;
2758
- report.commitSha = headCommit(pkg.dir);
2759
- report.dirty = hasMeaningfulChanges(pkg.dir);
3306
+ report.commitSha = headCommit(managedRepo.dir);
3307
+ report.dirty = hasMeaningfulChanges(managedRepo.dir);
2760
3308
  }
2761
3309
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
2762
3310
  const stateAfterSwitch = resolveTreeseedWorkflowState(root);
@@ -2765,7 +3313,7 @@ async function workflowSwitch(helpers, input) {
2765
3313
  root,
2766
3314
  workflowRun.runId,
2767
3315
  "preview",
2768
- () => deployBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
3316
+ () => reconcileWorkflowBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
2769
3317
  ) ?? null;
2770
3318
  }
2771
3319
  const state = resolveTreeseedWorkflowState(root);
@@ -2819,17 +3367,386 @@ async function workflowSwitch(helpers, input) {
2819
3367
  toError("switch", error);
2820
3368
  }
2821
3369
  }
2822
- async function workflowDev(helpers, input = {}) {
2823
- try {
2824
- return withContextEnv(helpers.context.env, async () => {
2825
- if (helpers.context.transport === "api") {
2826
- workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
2827
- }
2828
- const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
2829
- const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
2830
- const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
2831
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
2832
- assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
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" });
2833
3750
  const args = [packageScriptPath("tenant-dev")];
2834
3751
  if (input.watch) {
2835
3752
  args.push("--watch");
@@ -2897,6 +3814,7 @@ async function workflowSave(helpers, input) {
2897
3814
  return await withContextEnv(helpers.context.env, async () => {
2898
3815
  const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
2899
3816
  const root = workspaceRoot(tenantRoot);
3817
+ ensureTreeseedLocalStateExcluded(root);
2900
3818
  const rootBranch = currentBranch(repoRoot(root)) || null;
2901
3819
  reattachRepairablePackageRepos(root, [rootBranch, STAGING_BRANCH, PRODUCTION_BRANCH].filter((branch2) => Boolean(branch2)), {
2902
3820
  operation: "save",
@@ -2916,6 +3834,9 @@ async function workflowSave(helpers, input) {
2916
3834
  const planAutoResumeRun = executionMode === "plan" ? findAutoResumableSaveRun(root, branch) : null;
2917
3835
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2918
3836
  const message = String(effectiveInput.message ?? "").trim();
3837
+ const saveLane = normalizeSaveLane(effectiveInput.lane);
3838
+ const saveCiMode = normalizeSaveCiMode(effectiveInput.ciMode, branch, saveLane);
3839
+ const releaseCandidateMode = normalizeReleaseCandidateMode(effectiveInput.releaseCandidate, "save", saveLane);
2919
3840
  const optionsHotfix = effectiveInput.hotfix === true;
2920
3841
  const previewInitialized = branchPreviewInitialized(root, branch);
2921
3842
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
@@ -2925,7 +3846,7 @@ async function workflowSave(helpers, input) {
2925
3846
  if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
2926
3847
  workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
2927
3848
  }
2928
- const packageReports = createWorkspacePackageReports(root);
3849
+ const packageReports = createManagedWorkflowRepoReports(root);
2929
3850
  const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
2930
3851
  const blockers = [];
2931
3852
  if (executionMode === "plan") {
@@ -2942,12 +3863,13 @@ async function workflowSave(helpers, input) {
2942
3863
  message,
2943
3864
  bump: effectiveInput.bump ?? "patch",
2944
3865
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2945
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
3866
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
2946
3867
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2947
3868
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2948
3869
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2949
3870
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
2950
3871
  });
3872
+ const applicationSelection = selectWorkflowApplications(root);
2951
3873
  const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
2952
3874
  return buildWorkflowResult(
2953
3875
  "save",
@@ -2967,8 +3889,11 @@ async function workflowSave(helpers, input) {
2967
3889
  failure: planAutoResumeRun.failure
2968
3890
  } : null,
2969
3891
  workspaceLinks,
2970
- ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
3892
+ ciMode: saveCiMode,
3893
+ lane: saveLane,
2971
3894
  verifyMode: effectiveInput.verifyMode ?? "fast",
3895
+ releaseCandidateMode,
3896
+ applicationSelection,
2972
3897
  ...worktreePayload(root, effectiveInput.worktreeMode),
2973
3898
  repositoryPlan,
2974
3899
  waves: repositoryPlan.waves,
@@ -2977,8 +3902,9 @@ async function workflowSave(helpers, input) {
2977
3902
  { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
2978
3903
  ...repositoryPlan.plannedSteps,
2979
3904
  { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2980
- ...shouldUseHostedSaveCi(effectiveInput, branch) ? [{ id: "hosted-ci", description: `Wait for hosted save workflows on ${branch}` }] : [],
2981
- ...branch === STAGING_BRANCH ? [{ id: "release-candidate", description: "Run 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` }] : [],
2982
3908
  { id: "workspace-link", description: "Restore local workspace links after save" },
2983
3909
  ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2984
3910
  ]
@@ -3010,14 +3936,16 @@ async function workflowSave(helpers, input) {
3010
3936
  verify: effectiveInput.verify !== false,
3011
3937
  bump: effectiveInput.bump ?? "patch",
3012
3938
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
3013
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
3939
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
3014
3940
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3015
3941
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3016
3942
  verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
3017
- ciMode: effectiveInput.ciMode ?? (branch === STAGING_BRANCH ? "hosted" : "auto"),
3943
+ ciMode: saveCiMode,
3944
+ lane: saveLane,
3018
3945
  worktreeMode: effectiveInput.worktreeMode ?? "auto",
3019
3946
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
3020
3947
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto",
3948
+ releaseCandidate: releaseCandidateMode,
3021
3949
  verifyDeployedResources: effectiveInput.verifyDeployedResources === true
3022
3950
  },
3023
3951
  [
@@ -3029,15 +3957,23 @@ async function workflowSave(helpers, input) {
3029
3957
  branch,
3030
3958
  resumable: true
3031
3959
  },
3032
- ...shouldUseHostedSaveCi(effectiveInput, branch) ? [{
3960
+ ...shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? [{
3033
3961
  id: "hosted-ci",
3034
- 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}`,
3035
3963
  repoName: rootRepo.name,
3036
3964
  repoPath: rootRepo.path,
3037
3965
  branch,
3038
3966
  resumable: true
3039
3967
  }] : [],
3040
- ...branch === STAGING_BRANCH ? [{
3968
+ ...saveLane === "promotion" ? [{
3969
+ id: "release-proof",
3970
+ description: "Run authoritative hosted release proof records",
3971
+ repoName: rootRepo.name,
3972
+ repoPath: rootRepo.path,
3973
+ branch,
3974
+ resumable: true
3975
+ }] : [],
3976
+ ...branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? [{
3041
3977
  id: "release-candidate",
3042
3978
  description: "Run release-candidate readiness checks",
3043
3979
  repoName: rootRepo.name,
@@ -3078,27 +4014,29 @@ async function workflowSave(helpers, input) {
3078
4014
  message,
3079
4015
  bump: effectiveInput.bump ?? "patch",
3080
4016
  devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
3081
- devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
4017
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-commit",
3082
4018
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3083
4019
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3084
4020
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
3085
4021
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
3086
4022
  workflowRunId: workflowRun.runId,
4023
+ deferPushUntilVerified: branch === STAGING_BRANCH,
3087
4024
  onProgress: (line, stream) => helpers.write(line, stream),
3088
- onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
3089
- const packageReportsForWave = reports.filter((repo, index) => nodes[index]?.kind === "package");
3090
- const rootReportForWave = nodes.some((node) => node.kind === "project") ? waveRootRepo : null;
4025
+ onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
4026
+ const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
4027
+ const rootReportForWave = nodes.some((node) => node.id === ".") ? waveRootRepo : null;
4028
+ const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
3091
4029
  const gates = [
3092
- ...gatesForSavedPackageReports(packageReportsForWave),
3093
- ...rootReportForWave ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
4030
+ ...gatesForSavedRepositoryReports(root, nonRootReportsForWave),
4031
+ ...rootReportForWave && !hostedEnvironment ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
3094
4032
  ];
3095
4033
  if (gates.length === 0) {
3096
4034
  return [];
3097
4035
  }
3098
- const packageNames = packageReportsForWave.map((repo) => repo.name).join(", ");
3099
- if (packageNames) {
3100
- helpers.write(`[save][workflow] Waiting for hosted package gates before saving dependents: ${packageNames}.`);
3101
- } else if (rootReportForWave) {
4036
+ const repositoryNames = gates.map((gate) => gate.name).join(", ");
4037
+ if (nonRootReportsForWave.length > 0) {
4038
+ helpers.write(`[save][workflow] Waiting for hosted repository gates before saving dependents: ${repositoryNames}.`);
4039
+ } else if (rootReportForWave && !hostedEnvironment) {
3102
4040
  helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
3103
4041
  }
3104
4042
  return waitForWorkflowGates("save", gates, "hosted", {
@@ -3115,7 +4053,7 @@ async function workflowSave(helpers, input) {
3115
4053
  const savedPackageReports = saveResult?.repos ?? packageReports;
3116
4054
  const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
3117
4055
  helpers.write("[save][workflow] Repository save phase complete; checking command readiness.");
3118
- 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();
3119
4057
  const commitCreated = savedRootRepo.committed === true;
3120
4058
  const branchSync = {
3121
4059
  ...savedRootRepo.publishWait ?? {},
@@ -3131,43 +4069,13 @@ async function workflowSave(helpers, input) {
3131
4069
  lockfileValidation: repo.lockfileValidation
3132
4070
  }))
3133
4071
  };
3134
- const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
3135
- if (branch === STAGING_BRANCH) {
4072
+ const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
4073
+ const hostedEnvironment = saveHostedEnvironmentForBranch(branch);
4074
+ if (hostedEnvironment) {
3136
4075
  const workflowGates = saveResult?.workflowGates ?? [];
3137
- if (effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
3138
- return { workflowGates };
3139
- }
3140
- helpers.write("[save][workflow] Dispatching hosted market deploy gate for deployed resource verification.");
3141
- const repository = resolveGitHubRepositorySlug(savedRootRepo.path);
3142
- await dispatchGitHubWorkflowRun(repository, {
3143
- workflow: "deploy.yml",
3144
- branch,
3145
- inputs: {
3146
- environment: "staging",
3147
- action_kind: "deploy_web"
3148
- }
3149
- });
3150
- await sleep(5e3);
3151
- helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
3152
- const dispatchedGates = await waitForWorkflowGates("save", [
3153
- hostedDeployGate({
3154
- name: savedRootRepo.name,
3155
- repoPath: savedRootRepo.path,
3156
- repository,
3157
- workflow: "deploy.yml",
3158
- branch,
3159
- headSha: savedRootRepo.commitSha
3160
- })
3161
- ], "hosted", {
3162
- root,
3163
- runId: workflowRun.runId,
3164
- onProgress: (line, stream) => helpers.write(line, stream)
3165
- });
3166
4076
  return {
3167
- workflowGates: [
3168
- ...workflowGates.filter((gate) => !(gate.repository === repository && gate.workflow === "deploy.yml")),
3169
- ...dispatchedGates
3170
- ]
4077
+ workflowGates,
4078
+ hostedReconcile: await reconcileSaveHostedEnvironment(root, hostedEnvironment, helpers, workflowRun.runId)
3171
4079
  };
3172
4080
  }
3173
4081
  helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
@@ -3186,21 +4094,44 @@ async function workflowSave(helpers, input) {
3186
4094
  branch,
3187
4095
  headSha: savedRootRepo.commitSha
3188
4096
  })] : [],
3189
- ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
3190
- name: repo.name,
3191
- repoPath: repo.path,
3192
- workflow: "verify.yml",
3193
- branch: String(repo.branch),
3194
- headSha: String(repo.commitSha)
3195
- }))
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
+ })
3196
4108
  ], "hosted", {
3197
4109
  root,
3198
4110
  runId: workflowRun.runId,
3199
4111
  onProgress: (line, stream) => helpers.write(line, stream)
3200
4112
  }).then((workflowGates) => ({ workflowGates }));
3201
4113
  }) : { workflowGates: [] };
3202
- const releaseCandidate = branch === STAGING_BRANCH ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
3203
- helpers.write("[save][workflow] Running staging release-candidate readiness checks.");
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}).`);
3204
4135
  const releaseSession = resolveTreeseedWorkflowSession(root);
3205
4136
  const stagingReleasePlan = buildReleasePlanSnapshot({
3206
4137
  root,
@@ -3211,28 +4142,42 @@ async function workflowSave(helpers, input) {
3211
4142
  rootRepo: savedRootRepo,
3212
4143
  blockers: []
3213
4144
  });
3214
- return runReleaseCandidateForPlan("save", root, stagingReleasePlan);
4145
+ return runReleaseCandidateProofForPlan("save", root, stagingReleasePlan, {
4146
+ mode: releaseCandidateMode,
4147
+ lane: saveLane,
4148
+ write: (line, stream) => helpers.write(line, stream)
4149
+ });
3215
4150
  }) : null;
3216
4151
  let previewAction = { status: "skipped" };
3217
4152
  if (beforeState.branchRole === "feature" && branch) {
3218
4153
  if (effectiveInput.preview === true) {
3219
4154
  previewAction = {
3220
4155
  status: previewInitialized ? "refreshed" : "created",
3221
- 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 }))
3222
4157
  };
3223
4158
  } else if (effectiveInput.refreshPreview !== false && previewInitialized) {
3224
4159
  previewAction = {
3225
4160
  status: "refreshed",
3226
- 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 }))
3227
4162
  };
3228
4163
  }
3229
4164
  }
3230
- const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
4165
+ const applicationSelection = selectWorkflowApplications(root, {
4166
+ packageSelection: {
4167
+ selected: savedPackageReports.map((report) => report.name)
4168
+ }
4169
+ });
4170
+ const hostingAudit = await workflowHostedVerificationGateRequired(
3231
4171
  "save",
3232
4172
  root,
3233
4173
  helpers,
3234
4174
  scope === "prod" ? "prod" : scope === "local" ? "local" : "staging",
3235
- { enabled: effectiveInput.verifyDeployedResources === true, strict: true }
4175
+ {
4176
+ enabled: effectiveInput.verifyDeployedResources === true,
4177
+ strict: true,
4178
+ live: effectiveInput.verifyDeployedResources === true,
4179
+ appId: singleSelectedWorkflowAppId(applicationSelection)
4180
+ }
3236
4181
  );
3237
4182
  const payload = {
3238
4183
  mode: saveResult?.mode ?? mode,
@@ -3257,10 +4202,15 @@ async function workflowSave(helpers, input) {
3257
4202
  workspaceLinks,
3258
4203
  commandReadiness,
3259
4204
  lockfileValidation,
3260
- ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
4205
+ ciMode: saveCiMode,
4206
+ lane: saveLane,
3261
4207
  verifyMode: effectiveInput.verifyMode ?? "fast",
4208
+ releaseCandidateMode,
4209
+ applicationSelection,
3262
4210
  workflowGates: saveWorkflowGates?.workflowGates ?? [],
4211
+ hostedReconcile: saveWorkflowGates?.hostedReconcile ?? null,
3263
4212
  releaseCandidate,
4213
+ releaseProof,
3264
4214
  hostingAudit,
3265
4215
  ...worktreePayload(root, effectiveInput.worktreeMode)
3266
4216
  };
@@ -3342,17 +4292,17 @@ async function workflowClose(helpers, input) {
3342
4292
  failure: planAutoResumeRun.failure
3343
4293
  } : null,
3344
4294
  ...worktreePayload(root, effectiveInput.worktreeMode),
3345
- autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
3346
- repos: createWorkspacePackageReports(root),
4295
+ autoSaveRequired: session.rootRepo.dirty || session.managedRepos.some((repo) => repo.dirty),
4296
+ repos: createManagedWorkflowRepoReports(root),
3347
4297
  rootRepo: createWorkspaceRootRepoReport(root),
3348
4298
  blockers,
3349
4299
  plannedSteps: [
3350
4300
  { id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
3351
4301
  { id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
3352
4302
  { id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
3353
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3354
- id: `cleanup-${pkg.name}`,
3355
- 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}`
3356
4306
  })),
3357
4307
  { id: "workspace-link", description: "Restore local workspace links on the final branch" },
3358
4308
  ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
@@ -3374,6 +4324,7 @@ async function workflowClose(helpers, input) {
3374
4324
  const featureBranch = assertFeatureBranch(root);
3375
4325
  const mode = activeSession.mode;
3376
4326
  const repoDir = activeSession.gitRoot;
4327
+ const managedWorktreeForClose = isManagedWorkflowWorktree(root);
3377
4328
  assertSessionBranchSafety("close", activeSession);
3378
4329
  if (mode === "recursive-workspace") {
3379
4330
  assertWorkspaceClean(root);
@@ -3381,7 +4332,7 @@ async function workflowClose(helpers, input) {
3381
4332
  assertCleanWorktree(root);
3382
4333
  }
3383
4334
  const rootRepo = createWorkspaceRootRepoReport(root);
3384
- const packageReports = createWorkspacePackageReports(root);
4335
+ const packageReports = createManagedWorkflowRepoReports(root);
3385
4336
  const workflowRun = acquireWorkflowRun(
3386
4337
  "close",
3387
4338
  activeSession,
@@ -3419,30 +4370,34 @@ async function workflowClose(helpers, input) {
3419
4370
  }
3420
4371
  try {
3421
4372
  await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3422
- 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));
3423
4374
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
3424
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
4375
+ const head = updateHead(repoDir);
4376
+ const tag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
3425
4377
  const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
3426
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
3427
- if (effectiveInput.deleteBranch !== false) {
4378
+ if (!managedWorktreeForClose) {
4379
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
4380
+ }
4381
+ if (effectiveInput.deleteBranch !== false && !managedWorktreeForClose) {
3428
4382
  deleteLocalBranch(repoDir, featureBranch);
3429
4383
  }
3430
4384
  return {
3431
- deprecatedTag,
4385
+ head,
4386
+ tagName: tag.tagName,
3432
4387
  deletedRemote,
3433
- deletedLocal: effectiveInput.deleteBranch !== false,
3434
- branch: currentBranch(repoDir) || STAGING_BRANCH,
4388
+ deletedLocal: effectiveInput.deleteBranch !== false && !managedWorktreeForClose,
4389
+ branch: managedWorktreeForClose ? featureBranch : currentBranch(repoDir) || STAGING_BRANCH,
3435
4390
  dirty: hasMeaningfulChanges(repoDir)
3436
4391
  };
3437
4392
  });
3438
- rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? null);
3439
- 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 ?? "");
3440
4395
  rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
3441
4396
  rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
3442
4397
  rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
3443
4398
  rootRepo.dirty = rootCleanup?.dirty === true;
3444
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3445
- const report = findReportByName(packageReports, pkg.name);
4399
+ for (const managedRepo of checkedOutManagedWorkflowRepos(root)) {
4400
+ const report = findReportByName(packageReports, managedRepo.name);
3446
4401
  if (!report) {
3447
4402
  continue;
3448
4403
  }
@@ -3453,16 +4408,21 @@ async function workflowClose(helpers, input) {
3453
4408
  Object.assign(report, cleanup);
3454
4409
  }
3455
4410
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3456
- const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
4411
+ const finalBranch = managedWorktreeForClose ? STAGING_BRANCH : currentBranch(repoDir) || STAGING_BRANCH;
3457
4412
  const managedWorktree = managedWorkflowWorktreeMetadata(root);
3458
- 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
+ }
3459
4420
  const payload = {
3460
4421
  mode,
3461
4422
  branchName: featureBranch,
3462
4423
  message,
3463
4424
  autoSaved: autoSave.performed,
3464
4425
  autoSaveResult: autoSave.save,
3465
- deprecatedTag: rootCleanup?.deprecatedTag ?? null,
3466
4426
  repos: packageReports,
3467
4427
  rootRepo,
3468
4428
  previewCleanup,
@@ -3483,6 +4443,7 @@ async function workflowClose(helpers, input) {
3483
4443
  payload,
3484
4444
  {
3485
4445
  runId: workflowRun.runId,
4446
+ includeFinalState: !managedWorktreeForClose,
3486
4447
  nextSteps: createNextSteps([
3487
4448
  { operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
3488
4449
  ])
@@ -3505,345 +4466,460 @@ async function workflowClose(helpers, input) {
3505
4466
  toError("close", error);
3506
4467
  }
3507
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
+ }
3508
4659
  async function workflowStage(helpers, input) {
3509
4660
  try {
3510
4661
  return await withContextEnv(helpers.context.env, async () => {
3511
4662
  const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
3512
4663
  const root = workspaceRoot(tenantRoot);
3513
4664
  const executionMode = normalizeExecutionMode(input);
3514
- const initialSession = resolveTreeseedWorkflowSession(root);
4665
+ const session = resolveTreeseedWorkflowSession(root);
3515
4666
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
3516
- const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
3517
- 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;
3518
4670
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
3519
4671
  const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
3520
- const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
3521
- const waitForStaging = effectiveInput.verifyDeployedResources === true || effectiveInput.waitForStaging !== false;
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.");
4674
+ }
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}.`);
4681
+ }
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
+ };
3522
4710
  if (executionMode === "plan") {
3523
- const blockers = [];
3524
- if (initialSession.branchRole !== "feature") {
3525
- blockers.push("Stage only applies to task branches.");
3526
- }
3527
- try {
3528
- validateStagingWorkflowContracts(root);
3529
- } catch (error) {
3530
- blockers.push(error instanceof Error ? error.message : String(error));
3531
- }
3532
- return buildWorkflowResult(
3533
- "stage",
3534
- root,
3535
- {
3536
- mode: initialSession.mode,
3537
- branchName: initialSession.branchName,
3538
- mergeTarget: STAGING_BRANCH,
3539
- mergeStrategy: "squash",
3540
- message,
3541
- ciMode,
3542
- autoResumeCandidate: planAutoResumeRun ? {
3543
- runId: planAutoResumeRun.runId,
3544
- branch: planAutoResumeRun.session.branchName,
3545
- failure: planAutoResumeRun.failure
3546
- } : null,
3547
- ...worktreePayload(root, effectiveInput.worktreeMode),
3548
- autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
3549
- blockers,
3550
- rootRepo: createWorkspaceRootRepoReport(root),
3551
- repos: createWorkspacePackageReports(root),
3552
- plannedSteps: [
3553
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3554
- id: `merge-${pkg.name}`,
3555
- description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
3556
- })),
3557
- { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
3558
- { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
3559
- { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
3560
- { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
3561
- { id: "release-candidate", description: "Run release-candidate readiness checks for the exact staging state" },
3562
- { id: "preview-cleanup", description: "Destroy preview resources" },
3563
- { id: "cleanup-root", description: "Archive and delete the task branch from market" },
3564
- ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
3565
- id: `cleanup-${pkg.name}`,
3566
- description: `Archive and delete the task branch from ${pkg.name}`
3567
- })),
3568
- { id: "workspace-link", description: "Restore local workspace links on staging" }
3569
- ]
3570
- },
3571
- {
3572
- executionMode,
3573
- nextSteps: createNextSteps([
3574
- { operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
3575
- ])
3576
- }
3577
- );
3578
- }
3579
- const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
3580
- message,
3581
- autoSave: effectiveInput.autoSave
3582
- });
3583
- const session = resolveTreeseedWorkflowSession(root);
3584
- const featureBranch = assertFeatureBranch(root);
3585
- const mode = session.mode;
3586
- assertSessionBranchSafety("stage", session);
3587
- if (mode === "recursive-workspace") {
3588
- assertWorkspaceClean(root);
3589
- } else {
3590
- assertCleanWorktree(root);
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
+ });
3591
4719
  }
3592
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3593
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3594
- validateStagingWorkflowContracts(root);
3595
- runWorkspaceSavePreflight({ cwd: root });
3596
- const repoDir = session.gitRoot;
3597
- const rootRepo = createWorkspaceRootRepoReport(root);
3598
- const packageReports = createWorkspacePackageReports(root);
3599
- const workflowRun = acquireWorkflowRun(
3600
- "stage",
3601
- session,
3602
- {
4720
+ if (effectiveInput.autoSave === true) {
4721
+ await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
3603
4722
  message,
3604
- waitForStaging,
3605
- deletePreview: effectiveInput.deletePreview !== false,
3606
- deleteBranch: effectiveInput.deleteBranch !== false,
3607
- ciMode,
3608
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
3609
- verifyDeployedResources: effectiveInput.verifyDeployedResources === true
3610
- },
3611
- [
3612
- { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3613
- ...packageReports.map((report) => ({
3614
- id: `merge-${report.name}`,
3615
- description: `Merge ${featureBranch} into ${report.name} staging`,
3616
- repoName: report.name,
3617
- repoPath: report.path,
3618
- branch: featureBranch,
3619
- resumable: true
3620
- })),
3621
- { id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3622
- { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3623
- { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3624
- { id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3625
- { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
3626
- ...packageReports.map((report) => ({
3627
- id: `cleanup-${report.name}`,
3628
- description: `Archive ${featureBranch} in ${report.name}`,
3629
- repoName: report.name,
3630
- repoPath: report.path,
3631
- branch: featureBranch,
3632
- resumable: true
3633
- })),
3634
- { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3635
- ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
3636
- ],
3637
- autoResumeRun ? {
3638
- ...helpers.context,
3639
- workflow: {
3640
- ...helpers.context.workflow ?? {},
3641
- resumeRunId: autoResumeRun.runId
3642
- }
3643
- } : helpers.context
3644
- );
3645
- if (autoResumeRun) {
3646
- 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);
3647
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
+ });
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);
3648
4754
  try {
3649
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3650
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3651
- const report = findReportByName(packageReports, pkg.name);
3652
- if (!report) {
3653
- continue;
3654
- }
3655
- if (!ensureLocalTaskBranch(pkg.dir, featureBranch)) {
3656
- report.skippedReason = "branch-missing";
3657
- skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
3658
- 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
+ });
3659
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 = [];
3660
4768
  try {
3661
- const mergeResult = await executeJournalStep(root, workflowRun.runId, `merge-${report.name}`, () => squashMergeBranchIntoStaging(pkg.dir, featureBranch, message, { pushTarget: true }));
3662
- report.merged = mergeResult.committed;
3663
- report.committed = mergeResult.committed;
3664
- report.pushed = mergeResult.pushed;
3665
- report.commitSha = mergeResult.commitSha;
3666
- report.branch = STAGING_BRANCH;
3667
- checkoutBranch(pkg.dir, featureBranch);
3668
- 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
+ });
3669
4795
  } catch (error) {
3670
- const reportData = collectMergeConflictReport(pkg.dir);
3671
- throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(reportData, pkg.dir, STAGING_BRANCH), {
3672
- details: { branch: featureBranch, packageName: pkg.name, report: reportData, originalError: error instanceof Error ? error.message : String(error) },
3673
- 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
3674
4802
  });
3675
4803
  }
3676
- }
3677
- let rootMerge = null;
3678
- try {
3679
- rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
3680
- assertCleanWorktree(root);
3681
- if (isManagedWorkflowWorktree(root)) {
3682
- checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
3683
- } else {
3684
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
3685
- }
3686
- run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
3687
- if (mode === "recursive-workspace") {
3688
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
3689
- }
3690
- const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
3691
- root,
3692
- gitRoot: repoDir,
3693
- branch: STAGING_BRANCH,
3694
- onProgress: (line, stream) => helpers.write(line, stream)
3695
- });
3696
- if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
3697
- run("git", ["add", "-A"], { cwd: repoDir });
3698
- run("git", ["commit", "-m", message], { cwd: repoDir });
3699
- }
3700
- if (isManagedWorkflowWorktree(root)) {
3701
- pushHeadToBranch(repoDir, STAGING_BRANCH);
3702
- } else {
3703
- pushBranch(repoDir, STAGING_BRANCH);
3704
- }
3705
- return {
3706
- commitSha: headCommit(repoDir),
3707
- branch: STAGING_BRANCH,
3708
- committed: hasMeaningfulChanges(repoDir) ? false : true,
3709
- lockfileValidation: lockfileSafety.lockfileValidation,
3710
- lockfileInstall: lockfileSafety.install
3711
- };
3712
- });
3713
- rootRepo.merged = true;
3714
- rootRepo.committed = true;
3715
- rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
3716
- rootRepo.pushed = true;
3717
- rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
3718
- } catch (error) {
3719
- const report = collectMergeConflictReport(repoDir);
3720
- throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
3721
- details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
3722
- exitCode: 12
3723
- });
3724
- }
3725
- 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", [
3726
- hostedDeployGate({
3727
- name: rootRepo.name,
3728
- repoPath: rootRepo.path,
3729
- workflow: "deploy.yml",
3730
- branch: STAGING_BRANCH,
3731
- headSha: rootRepo.commitSha
3732
- }),
3733
- ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
3734
- name: report.name,
3735
- repoPath: report.path,
3736
- workflow: "verify.yml",
3737
- branch: STAGING_BRANCH,
3738
- headSha: String(report.commitSha)
3739
- }))
3740
- ], ciMode, {
3741
- root,
3742
- runId: workflowRun.runId,
3743
- onProgress: (line, stream) => helpers.write(line, stream)
3744
- }).then((workflowGates) => ({
3745
- status: "completed",
3746
- workflowGates
3747
- })));
3748
- const stagingWait = {
3749
- status: String(stageWorkflowGateResult?.status ?? "completed"),
3750
- workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
3751
- };
3752
- const stageReleasePlan = buildReleasePlanSnapshot({
3753
- root,
3754
- mode,
3755
- level: "patch",
3756
- packageSelection: session.packageSelection,
3757
- packageReports,
3758
- rootRepo,
3759
- blockers: []
4804
+ return { status: "completed", results };
3760
4805
  });
3761
- const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("stage", root, stageReleasePlan));
3762
- const hostingAudit = await runReadOnlyHostingAuditForWorkflow(
3763
- "stage",
3764
- root,
3765
- helpers,
3766
- "staging",
3767
- { enabled: effectiveInput.verifyDeployedResources === true, strict: true }
3768
- );
3769
- 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));
3770
- const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
3771
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
3772
- const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
3773
- if (effectiveInput.deleteBranch !== false) {
3774
- 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
+ });
3775
4835
  }
3776
4836
  return {
3777
- deprecatedTag,
3778
- deletedRemote,
3779
- deletedLocal: effectiveInput.deleteBranch !== false,
3780
- branch: currentBranch(repoDir) || STAGING_BRANCH
4837
+ mode: verifyMode,
4838
+ status: "passed",
4839
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4840
+ proof
3781
4841
  };
3782
4842
  });
3783
- rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? rootRepo.tagName ?? "");
3784
- rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
3785
- rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
3786
- rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
3787
- rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
3788
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3789
- const report = findReportByName(packageReports, pkg.name);
3790
- if (!report) {
3791
- continue;
3792
- }
3793
- const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
3794
- deleteBranch: effectiveInput.deleteBranch !== false,
3795
- 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
3796
4849
  }));
3797
- Object.assign(report, cleanup);
3798
- }
3799
- const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3800
- const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
3801
- const managedWorktree = managedWorkflowWorktreeMetadata(root);
3802
- 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" });
3803
4893
  const payload = {
3804
- mode,
3805
- branchName: featureBranch,
3806
- mergeTarget: STAGING_BRANCH,
3807
- mergeStrategy: "squash",
3808
- message,
3809
- autoSaved: autoSave.performed,
3810
- autoSaveResult: autoSave.save,
3811
- deprecatedTag: rootCleanup?.deprecatedTag ?? null,
3812
- repos: packageReports,
3813
- rootRepo,
3814
- stagingWait,
3815
- releaseCandidate,
3816
- previewCleanup,
3817
- lockfileValidation: rootMerge?.lockfileValidation ?? null,
3818
- lockfileInstall: rootMerge?.lockfileInstall ?? null,
3819
- remoteDeleted: rootRepo.deletedRemote,
3820
- localDeleted: rootRepo.deletedLocal,
3821
- 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,
3822
4905
  workspaceLinks,
3823
- ciMode,
3824
- workflowGates: stagingWait.workflowGates,
3825
- hostingAudit,
3826
- worktreeCleanup,
3827
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
3828
- managedWorktree,
3829
- worktreePath: managedWorktree?.worktreePath ?? null,
3830
- 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."
3831
4908
  };
3832
4909
  completeWorkflowRun(root, workflowRun.runId, payload);
3833
- return buildWorkflowResult(
3834
- "stage",
3835
- root,
3836
- payload,
3837
- {
3838
- runId: workflowRun.runId,
3839
- nextSteps: createNextSteps([
3840
- { operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
3841
- { operation: "status", reason: "Inspect staging readiness after the task branch merge." }
3842
- ])
3843
- }
3844
- );
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
+ });
3845
4918
  } catch (error) {
3846
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4919
+ try {
4920
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4921
+ } catch {
4922
+ }
3847
4923
  failWorkflowRun(root, workflowRun.runId, error, {
3848
4924
  resumable: true,
3849
4925
  runId: workflowRun.runId,
@@ -3859,44 +4935,97 @@ async function workflowStage(helpers, input) {
3859
4935
  toError("stage", error);
3860
4936
  }
3861
4937
  }
3862
- async function workflowTagsCleanup(helpers, input = {}) {
3863
- try {
3864
- return await withContextEnv(helpers.context.env, async () => {
3865
- const root = resolveProjectRootOrThrow("tags:cleanup", helpers.cwd());
3866
- const executionMode = input.plan === true || input.dryRun === true ? "plan" : "execute";
3867
- const cleanup = collectStaleDevTagCleanupReports(root, input, { execute: executionMode === "execute" });
3868
- return buildWorkflowResult("tags:cleanup", root, {
3869
- ...cleanup,
3870
- mode: hasCompleteTreeseedPackageCheckout(root) ? "recursive-workspace" : "root-only"
3871
- }, {
3872
- executionMode,
3873
- summary: executionMode === "plan" ? "Treeseed dev tag cleanup plan ready." : "Treeseed dev tag cleanup completed.",
3874
- 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." }])
3875
- });
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:")));
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
+ }
3876
4977
  });
3877
- } catch (error) {
3878
- toError("tags:cleanup", error);
3879
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
+ });
3880
5018
  }
3881
5019
  async function workflowRelease(helpers, input) {
3882
5020
  try {
3883
5021
  return await withContextEnv(helpers.context.env, async () => {
3884
- const root = resolveProjectRootOrThrow("release", helpers.cwd());
3885
- reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
3886
- operation: "release",
3887
- onProgress: (line, stream) => helpers.write(line, stream),
3888
- throwOnBlocker: true
3889
- });
5022
+ const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
3890
5023
  const session = resolveTreeseedWorkflowSession(root);
3891
- const gitRoot = session.gitRoot;
3892
- const mode = session.mode;
3893
5024
  const executionMode = normalizeExecutionMode(input);
3894
5025
  const rootRepo = createWorkspaceRootRepoReport(root);
3895
5026
  const packageReports = createWorkspacePackageReports(root);
3896
5027
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
3897
- const freshRelease = input.fresh === true && !explicitResumeRunId;
3898
- const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
3899
- 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;
3900
5029
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3901
5030
  const effectiveInput = autoResumeRun ? {
3902
5031
  ...autoResumeRun.input,
@@ -3904,11 +5033,10 @@ async function workflowRelease(helpers, input) {
3904
5033
  } : input;
3905
5034
  const level = effectiveInput.bump ?? "patch";
3906
5035
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
3907
- const isResume = Boolean(explicitResumeRunId || autoResumeRun);
3908
5036
  const packageSelection = session.packageSelection;
3909
5037
  const plannedRelease = buildReleasePlanSnapshot({
3910
5038
  root,
3911
- mode,
5039
+ mode: session.mode,
3912
5040
  level,
3913
5041
  repairVersionLine: effectiveInput.repairVersionLine === true,
3914
5042
  targetVersionLine: effectiveInput.targetVersionLine,
@@ -3917,70 +5045,91 @@ async function workflowRelease(helpers, input) {
3917
5045
  rootRepo,
3918
5046
  blockers: []
3919
5047
  });
3920
- const plannedPackageSelection = releasePlanPackageSelection(plannedRelease.packageSelection);
3921
- const selectedPackageNames = new Set(plannedPackageSelection.selected);
3922
- const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, plannedPackageSelection.selected, {
5048
+ const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5049
+ const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
3923
5050
  level,
3924
5051
  repairVersionLine: effectiveInput.repairVersionLine === true
3925
5052
  });
5053
+ const plannedReadiness = collectTreeseedDeploymentReadiness({
5054
+ tenantRoot: root,
5055
+ environment: "prod",
5056
+ appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
5057
+ });
5058
+ blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
3926
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
+ };
3927
5074
  if (executionMode === "plan") {
3928
- return buildWorkflowResult("release", root, {
3929
- ...plannedRelease,
3930
- ciMode,
3931
- fresh: input.fresh === true,
3932
- freshArchivedRuns: [],
3933
- ...worktreePayload(root, effectiveInput.worktreeMode),
3934
- autoResumeCandidate: planAutoResumeRun ? {
3935
- runId: planAutoResumeRun.runId,
3936
- branch: planAutoResumeRun.session.branchName,
3937
- failure: planAutoResumeRun.failure
3938
- } : null
3939
- }, {
3940
- executionMode,
3941
- nextSteps: createNextSteps([
3942
- { operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
3943
- ])
3944
- });
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
+ );
3945
5086
  }
3946
5087
  if (blockers.length > 0) {
3947
- workflowError("release", "validation_failed", blockers.join("\n"), {
3948
- details: { blockers }
5088
+ workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
5089
+ ${blockers.join("\n")}`, {
5090
+ details: { blockers, releasePlan: plannedRelease }
3949
5091
  });
3950
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);
3951
5104
  const workflowRun = acquireWorkflowRun(
3952
5105
  "release",
3953
5106
  session,
3954
5107
  {
5108
+ ...effectiveInput,
3955
5109
  bump: level,
3956
- repairVersionLine: effectiveInput.repairVersionLine === true,
3957
- targetVersionLine: effectiveInput.targetVersionLine,
3958
- devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
3959
- gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3960
- gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3961
5110
  ciMode,
3962
- fresh: input.fresh === true,
3963
- worktreeMode: effectiveInput.worktreeMode ?? "auto",
3964
- workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
5111
+ fresh: input.fresh === true
3965
5112
  },
3966
5113
  [
3967
- { id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3968
- { id: "release-staging-gates", description: "Verify current staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3969
- { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3970
- { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3971
- ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
3972
- ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
3973
- id: `release-${report.name}`,
3974
- description: `Release ${report.name}`,
3975
- repoName: report.name,
3976
- repoPath: report.path,
3977
- branch: STAGING_BRANCH,
3978
- resumable: true
3979
- })),
3980
- { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3981
- { id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
3982
- { id: "release-back-merge", description: "Back-merge main into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3983
- ...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 }
3984
5133
  ],
3985
5134
  autoResumeRun ? {
3986
5135
  ...helpers.context,
@@ -3991,546 +5140,188 @@ async function workflowRelease(helpers, input) {
3991
5140
  } : helpers.context
3992
5141
  );
3993
5142
  if (autoResumeRun) {
3994
- helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
3995
- }
3996
- const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
3997
- if (resumeAtRootGates) {
3998
- helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
5143
+ helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId}.`);
3999
5144
  }
4000
- let releaseCleanupSnapshot = null;
4001
5145
  try {
4002
- const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
4003
- const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
4004
- const effectiveSelectedPackageNames = new Set(effectivePackageSelection.selected);
4005
- const effectiveVersions = releasePlanVersionMap(releasePlan.plannedVersions);
4006
- const effectiveStableDependencyVersions = releasePlanStableDependencyVersionMap(releasePlan);
4007
- const effectiveDependencyReplacementVersions = new Map([
4008
- ...effectiveStableDependencyVersions.entries(),
4009
- ...effectiveVersions.entries()
4010
- ]);
4011
- const rootVersion = String(releasePlan.rootVersion);
4012
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
4013
- assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
4014
- const stagingGateResult = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-staging-gates") : await executeJournalStep(root, workflowRun.runId, "release-staging-gates", () => {
4015
- helpers.write("[release][workflow] Verifying current staging gates before production release.");
4016
- 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) => ({
4017
5168
  name: pkg.name,
4018
- repoPath: pkg.dir,
4019
- workflow: "verify.yml",
4020
- branch: STAGING_BRANCH,
4021
- headSha: headCommit(pkg.dir)
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" }
4022
5171
  }));
4023
- return waitForWorkflowGates("release", [
4024
- hostedDeployGate({
4025
- name: rootRepo.name,
4026
- repoPath: rootRepo.path,
4027
- workflow: "deploy.yml",
4028
- branch: STAGING_BRANCH,
4029
- headSha: headCommit(gitRoot)
4030
- }),
4031
- ...packageGates
4032
- ], ciMode, {
4033
- root,
4034
- runId: workflowRun.runId,
4035
- onProgress: (line, stream) => helpers.write(line, stream)
4036
- }).then((workflowGates) => ({ workflowGates }));
4037
- });
4038
- const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
4039
- if (!resumeAtRootGates && !isResume) {
4040
- assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
4041
- assertCleanWorktree(root);
4042
- }
4043
- if (!resumeAtRootGates) {
4044
- prepareReleaseBranches(root);
4045
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4046
- runWorkspaceReleasePreflight({ cwd: root });
4047
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
4048
- }
4049
- if (mode === "root-only") {
4050
- const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
4051
- setRootPackageJsonVersion(root, rootVersion);
4052
- run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
4053
- const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
4054
- const changelog = updateReleaseChangelog(gitRoot, {
4055
- version: rootVersion,
4056
- commits: rootCommitsBeforeChangelog,
4057
- extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
4058
- });
4059
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4060
- subject: `release: ${level} bump`,
4061
- version: rootVersion,
4062
- tagName: rootVersion,
4063
- commits: rootCommitsBeforeChangelog,
4064
- changelog
4065
- }));
4066
- pushBranch(gitRoot, STAGING_BRANCH);
4067
- const stagingCommit = headCommit(gitRoot);
4068
- const rootCommits = releaseHistoryCommits(gitRoot);
4069
- const released = mergeBranchIntoTarget(root, {
4070
- sourceBranch: STAGING_BRANCH,
4071
- targetBranch: PRODUCTION_BRANCH,
4072
- message: releaseAdminMessage({
4073
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4074
- version: rootVersion,
4075
- tagName: rootVersion,
4076
- commits: rootCommits,
4077
- changelog
4078
- }),
4079
- pushTarget: true
4080
- });
4081
- const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
4082
- subject: `release: ${rootVersion}`,
4083
- version: rootVersion,
4084
- tagName: rootVersion,
4085
- commits: rootCommits,
4086
- changelog
4087
- }));
4088
- syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
4089
- return {
4090
- rootVersion,
4091
- stagingCommit,
4092
- releasedCommit: released.commitSha,
4093
- tag,
4094
- changelog,
4095
- adminCommitSummary: {
4096
- commitCount: rootCommits.length,
4097
- notableCommits: rootCommits.slice(0, 12)
4098
- }
4099
- };
4100
- });
4101
- rootRepo.committed = true;
4102
- rootRepo.pushed = true;
4103
- rootRepo.merged = true;
4104
- rootRepo.branch = PRODUCTION_BRANCH;
4105
- rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
4106
- rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
4107
- const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
4108
- hostedDeployGate({
4109
- name: rootRepo.name,
4110
- repoPath: rootRepo.path,
4111
- workflow: "deploy.yml",
4112
- branch: rootVersion,
4113
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
4114
- })
4115
- ].filter((gate) => gate.headSha), ciMode, {
4116
- root,
4117
- runId: workflowRun.runId,
4118
- onProgress: (line, stream) => helpers.write(line, stream)
4119
- }).then((workflowGates) => ({ workflowGates })));
4120
- const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
4121
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4122
- const hostingAudit2 = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
4123
- enabled: true,
4124
- strict: false
4125
- });
4126
- const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
4127
- version: rootVersion,
4128
- changelog: rootRelease2?.changelog ?? null
4129
- }));
4130
- const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4131
- const payload2 = {
4132
- mode,
4133
- mergeStrategy: "merge-commit",
4134
- level,
4135
- fresh: input.fresh === true,
4136
- freshArchivedRuns: freshPreparation.archived,
4137
- resumed: workflowRun.resumed,
4138
- resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
4139
- autoResumed: autoResumeRun != null,
4140
- rootVersion: String(rootRelease2?.rootVersion ?? ""),
4141
- releaseTag: String(rootRelease2?.rootVersion ?? ""),
4142
- releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
4143
- stagingBranch: STAGING_BRANCH,
4144
- productionBranch: PRODUCTION_BRANCH,
4145
- touchedPackages: [],
4146
- packageSelection: { changed: [], dependents: [], selected: [] },
4147
- publishWait: [],
4148
- repos: [],
4149
- rootRepo,
4150
- releaseCandidate,
4151
- stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4152
- releaseBackMerge: releaseBackMerge2,
4153
- hostedDeploymentState: hostedDeploymentState2,
4154
- finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
4155
- pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
4156
- workspaceLinks: workspaceLinks2,
4157
- ciMode,
4158
- workflowGates: [
4159
- ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4160
- ...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
4161
- ],
4162
- hostingAudit: hostingAudit2,
4163
- ...worktreePayload(root, effectiveInput.worktreeMode)
4164
- };
4165
- completeWorkflowRun(root, workflowRun.runId, payload2);
4166
- return buildWorkflowResult("release", root, payload2, {
4167
- runId: workflowRun.runId,
4168
- nextSteps: createNextSteps([
4169
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
4170
- ])
4171
- });
4172
- }
4173
- if (!resumeAtRootGates) {
4174
- validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
4175
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4176
- if (effectiveSelectedPackageNames.has(pkg.name)) {
4177
- prepareReleaseBranches(pkg.dir);
4178
- }
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}`);
4179
5178
  }
4180
- }
4181
- releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
4182
- const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
4183
- const releasedPackageDevTags2 = Object.fromEntries(
4184
- checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
4185
- const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
4186
- return [pkg.name, String(packageJson.version ?? "")];
4187
- }).filter(([, version]) => version.includes("-dev."))
4188
- );
4189
- const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveDependencyReplacementVersions);
4190
- applyStableWorkspaceVersionChanges(root, effectiveVersions);
4191
- setRootPackageJsonVersion(root, rootVersion);
4192
- const releaseInstalls2 = [
4193
- { name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
4194
- ];
4195
- assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
4196
5179
  return {
4197
- releasedPackageDevTags: releasedPackageDevTags2,
4198
- replacedDevReferences: replacedDevReferences2,
4199
- releaseInstalls: releaseInstalls2
5180
+ versions: Object.fromEntries(allVersions.entries()),
5181
+ adapterMetadata,
5182
+ rootInstall,
5183
+ workspaceUnlink
4200
5184
  };
4201
- }, { rerunCompleted: workflowRun.resumed && !resumeAtRootGates });
4202
- const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
4203
- const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
4204
- const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
4205
- const publishWait = [];
4206
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4207
- const report = findReportByName(packageReports, pkg.name);
4208
- if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
4209
- if (report) {
4210
- report.skippedReason = "unchanged";
4211
- }
4212
- continue;
4213
- }
4214
- const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
4215
- checkoutBranch(pkg.dir, STAGING_BRANCH);
4216
- const tagName = String(effectiveVersions.get(pkg.name));
4217
- releaseInstalls.push({
4218
- name: pkg.name,
4219
- ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
4220
- });
4221
- assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
4222
- 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}`, () => {
4223
5191
  const changelog = updateReleaseChangelog(pkg.dir, {
4224
- version: tagName,
4225
- commits: packageCommitsBeforeChangelog,
4226
- extraDependencyBullets: [`Release ${pkg.name} ${tagName}.`]
4227
- });
4228
- if (hasMeaningfulChanges(pkg.dir)) {
4229
- run("git", ["add", "-A"], { cwd: pkg.dir });
4230
- run("git", ["commit", "-m", releaseAdminMessage({
4231
- subject: `release: ${tagName}`,
4232
- version: tagName,
4233
- tagName,
4234
- commits: packageCommitsBeforeChangelog,
4235
- changelog,
4236
- extraLines: [`Package: ${pkg.name}`]
4237
- })], { cwd: pkg.dir });
4238
- }
4239
- pushBranch(pkg.dir, STAGING_BRANCH);
4240
- const packageCommits = releaseHistoryCommits(pkg.dir);
4241
- const mergeResult = mergeBranchIntoTarget(pkg.dir, {
4242
- sourceBranch: STAGING_BRANCH,
4243
- targetBranch: PRODUCTION_BRANCH,
4244
- message: releaseAdminMessage({
4245
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4246
- version: tagName,
4247
- tagName,
4248
- commits: packageCommits,
4249
- changelog,
4250
- extraLines: [`Package: ${pkg.name}`]
4251
- }),
4252
- pushTarget: true
4253
- });
4254
- const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
4255
- subject: `release: ${tagName}`,
4256
- version: tagName,
4257
- tagName,
4258
- commits: packageCommits,
4259
- changelog,
4260
- extraLines: [`Package: ${pkg.name}`]
4261
- }));
4262
- const workflowGates = await waitForWorkflowGates("release", [
4263
- {
4264
- name: pkg.name,
4265
- repoPath: pkg.dir,
4266
- workflow: "publish.yml",
4267
- headSha: mergeResult.commitSha,
4268
- branch: tagName
4269
- }
4270
- ], ciMode, {
4271
- root,
4272
- runId: workflowRun.runId,
4273
- onProgress: (line, stream) => helpers.write(line, stream)
5192
+ version,
5193
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
5194
+ targetRef: "HEAD"
4274
5195
  });
4275
- const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
4276
- assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
4277
- const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
4278
- subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
4279
- version: tagName,
4280
- tagName,
4281
- sourceRef: PRODUCTION_BRANCH,
4282
- targetRef: STAGING_BRANCH,
4283
- commits: packageCommits,
4284
- changelog,
4285
- 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
4286
5203
  }));
4287
- 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}`);
4288
5207
  return {
4289
- commitSha: mergeResult.commitSha,
4290
- tagName,
4291
- tag,
5208
+ name: pkg.name,
5209
+ path: relative(root, pkg.dir),
5210
+ version,
4292
5211
  changelog,
4293
- adminCommitSummary: {
4294
- commitCount: packageCommits.length,
4295
- notableCommits: packageCommits.slice(0, 12)
4296
- },
4297
- publish,
4298
- workflowGates,
4299
- backMerge
5212
+ commit,
5213
+ promotion,
5214
+ tag
4300
5215
  };
4301
5216
  });
4302
- report.committed = true;
4303
- report.pushed = true;
4304
- report.merged = true;
4305
- report.tagName = String(releasedPackage?.tagName ?? "");
4306
- report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
4307
- report.publishWait = releasedPackage?.publish ?? null;
4308
- report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
4309
- report.backMerge = releasedPackage?.backMerge ?? null;
4310
- report.changelog = releasedPackage?.changelog ?? null;
4311
- report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
4312
- report.branch = STAGING_BRANCH;
4313
- publishWait.push({
4314
- name: report.name,
4315
- ...releasedPackage?.publish ?? {}
4316
- });
5217
+ packageReleases.push(packageRelease);
4317
5218
  }
4318
- assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
4319
5219
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
4320
- setRootPackageJsonVersion(root, rootVersion);
4321
- run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
4322
- const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
4323
- const changelog = updateReleaseChangelog(gitRoot, {
4324
- version: rootVersion,
4325
- commits: rootCommitsBeforeChangelog,
4326
- extraDependencyBullets: [
4327
- `Release @treeseed/market ${rootVersion}.`,
4328
- ...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
4329
- ]
5220
+ const changelog = updateReleaseChangelog(repoRoot(root), {
5221
+ version: plannedRelease.rootVersion,
5222
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
5223
+ targetRef: "HEAD",
5224
+ extraDependencyBullets: versionLines(stableVersions)
4330
5225
  });
4331
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4332
- subject: `release: ${level} bump`,
4333
- version: rootVersion,
4334
- tagName: rootVersion,
4335
- commits: rootCommitsBeforeChangelog,
4336
- changelog,
4337
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
4338
- }));
4339
- pushBranch(gitRoot, STAGING_BRANCH);
4340
- const stagingCommit = headCommit(gitRoot);
4341
- const rootCommits = releaseHistoryCommits(gitRoot);
4342
- let released;
4343
- let submoduleReconciliation = null;
4344
- const mergeMessage = releaseAdminMessage({
4345
- subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
4346
- version: rootVersion,
4347
- tagName: rootVersion,
4348
- commits: rootCommits,
4349
- changelog,
4350
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
4351
- });
4352
- try {
4353
- released = mergeBranchIntoTarget(root, {
4354
- sourceBranch: STAGING_BRANCH,
4355
- targetBranch: PRODUCTION_BRANCH,
4356
- message: mergeMessage,
4357
- pushTarget: false,
4358
- quietMerge: true
4359
- });
4360
- } catch (error) {
4361
- const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
4362
- if (!reconciliation.resolved) {
4363
- throw error;
4364
- }
4365
- helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
4366
- submoduleReconciliation = reconciliation;
4367
- commitAllIfChanged(gitRoot, mergeMessage);
4368
- released = { commitSha: headCommit(gitRoot) };
4369
- }
4370
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
4371
- if (effectiveSelectedPackageNames.has(pkg.name)) {
4372
- syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
4373
- }
4374
- }
4375
- const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
4376
- commitAllIfChanged(gitRoot, releaseAdminMessage({
4377
- subject: "release: sync package main heads",
4378
- version: rootVersion,
4379
- tagName: rootVersion,
4380
- 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,
4381
5231
  targetRef: PRODUCTION_BRANCH,
4382
- commits: mainPointerCommits,
4383
5232
  changelog,
4384
- extraLines: versionLines(effectiveVersions).map((line) => `Main package ${line}`)
5233
+ extraLines: versionLines(stableVersions).map((line) => `Released package ${line}`)
4385
5234
  }));
4386
- const releasedCommit = headCommit(gitRoot);
4387
- const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit, releaseAdminMessage({
4388
- subject: `release: ${rootVersion}`,
4389
- version: rootVersion,
4390
- tagName: rootVersion,
4391
- commits: rootCommits,
4392
- changelog,
4393
- extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
4394
- }));
4395
- run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
4396
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
4397
- 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}`);
4398
5238
  return {
4399
- rootVersion,
4400
- stagingCommit,
4401
- releasedCommit,
4402
- mergeCommit: released.commitSha,
4403
- tag,
5239
+ name: "@treeseed/market",
5240
+ version: plannedRelease.rootVersion,
5241
+ releaseTag: plannedRelease.releaseTag,
4404
5242
  changelog,
4405
- adminCommitSummary: {
4406
- commitCount: rootCommits.length,
4407
- notableCommits: rootCommits.slice(0, 12)
4408
- },
4409
- submoduleReconciliation
5243
+ commit,
5244
+ promotion,
5245
+ tag
4410
5246
  };
4411
5247
  });
4412
- rootRepo.committed = true;
4413
- rootRepo.pushed = true;
4414
- rootRepo.merged = true;
4415
- rootRepo.branch = PRODUCTION_BRANCH;
4416
- rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
4417
- rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
4418
- const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
5248
+ const publishGates = [
4419
5249
  hostedDeployGate({
4420
- name: rootRepo.name,
4421
- repoPath: rootRepo.path,
4422
- workflow: "deploy.yml",
4423
- branch: rootVersion,
4424
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
4425
- })
4426
- ].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, {
4427
5270
  root,
4428
5271
  runId: workflowRun.runId,
4429
5272
  onProgress: (line, stream) => helpers.write(line, stream)
4430
5273
  }).then((workflowGates) => ({ workflowGates })));
4431
- const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
4432
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4433
- const hostingAudit = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
4434
- enabled: true,
4435
- strict: false
4436
- });
4437
- const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
4438
- version: rootVersion,
4439
- changelog: rootRelease?.changelog ?? null,
4440
- selectedVersions: effectiveVersions
4441
- }));
4442
- const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
4443
- 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", () => {
4444
- const cleanup = collectStaleDevTagCleanupReports(root, {
4445
- includePackages: effectivePackageSelection.selected,
4446
- branchScope: "all"
4447
- }, { execute: true });
4448
- 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 };
4449
5286
  });
4450
- syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
4451
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
5287
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
4452
5288
  const payload = {
4453
- mode,
4454
- mergeStrategy: "merge-commit",
4455
- level,
4456
- fresh: input.fresh === true,
4457
- freshArchivedRuns: freshPreparation.archived,
4458
- resumed: workflowRun.resumed,
4459
- resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
4460
- autoResumed: autoResumeRun != null,
4461
- rootVersion: String(rootRelease?.rootVersion ?? ""),
4462
- releaseTag: String(rootRelease?.rootVersion ?? ""),
4463
- releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
4464
- stagingBranch: STAGING_BRANCH,
4465
- productionBranch: PRODUCTION_BRANCH,
4466
- touchedPackages: effectivePackageSelection.selected,
4467
- packageSelection: effectivePackageSelection,
4468
- replacedDevReferences,
4469
- releaseInstalls,
4470
- devTagCleanup,
4471
- publishWait,
4472
- repos: packageReports,
4473
- rootRepo,
4474
- releaseCandidate,
4475
- stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4476
- releaseBackMerge,
4477
- hostedDeploymentState,
4478
- finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
4479
- pushStatus: {
4480
- stagingPushed: true,
4481
- productionPushed: true,
4482
- tagPushed: true
4483
- },
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,
4484
5300
  workspaceLinks,
4485
- ciMode,
4486
- workflowGates: [
4487
- ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4488
- ...packageReports.flatMap((report) => report.workflowGates),
4489
- ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
4490
- ],
4491
- hostingAudit,
4492
- ...worktreePayload(root, effectiveInput.worktreeMode)
5301
+ releasedCommit: String(rootRelease.commit.commitSha ?? ""),
5302
+ touchedPackages: selectedPackageNames,
5303
+ finalBranch: STAGING_BRANCH
4493
5304
  };
4494
5305
  completeWorkflowRun(root, workflowRun.runId, payload);
4495
5306
  return buildWorkflowResult("release", root, payload, {
4496
5307
  runId: workflowRun.runId,
5308
+ summary: "Treeseed production release completed successfully.",
4497
5309
  nextSteps: createNextSteps([
4498
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
5310
+ { operation: "status", reason: "Inspect release state after production promotion." }
4499
5311
  ])
4500
5312
  });
4501
5313
  } catch (error) {
4502
- const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
4503
- const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
4504
- const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
4505
- const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
4506
- helpers.write(`[release][recovery] Last release phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`, "stderr");
4507
5314
  try {
4508
- const repair = reattachRepairablePackageRepos(root, [STAGING_BRANCH, PRODUCTION_BRANCH], {
4509
- onProgress: (line, stream) => helpers.write(line, stream)
4510
- });
4511
- if (repair.blockers.length > 0) {
4512
- helpers.write(`[release][recovery] Package repos need manual review before retrying:
4513
- ${repair.blockers.join("\n")}`, "stderr");
4514
- }
4515
- } catch (repairError) {
4516
- helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
4517
- }
4518
- if (localCleanup.restored.length > 0) {
4519
- helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
4520
- }
4521
- if (localCleanup.manualReview.length > 0) {
4522
- helpers.write(`[release][recovery] Local cleanup needs manual review:
4523
- ${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 {
4524
5317
  }
4525
- helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
4526
5318
  failWorkflowRun(root, workflowRun.runId, error, {
4527
5319
  resumable: true,
4528
5320
  runId: workflowRun.runId,
4529
5321
  command: "release",
4530
- message: `Resume the interrupted release on ${STAGING_BRANCH}. Last phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`,
4531
- recoverCommand: "npx trsd recover --json",
4532
- resumeCommand: `npx trsd release --${level} --json`,
4533
- localCleanup
5322
+ message: "Resume the interrupted production release after fixing the cause.",
5323
+ recoverCommand: "treeseed recover",
5324
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
4534
5325
  });
4535
5326
  throw error;
4536
5327
  }
@@ -4592,6 +5383,8 @@ async function workflowResume(helpers, input) {
4592
5383
  return workflowSwitch(resumedHelpers, journal.input);
4593
5384
  case "save":
4594
5385
  return workflowSave(resumedHelpers, journal.input);
5386
+ case "update":
5387
+ return workflowUpdate(resumedHelpers, journal.input);
4595
5388
  case "close":
4596
5389
  return workflowClose(resumedHelpers, journal.input);
4597
5390
  case "stage":
@@ -4614,7 +5407,7 @@ async function workflowRecover(helpers, input = {}) {
4614
5407
  try {
4615
5408
  return await withContextEnv(helpers.context.env, async () => {
4616
5409
  const root = resolveProjectRootOrThrow("recover", helpers.cwd());
4617
- const lock = inspectWorkflowLock(root);
5410
+ const lock = inspectWorkflowLock(root, { scope: "worktree" });
4618
5411
  const journals = listWorkflowRunJournals(root);
4619
5412
  const session = resolveTreeseedWorkflowSession(root);
4620
5413
  const currentHeads = Object.fromEntries(
@@ -4720,6 +5513,7 @@ async function workflowDestroy(helpers, input) {
4720
5513
  const dryRun = executionMode === "plan";
4721
5514
  const force = input.force === true;
4722
5515
  const deleteData = input.deleteData === true;
5516
+ const sweepTreeseed = input.sweepTreeseed === true;
4723
5517
  const destroyRemote = input.destroyRemote !== false;
4724
5518
  const destroyLocal = input.destroyLocal !== false;
4725
5519
  const removeBuildArtifacts = input.removeBuildArtifacts === true;
@@ -4733,6 +5527,7 @@ async function workflowDestroy(helpers, input) {
4733
5527
  dryRun,
4734
5528
  force,
4735
5529
  deleteData,
5530
+ sweepTreeseed,
4736
5531
  destroyRemote,
4737
5532
  destroyLocal,
4738
5533
  removeBuildArtifacts,
@@ -4743,12 +5538,13 @@ async function workflowDestroy(helpers, input) {
4743
5538
  },
4744
5539
  plannedSteps: [
4745
5540
  ...destroyRemote ? [{ id: "destroy-remote", description: `Destroy remote ${scope} resources` }] : [],
5541
+ ...sweepTreeseed ? [{ id: "sweep-treeseed-resources", description: "Sweep TreeSeed-owned provider resources across persistent environments" }] : [],
4746
5542
  ...destroyLocal ? [{ id: "cleanup-local", description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}` }] : []
4747
5543
  ],
4748
5544
  remoteResult: null
4749
5545
  };
4750
5546
  if (executionMode === "plan") {
4751
- const plannedRemoteResult = destroyRemote ? await destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: true, force, deleteData, target }) : null;
5547
+ const plannedRemoteResult = destroyRemote ? await destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: true, force, deleteData, sweepTreeseed, target }) : null;
4752
5548
  return buildWorkflowResult(
4753
5549
  "destroy",
4754
5550
  tenantRoot,
@@ -4759,7 +5555,7 @@ async function workflowDestroy(helpers, input) {
4759
5555
  {
4760
5556
  executionMode,
4761
5557
  nextSteps: createNextSteps([
4762
- { operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, deleteData, removeBuildArtifacts } },
5558
+ { operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, deleteData, sweepTreeseed, removeBuildArtifacts } },
4763
5559
  { operation: "status", reason: "Confirm the current environment state before making destructive changes." }
4764
5560
  ])
4765
5561
  }
@@ -4772,6 +5568,7 @@ async function workflowDestroy(helpers, input) {
4772
5568
  environment: scope,
4773
5569
  force,
4774
5570
  deleteData,
5571
+ sweepTreeseed,
4775
5572
  destroyRemote,
4776
5573
  destroyLocal,
4777
5574
  removeBuildArtifacts
@@ -4801,7 +5598,7 @@ async function workflowDestroy(helpers, input) {
4801
5598
  if (!confirmed) {
4802
5599
  workflowError("destroy", "confirmation_required", `Destroy confirmation required. Re-run with confirm="${expectedConfirmation}".`);
4803
5600
  }
4804
- const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: false, force, deleteData, target })) : null;
5601
+ const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyTreeseedEnvironmentResources(tenantRoot, { dryRun: false, force, deleteData, sweepTreeseed, target })) : null;
4805
5602
  if (!destroyRemote) {
4806
5603
  skipJournalStep(root, workflowRun.runId, "destroy-remote", { skippedReason: "destroyRemote=false" });
4807
5604
  }
@@ -4851,19 +5648,27 @@ async function workflowDestroy(helpers, input) {
4851
5648
  }
4852
5649
  export {
4853
5650
  TreeseedWorkflowError,
5651
+ destroyTreeseedBranchPreview,
5652
+ normalizeReleaseCandidateMode,
5653
+ normalizeSaveCiMode,
5654
+ normalizeSaveLane,
5655
+ reconcileTreeseedBranchPreview,
5656
+ shouldUseHostedSaveCi,
4854
5657
  workflowCi,
4855
5658
  workflowClose,
4856
5659
  workflowConfig,
4857
5660
  workflowDestroy,
4858
5661
  workflowDev,
4859
5662
  workflowExport,
5663
+ workflowProof,
4860
5664
  workflowRecover,
4861
5665
  workflowRelease,
5666
+ workflowReleaseCandidate,
4862
5667
  workflowResume,
4863
5668
  workflowSave,
4864
5669
  workflowStage,
4865
5670
  workflowStatus,
4866
5671
  workflowSwitch,
4867
- workflowTagsCleanup,
4868
- workflowTasks
5672
+ workflowTasks,
5673
+ workflowUpdate
4869
5674
  };