@treeseed/sdk 0.6.7 → 0.6.9

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 (49) hide show
  1. package/dist/copilot.d.ts +15 -0
  2. package/dist/copilot.js +75 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +18 -0
  5. package/dist/managed-dependencies.d.ts +56 -0
  6. package/dist/managed-dependencies.js +668 -0
  7. package/dist/operations/providers/default.js +30 -1
  8. package/dist/operations/services/commit-message-provider.d.ts +33 -0
  9. package/dist/operations/services/commit-message-provider.js +319 -0
  10. package/dist/operations/services/config-runtime.js +50 -23
  11. package/dist/operations/services/git-remote-policy.d.ts +9 -0
  12. package/dist/operations/services/git-remote-policy.js +55 -0
  13. package/dist/operations/services/git-workflow.js +22 -3
  14. package/dist/operations/services/github-api.js +9 -4
  15. package/dist/operations/services/knowledge-coop-launch.js +4 -0
  16. package/dist/operations/services/local-dev.js +7 -2
  17. package/dist/operations/services/package-reference-policy.d.ts +70 -0
  18. package/dist/operations/services/package-reference-policy.js +330 -0
  19. package/dist/operations/services/project-platform.d.ts +4 -0
  20. package/dist/operations/services/project-platform.js +28 -4
  21. package/dist/operations/services/railway-deploy.d.ts +4 -1
  22. package/dist/operations/services/railway-deploy.js +76 -38
  23. package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
  24. package/dist/operations/services/repository-save-orchestrator.js +1462 -0
  25. package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
  26. package/dist/operations/services/workspace-dependency-mode.js +404 -0
  27. package/dist/operations/services/workspace-preflight.js +5 -0
  28. package/dist/operations/services/workspace-save.js +10 -6
  29. package/dist/operations-registry.js +5 -0
  30. package/dist/operations-types.d.ts +1 -0
  31. package/dist/platform/books-data.js +4 -1
  32. package/dist/platform/env.yaml +6 -3
  33. package/dist/reconcile/builtin-adapters.js +37 -7
  34. package/dist/scripts/cleanup-markdown.js +4 -0
  35. package/dist/scripts/prepare.js +14 -0
  36. package/dist/scripts/publish-package.js +5 -0
  37. package/dist/scripts/tenant-workflow-action.js +11 -2
  38. package/dist/verification.js +46 -13
  39. package/dist/workflow/operations.d.ts +381 -55
  40. package/dist/workflow/operations.js +725 -261
  41. package/dist/workflow-state.d.ts +40 -1
  42. package/dist/workflow-state.js +220 -17
  43. package/dist/workflow-support.d.ts +3 -0
  44. package/dist/workflow-support.js +34 -0
  45. package/dist/workflow.d.ts +19 -3
  46. package/dist/workflow.js +3 -3
  47. package/dist/wrangler-d1.js +6 -1
  48. package/package.json +17 -1
  49. package/templates/github/deploy.workflow.yml +59 -14
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import {
5
5
  applyTreeseedEnvironmentToProcess,
@@ -23,6 +23,7 @@ import {
23
23
  setTreeseedRemoteSession,
24
24
  writeTreeseedMachineConfig
25
25
  } from "../operations/services/config-runtime.js";
26
+ import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
26
27
  import { ControlPlaneClient } from "../control-plane-client.js";
27
28
  import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
28
29
  import {
@@ -64,13 +65,12 @@ import {
64
65
  syncBranchWithOrigin,
65
66
  waitForStagingAutomation
66
67
  } from "../operations/services/git-workflow.js";
67
- import { waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
68
+ import { getGitHubAutomationMode, resolveGitHubRepositorySlug, waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
69
+ import { createGitHubApiClient } from "../operations/services/github-api.js";
68
70
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
69
71
  import { runTenantDeployPreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
70
72
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
71
73
  import {
72
- applyWorkspaceVersionChanges,
73
- assertWorkspaceVersionConsistency,
74
74
  collectMergeConflictReport,
75
75
  currentBranch,
76
76
  formatMergeConflictReport,
@@ -81,11 +81,30 @@ import {
81
81
  planWorkspaceReleaseBump,
82
82
  repoRoot
83
83
  } from "../operations/services/workspace-save.js";
84
+ import {
85
+ planRepositorySave,
86
+ refreshAndValidateRootWorkspaceLockfileForSave,
87
+ repositorySaveErrorDetails,
88
+ runRepositorySaveOrchestrator
89
+ } from "../operations/services/repository-save-orchestrator.js";
90
+ import {
91
+ assertNoInternalDevReferences,
92
+ cleanupDevTags,
93
+ collectInternalDevReferenceIssues,
94
+ devTagFromDependencySpec,
95
+ rewriteProjectInternalDependenciesToStableVersions
96
+ } from "../operations/services/package-reference-policy.js";
97
+ import {
98
+ ensureLocalWorkspaceLinks,
99
+ inspectWorkspaceDependencyMode,
100
+ unlinkLocalWorkspaceLinks
101
+ } from "../operations/services/workspace-dependency-mode.js";
84
102
  import {
85
103
  changedWorkspacePackages,
86
104
  publishableWorkspacePackages,
87
105
  run,
88
106
  sortWorkspacePackages,
107
+ workspacePackages,
89
108
  workspaceRoot
90
109
  } from "../operations/services/workspace-tools.js";
91
110
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
@@ -129,6 +148,63 @@ function defaultWrite(output, stream = "stdout") {
129
148
  (stream === "stderr" ? process.stderr : process.stdout).write(`${output}
130
149
  `);
131
150
  }
151
+ function shouldManageWorkspaceLinks(mode, env = process.env) {
152
+ if (mode === "off") return false;
153
+ const envMode = String(env?.TREESEED_WORKSPACE_LINKS ?? "auto").trim().toLowerCase();
154
+ return envMode !== "off" && envMode !== "false" && envMode !== "0";
155
+ }
156
+ function ensureWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
157
+ if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
158
+ return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
159
+ }
160
+ const report = ensureLocalWorkspaceLinks(root, { mode, env: helpers.context.env });
161
+ if (report.created.length > 0) {
162
+ helpers.write(`[workspace][link] Linked ${report.created.length} local workspace package paths.`);
163
+ }
164
+ return report;
165
+ }
166
+ function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
167
+ if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
168
+ return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
169
+ }
170
+ const report = unlinkLocalWorkspaceLinks(root, { mode, env: helpers.context.env });
171
+ if (report.removed.length > 0) {
172
+ helpers.write(`[workspace][unlink] Removed ${report.removed.length} local workspace package links for deployment install.`);
173
+ }
174
+ return report;
175
+ }
176
+ function ensureTreeseedCommandReadiness(root) {
177
+ if (getGitHubAutomationMode() === "stub") {
178
+ return {
179
+ status: "skipped",
180
+ reason: "stubbed",
181
+ checks: [],
182
+ missing: []
183
+ };
184
+ }
185
+ const checks = [
186
+ { id: "sdk", path: resolve(root, "node_modules/@treeseed/sdk/package.json") },
187
+ { id: "sdk-workflow-support", path: resolve(root, "node_modules/@treeseed/sdk/dist/workflow-support.js") },
188
+ { id: "core", path: resolve(root, "node_modules/@treeseed/core/package.json") },
189
+ { id: "core-api", path: resolve(root, "node_modules/@treeseed/core/dist/api.js") },
190
+ { id: "cli", path: resolve(root, "node_modules/@treeseed/cli/package.json") },
191
+ { id: "cli-entrypoint", path: resolve(root, "node_modules/@treeseed/cli/dist/cli/main.js") },
192
+ { id: "trsd-bin", path: resolve(root, "node_modules/.bin/trsd") }
193
+ ];
194
+ const missing = checks.filter((check) => !existsSync(check.path));
195
+ const report = {
196
+ status: missing.length === 0 ? "passed" : "failed",
197
+ checks: checks.map((check) => ({ ...check, exists: existsSync(check.path) })),
198
+ missing
199
+ };
200
+ if (missing.length > 0) {
201
+ workflowError("save", "validation_failed", `Treeseed save restored workspace links, but command readiness failed.
202
+ ${missing.map((check) => `${check.id}: ${check.path}`).join("\n")}`, {
203
+ details: report
204
+ });
205
+ }
206
+ return report;
207
+ }
132
208
  function workflowError(operation, code, message, options = {}) {
133
209
  throw new TreeseedWorkflowError(operation, code, message, options);
134
210
  }
@@ -137,9 +213,9 @@ function ageDays(lastCommitDate) {
137
213
  if (!Number.isFinite(timestamp)) return null;
138
214
  return Math.max(0, Math.floor((Date.now() - timestamp) / 864e5));
139
215
  }
140
- function withContextEnv(env, action) {
216
+ async function withContextEnv(env, action) {
141
217
  if (!env) {
142
- return action();
218
+ return await action();
143
219
  }
144
220
  const previous = /* @__PURE__ */ new Map();
145
221
  for (const [key, value] of Object.entries(env)) {
@@ -151,7 +227,7 @@ function withContextEnv(env, action) {
151
227
  }
152
228
  }
153
229
  try {
154
- return action();
230
+ return await action();
155
231
  } finally {
156
232
  for (const [key, value] of previous.entries()) {
157
233
  if (value === void 0) {
@@ -293,19 +369,96 @@ function ensureLocalReadinessOrThrow(operation, tenantRoot) {
293
369
  }
294
370
  return state;
295
371
  }
296
- function bumpRootPackageJson(root, level) {
372
+ function planRootPackageVersion(root, level) {
373
+ const packageJsonPath = resolve(root, "package.json");
374
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
375
+ return incrementVersion(String(packageJson.version ?? "0.0.0"), level);
376
+ }
377
+ function setRootPackageJsonVersion(root, version) {
297
378
  const packageJsonPath = resolve(root, "package.json");
298
379
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
299
- packageJson.version = incrementVersion(String(packageJson.version ?? "0.0.0"), level);
380
+ packageJson.version = version;
300
381
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
301
382
  `, "utf8");
302
383
  return String(packageJson.version);
303
384
  }
385
+ function writeJsonFile(path, value) {
386
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
387
+ `, "utf8");
388
+ }
389
+ function applyStableWorkspaceVersionChanges(root, versions) {
390
+ for (const pkg of workspacePackages(root)) {
391
+ const packageJsonPath = resolve(pkg.dir, "package.json");
392
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
393
+ let changed = false;
394
+ const plannedVersion = versions.get(pkg.name);
395
+ if (plannedVersion && packageJson.version !== plannedVersion) {
396
+ packageJson.version = plannedVersion;
397
+ changed = true;
398
+ }
399
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
400
+ const values = packageJson[field];
401
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
402
+ for (const [dependencyName, version] of versions.entries()) {
403
+ if (!(dependencyName in values)) continue;
404
+ if (String(values[dependencyName]) === version) continue;
405
+ values[dependencyName] = version;
406
+ changed = true;
407
+ }
408
+ }
409
+ if (changed) {
410
+ writeJsonFile(packageJsonPath, packageJson);
411
+ }
412
+ }
413
+ }
414
+ function gitObjectCommit(repoDir, ref) {
415
+ try {
416
+ return run("git", ["rev-list", "-n", "1", ref], { cwd: repoDir, capture: true }).trim() || null;
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function remoteTagCommit(repoDir, tagName) {
422
+ const output = run("git", ["ls-remote", "origin", `refs/tags/${tagName}`, `refs/tags/${tagName}^{}`], { cwd: repoDir, capture: true }).trim();
423
+ if (!output) return null;
424
+ const peeled = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
425
+ const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
426
+ return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
427
+ }
428
+ function ensureReleaseTag(repoDir, tagName, commitSha) {
429
+ const localCommit = gitObjectCommit(repoDir, tagName);
430
+ if (localCommit && localCommit !== commitSha) {
431
+ throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
432
+ }
433
+ if (!localCommit) {
434
+ run("git", ["tag", "-a", tagName, commitSha, "-m", `release: ${tagName}`], { cwd: repoDir });
435
+ }
436
+ const remoteCommit = remoteTagCommit(repoDir, tagName);
437
+ if (remoteCommit && remoteCommit !== commitSha) {
438
+ throw new Error(`Release tag ${tagName} already exists on origin at ${remoteCommit}, expected ${commitSha}.`);
439
+ }
440
+ if (!remoteCommit) {
441
+ run("git", ["push", "origin", tagName], { cwd: repoDir });
442
+ }
443
+ return {
444
+ tagName,
445
+ local: localCommit ? "existing" : "created",
446
+ remote: remoteCommit ? "existing" : "pushed"
447
+ };
448
+ }
449
+ function commitAllIfChanged(repoDir, message) {
450
+ run("git", ["add", "-A"], { cwd: repoDir });
451
+ if (!hasMeaningfulChanges(repoDir)) {
452
+ return { committed: false, commitSha: headCommit(repoDir) };
453
+ }
454
+ run("git", ["commit", "-m", message], { cwd: repoDir });
455
+ return { committed: true, commitSha: headCommit(repoDir) };
456
+ }
304
457
  function createNextSteps(steps) {
305
458
  return steps.map(renderWorkflowStep);
306
459
  }
307
- function createStatusResult(cwd) {
308
- const state = resolveTreeseedWorkflowState(cwd);
460
+ function createStatusResult(cwd, options = {}) {
461
+ const state = resolveTreeseedWorkflowState(cwd, options);
309
462
  return buildWorkflowResult("status", cwd, state, {
310
463
  nextSteps: createNextSteps(state.recommendations),
311
464
  includeFinalState: false
@@ -555,13 +708,68 @@ function workflowSessionSnapshot(session) {
555
708
  function nextPendingJournalStep(journal) {
556
709
  return journal.steps.find((step) => step.status === "pending") ?? null;
557
710
  }
558
- async function executeJournalStep(root, runId, stepId, action) {
711
+ function findAutoResumableSaveRun(root, branch) {
712
+ if (!branch) return null;
713
+ return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
714
+ }
715
+ function stringRecord(value) {
716
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
717
+ }
718
+ function releasePlanHead(plan, repoName) {
719
+ if (repoName === "@treeseed/market") {
720
+ const rootRepo = stringRecord(plan.rootRepo);
721
+ return typeof rootRepo?.commitSha === "string" ? rootRepo.commitSha : null;
722
+ }
723
+ const repos = Array.isArray(plan.repos) ? plan.repos : [];
724
+ for (const repo of repos) {
725
+ const record = stringRecord(repo);
726
+ if (record?.name === repoName) {
727
+ return typeof record.commitSha === "string" ? record.commitSha : null;
728
+ }
729
+ }
730
+ return null;
731
+ }
732
+ function releasePlanMatchesCurrentHeads(plan, rootRepo, packageReports) {
733
+ if (releasePlanHead(plan, rootRepo.name) !== rootRepo.commitSha) {
734
+ return false;
735
+ }
736
+ const packageSelection = stringRecord(plan.packageSelection);
737
+ const selected = Array.isArray(packageSelection?.selected) ? packageSelection.selected.filter((name) => typeof name === "string") : packageReports.map((report) => report.name);
738
+ for (const name of selected) {
739
+ const current = packageReports.find((report) => report.name === name);
740
+ if (!current || releasePlanHead(plan, name) !== current.commitSha) {
741
+ return false;
742
+ }
743
+ }
744
+ return true;
745
+ }
746
+ function releaseRunHasCompletedMutation(journal) {
747
+ return journal.steps.some((step) => step.status === "completed" && step.id !== "release-plan" && step.id !== "workspace-unlink");
748
+ }
749
+ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
750
+ if (branch !== STAGING_BRANCH) return null;
751
+ return listInterruptedWorkflowRuns(root).find((journal) => {
752
+ if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
753
+ return false;
754
+ }
755
+ const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
756
+ const nextStep = nextPendingJournalStep(journal);
757
+ if (releaseRunHasCompletedMutation(journal)) {
758
+ if (nextStep?.id === "release-root" && releasePlanHead(releasePlan ?? {}, rootRepo.name) !== rootRepo.commitSha) {
759
+ return false;
760
+ }
761
+ return true;
762
+ }
763
+ return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
764
+ }) ?? null;
765
+ }
766
+ async function executeJournalStep(root, runId, stepId, action, options = {}) {
559
767
  const current = readWorkflowRunJournal(root, runId);
560
768
  const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
561
769
  if (!current || !step) {
562
770
  throw new Error(`Unknown workflow step "${stepId}" for run ${runId}.`);
563
771
  }
564
- if (step.status === "completed") {
772
+ if (step.status === "completed" && !options.rerunCompleted) {
565
773
  return step.data ?? null;
566
774
  }
567
775
  const data = await Promise.resolve(action());
@@ -707,6 +915,149 @@ function validateStagingWorkflowContracts(root) {
707
915
  });
708
916
  }
709
917
  }
918
+ function shouldSkipReleaseInstall() {
919
+ return process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
920
+ }
921
+ function runReleaseNpmInstall(repoDir, options = {}) {
922
+ if (shouldSkipReleaseInstall()) {
923
+ return { status: "skipped", reason: "stubbed" };
924
+ }
925
+ const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
926
+ run("npm", args, { cwd: repoDir });
927
+ return { status: "completed", reason: null };
928
+ }
929
+ function pathIsWithin(parent, candidate) {
930
+ const path = relative(parent, candidate);
931
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
932
+ }
933
+ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
934
+ const issues = collectInternalDevReferenceIssues(root, packageNames).filter((issue) => {
935
+ if (!pathIsWithin(repoDir, issue.filePath)) return false;
936
+ if (repoDir !== root) return true;
937
+ return !relative(root, issue.filePath).includes("/");
938
+ });
939
+ if (issues.length === 0) return;
940
+ const rendered = issues.map((issue) => `${issue.filePath}${issue.field ? ` ${issue.field}.${issue.dependencyName}` : ""}: ${issue.reason} ${issue.spec}`).join("\n");
941
+ throw new Error(`Stable release still contains internal Git/dev dependency references.
942
+ ${rendered}`);
943
+ }
944
+ function collectActiveDevTagReferences(root) {
945
+ return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
946
+ }
947
+ function releasePlanVersionMap(plannedVersions) {
948
+ return new Map(
949
+ Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
950
+ );
951
+ }
952
+ function releasePlanPackageSelection(value) {
953
+ const record = value && typeof value === "object" ? value : {};
954
+ return {
955
+ changed: Array.isArray(record.changed) ? record.changed.map(String) : [],
956
+ dependents: Array.isArray(record.dependents) ? record.dependents.map(String) : [],
957
+ selected: Array.isArray(record.selected) ? record.selected.map(String) : []
958
+ };
959
+ }
960
+ function buildReleasePlanSnapshot(input) {
961
+ const selectedPackageNames = new Set(input.packageSelection.selected);
962
+ const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames } : {});
963
+ const rootVersion = planRootPackageVersion(input.root, input.level);
964
+ const plannedVersions = {
965
+ "@treeseed/market": rootVersion,
966
+ ...Object.fromEntries(versionPlan.versions.entries())
967
+ };
968
+ const plannedDevReferenceRewrites = input.mode === "recursive-workspace" ? collectInternalDevReferenceIssues(input.root, selectedPackageNames) : [];
969
+ return {
970
+ mode: input.mode,
971
+ mergeStrategy: "merge-commit",
972
+ level: input.level,
973
+ rootVersion,
974
+ releaseTag: rootVersion,
975
+ stagingBranch: STAGING_BRANCH,
976
+ productionBranch: PRODUCTION_BRANCH,
977
+ packageSelection: input.packageSelection,
978
+ plannedVersions,
979
+ plannedDevReferenceRewrites,
980
+ plannedPublishWaits: input.packageSelection.selected.map((name) => ({
981
+ name,
982
+ workflow: "publish.yml",
983
+ branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
984
+ status: "planned"
985
+ })),
986
+ touchedPackages: input.packageSelection.selected,
987
+ repos: input.packageReports,
988
+ rootRepo: input.rootRepo,
989
+ finalBranch: STAGING_BRANCH,
990
+ plannedSteps: [
991
+ { id: "release-plan", description: "Record immutable release plan and target versions" },
992
+ { id: "workspace-unlink", description: "Remove local workspace links before stable release install" },
993
+ { id: "prepare-release-metadata", description: "Rewrite package metadata and lockfiles to production dependency mode" },
994
+ ...input.packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
995
+ id: `release-${report.name}`,
996
+ description: `Release ${report.name} from staging to main and tag ${plannedVersions[report.name] ?? "(planned)"}`
997
+ })),
998
+ { id: "release-root", description: `Release market ${rootVersion}` },
999
+ { id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
1000
+ { id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
1001
+ ],
1002
+ blockers: input.blockers
1003
+ };
1004
+ }
1005
+ function collectReleasePlanBlockers(session, mode, selectedPackageNames) {
1006
+ const blockers = [];
1007
+ if (session.branchName !== STAGING_BRANCH) {
1008
+ blockers.push("Release must start from staging.");
1009
+ }
1010
+ if (session.rootRepo.dirty) {
1011
+ blockers.push("@treeseed/market has uncommitted changes.");
1012
+ }
1013
+ if (!session.rootRepo.hasOriginRemote) {
1014
+ blockers.push("@treeseed/market is missing origin remote.");
1015
+ }
1016
+ if (mode === "recursive-workspace") {
1017
+ for (const repo of session.packageRepos) {
1018
+ if (!selectedPackageNames.includes(repo.name)) continue;
1019
+ if (repo.detached) blockers.push(`${repo.name} is detached.`);
1020
+ if (repo.branchName !== STAGING_BRANCH) blockers.push(`${repo.name} is on ${repo.branchName ?? "(detached)"} instead of staging.`);
1021
+ if (repo.dirty) blockers.push(`${repo.name} has uncommitted changes.`);
1022
+ if (!repo.hasOriginRemote) blockers.push(`${repo.name} is missing origin remote.`);
1023
+ }
1024
+ try {
1025
+ validatePackageReleaseWorkflows(session.root, selectedPackageNames);
1026
+ } catch (error) {
1027
+ blockers.push(error instanceof Error ? error.message : String(error));
1028
+ }
1029
+ }
1030
+ return blockers;
1031
+ }
1032
+ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
1033
+ if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1034
+ return;
1035
+ }
1036
+ createGitHubApiClient();
1037
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1038
+ if (!selectedPackageNames.has(pkg.name)) continue;
1039
+ resolveGitHubRepositorySlug(pkg.dir);
1040
+ }
1041
+ }
1042
+ function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
1043
+ if (!workflow || workflow.status !== "completed") {
1044
+ return;
1045
+ }
1046
+ if (workflow.conclusion === "success") {
1047
+ return;
1048
+ }
1049
+ const workflowName = typeof workflow.workflow === "string" ? workflow.workflow : "publish.yml";
1050
+ const repository = typeof workflow.repository === "string" ? workflow.repository : packageName;
1051
+ const url = typeof workflow.url === "string" && workflow.url ? `
1052
+ ${workflow.url}` : "";
1053
+ const conclusion = typeof workflow.conclusion === "string" && workflow.conclusion ? workflow.conclusion : "unknown";
1054
+ workflowError("release", "github_workflow_failed", `${packageName} ${workflowName} completed with conclusion ${conclusion} in ${repository}.${url}`, {
1055
+ details: {
1056
+ packageName,
1057
+ workflow
1058
+ }
1059
+ });
1060
+ }
710
1061
  function assertSessionBranchSafety(operation, session, {
711
1062
  requireCleanPackages = false,
712
1063
  requireCurrentBranch = false,
@@ -1011,8 +1362,26 @@ function collectReleasePackageSelection(root) {
1011
1362
  function hasStagedChanges(repoDir) {
1012
1363
  return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
1013
1364
  }
1014
- async function workflowStatus(helpers) {
1015
- return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
1365
+ async function workflowStatus(helpers, input = {}) {
1366
+ return withContextEnv(helpers.context.env, async () => {
1367
+ const resolved = resolveTreeseedWorkflowPaths(helpers.cwd());
1368
+ if (resolved.tenantRoot) {
1369
+ try {
1370
+ await ensureTreeseedSecretSessionForConfig({
1371
+ tenantRoot: resolved.cwd,
1372
+ interactive: false,
1373
+ env: helpers.context.env,
1374
+ createIfMissing: false,
1375
+ allowMigration: false
1376
+ });
1377
+ } catch {
1378
+ }
1379
+ }
1380
+ return createStatusResult(helpers.cwd(), {
1381
+ ...input,
1382
+ env: input.env ?? helpers.context.env
1383
+ });
1384
+ });
1016
1385
  }
1017
1386
  async function workflowTasks(helpers) {
1018
1387
  return withContextEnv(helpers.context.env, () => createTasksResult(helpers.cwd()));
@@ -1034,6 +1403,21 @@ async function workflowConfig(helpers, input = {}) {
1034
1403
  const bootstrapSystemsInput = input.systems;
1035
1404
  const skipUnavailable = input.skipUnavailable;
1036
1405
  const bootstrapExecution = input.bootstrapExecution ?? "parallel";
1406
+ const dependencyInstall = await installTreeseedDependencies({
1407
+ tenantRoot,
1408
+ force: input.installMissingTooling === true,
1409
+ env: helpers.context.env,
1410
+ write: (line) => maybePrint(helpers.write, line)
1411
+ });
1412
+ if (!dependencyInstall.ok) {
1413
+ workflowError(
1414
+ "config",
1415
+ "validation_failed",
1416
+ `Treeseed dependency initialization failed:
1417
+ - ${formatTreeseedDependencyFailureDetails(dependencyInstall)}`,
1418
+ { details: { dependencies: dependencyInstall } }
1419
+ );
1420
+ }
1037
1421
  const repairs = input.repair === false ? [] : resolveTreeseedWorkflowState(tenantRoot).deployConfigPresent ? applyTreeseedSafeRepairs(tenantRoot) : [];
1038
1422
  const toolHealth = ensureTreeseedActVerificationTooling({
1039
1423
  tenantRoot,
@@ -1367,6 +1751,7 @@ async function workflowSwitch(helpers, input) {
1367
1751
  plannedSteps: [
1368
1752
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
1369
1753
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
1754
+ { id: "workspace-link", description: "Apply local workspace links for integrated development" },
1370
1755
  ...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
1371
1756
  ]
1372
1757
  },
@@ -1398,6 +1783,7 @@ async function workflowSwitch(helpers, input) {
1398
1783
  branch: branchName,
1399
1784
  resumable: true
1400
1785
  })),
1786
+ { id: "workspace-link", description: "Apply local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
1401
1787
  ...preview ? [{ id: "preview", description: `Provision or refresh preview ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true }] : []
1402
1788
  ],
1403
1789
  helpers.context
@@ -1437,6 +1823,7 @@ async function workflowSwitch(helpers, input) {
1437
1823
  report.commitSha = headCommit(pkg.dir);
1438
1824
  report.dirty = hasMeaningfulChanges(pkg.dir);
1439
1825
  }
1826
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
1440
1827
  const stateAfterSwitch = resolveTreeseedWorkflowState(root);
1441
1828
  if (preview) {
1442
1829
  previewResult = await executeJournalStep(
@@ -1461,6 +1848,7 @@ async function workflowSwitch(helpers, input) {
1461
1848
  lastDeploymentTimestamp: state.preview.lastDeploymentTimestamp
1462
1849
  },
1463
1850
  previewResult,
1851
+ workspaceLinks,
1464
1852
  preconditions: {
1465
1853
  cleanWorktreeRequired: true,
1466
1854
  baseBranch: STAGING_BRANCH
@@ -1502,6 +1890,7 @@ async function workflowDev(helpers, input = {}) {
1502
1890
  workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
1503
1891
  }
1504
1892
  const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
1893
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
1505
1894
  const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
1506
1895
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
1507
1896
  assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
@@ -1537,7 +1926,8 @@ async function workflowDev(helpers, input = {}) {
1537
1926
  apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
1538
1927
  webUrl: "http://127.0.0.1:8787"
1539
1928
  },
1540
- readiness: readiness.readiness.local
1929
+ readiness: readiness.readiness.local,
1930
+ workspaceLinks
1541
1931
  });
1542
1932
  }
1543
1933
  const result = spawnSync(process.execPath, args, {
@@ -1558,7 +1948,8 @@ async function workflowDev(helpers, input = {}) {
1558
1948
  apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
1559
1949
  webUrl: "http://127.0.0.1:8787"
1560
1950
  },
1561
- readiness: readiness.readiness.local
1951
+ readiness: readiness.readiness.local,
1952
+ workspaceLinks
1562
1953
  });
1563
1954
  });
1564
1955
  } catch (error) {
@@ -1569,8 +1960,6 @@ async function workflowSave(helpers, input) {
1569
1960
  try {
1570
1961
  return await withContextEnv(helpers.context.env, async () => {
1571
1962
  const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
1572
- const message = ensureMessage("save", input.message, "a commit message");
1573
- const optionsHotfix = input.hotfix === true;
1574
1963
  const root = workspaceRoot(tenantRoot);
1575
1964
  const session = resolveTreeseedWorkflowSession(root);
1576
1965
  const gitRoot = session.gitRoot;
@@ -1580,6 +1969,12 @@ async function workflowSave(helpers, input) {
1580
1969
  const recursiveWorkspace = session.mode === "recursive-workspace";
1581
1970
  const mode = session.mode;
1582
1971
  const executionMode = normalizeExecutionMode(input);
1972
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
1973
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
1974
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableSaveRun(root, branch) : null;
1975
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
1976
+ const message = String(effectiveInput.message ?? "").trim();
1977
+ const optionsHotfix = effectiveInput.hotfix === true;
1583
1978
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
1584
1979
  if (!branch) {
1585
1980
  workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
@@ -1597,13 +1992,20 @@ async function workflowSave(helpers, input) {
1597
1992
  if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
1598
1993
  blockers.push("Main saves require --hotfix.");
1599
1994
  }
1600
- if (recursiveWorkspace) {
1601
- try {
1602
- assertWorkspaceVersionConsistency(root);
1603
- } catch (error) {
1604
- blockers.push(error instanceof Error ? error.message : String(error));
1605
- }
1606
- }
1995
+ const repositoryPlan = planRepositorySave({
1996
+ root,
1997
+ gitRoot,
1998
+ branch,
1999
+ message,
2000
+ bump: effectiveInput.bump ?? "patch",
2001
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2002
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2003
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2004
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2005
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2006
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
2007
+ });
2008
+ const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
1607
2009
  return buildWorkflowResult(
1608
2010
  "save",
1609
2011
  root,
@@ -1613,21 +2015,30 @@ async function workflowSave(helpers, input) {
1613
2015
  scope,
1614
2016
  hotfix: optionsHotfix,
1615
2017
  message,
1616
- repos: packageReports,
1617
- rootRepo,
2018
+ repos: repositoryPlan.repos,
2019
+ rootRepo: repositoryPlan.rootRepo,
1618
2020
  blockers,
2021
+ autoResumeCandidate: planAutoResumeRun ? {
2022
+ runId: planAutoResumeRun.runId,
2023
+ branch: planAutoResumeRun.session.branchName,
2024
+ failure: planAutoResumeRun.failure
2025
+ } : null,
2026
+ workspaceLinks,
2027
+ repositoryPlan,
2028
+ waves: repositoryPlan.waves,
2029
+ plannedVersions: repositoryPlan.plannedVersions,
1619
2030
  plannedSteps: [
1620
- ...packageReports.map((report) => ({ id: `save-${report.name}`, description: `Verify, commit, and push ${report.name}` })),
1621
- ...input.verify !== false ? [{ id: "verify-root", description: "Run market workspace verification" }] : [],
1622
- { id: "commit-root", description: "Commit market repo changes if present" },
1623
- { id: "sync-root", description: `Push ${branch} to origin` },
1624
- ...beforeState.branchRole === "feature" && (input.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2031
+ { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
2032
+ ...repositoryPlan.plannedSteps,
2033
+ { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2034
+ { id: "workspace-link", description: "Restore local workspace links after save" },
2035
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
1625
2036
  ]
1626
2037
  },
1627
2038
  {
1628
2039
  executionMode,
1629
2040
  nextSteps: createNextSteps([
1630
- { operation: "save", reason: "Run without --plan to persist the workspace checkpoint.", input: { message, hotfix: optionsHotfix, preview: input.preview === true } }
2041
+ { operation: "save", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to persist the workspace checkpoint.", input: { message, hotfix: optionsHotfix, preview: effectiveInput.preview === true } }
1631
2042
  ])
1632
2043
  }
1633
2044
  );
@@ -1646,44 +2057,28 @@ async function workflowSave(helpers, input) {
1646
2057
  {
1647
2058
  message,
1648
2059
  hotfix: optionsHotfix,
1649
- preview: input.preview === true,
1650
- refreshPreview: input.refreshPreview !== false,
1651
- verify: input.verify !== false
2060
+ preview: effectiveInput.preview === true,
2061
+ refreshPreview: effectiveInput.refreshPreview !== false,
2062
+ verify: effectiveInput.verify !== false,
2063
+ bump: effectiveInput.bump ?? "patch",
2064
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2065
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2066
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2067
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2068
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2069
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2070
+ workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
1652
2071
  },
1653
2072
  [
1654
- ...packageReports.map((report) => ({
1655
- id: `save-${report.name}`,
1656
- description: `Save ${report.name}`,
1657
- repoName: report.name,
1658
- repoPath: report.path,
1659
- branch,
1660
- resumable: true
1661
- })),
1662
- ...input.verify !== false ? [{
1663
- id: "verify-root",
1664
- description: "Verify market workspace",
1665
- repoName: rootRepo.name,
1666
- repoPath: rootRepo.path,
1667
- branch,
1668
- resumable: true
1669
- }] : [],
1670
- {
1671
- id: "commit-root",
1672
- description: "Commit market workspace changes",
1673
- repoName: rootRepo.name,
1674
- repoPath: rootRepo.path,
1675
- branch,
1676
- resumable: true
1677
- },
1678
2073
  {
1679
- id: "sync-root",
1680
- description: `Push ${branch} to origin`,
2074
+ id: "save-repositories",
2075
+ description: "Save dependency-ordered repositories",
1681
2076
  repoName: rootRepo.name,
1682
2077
  repoPath: rootRepo.path,
1683
2078
  branch,
1684
2079
  resumable: true
1685
2080
  },
1686
- ...beforeState.branchRole === "feature" && (input.preview === true || input.refreshPreview !== false && beforeState.preview.enabled) ? [{
2081
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && beforeState.preview.enabled) ? [{
1687
2082
  id: "preview",
1688
2083
  description: `Refresh preview ${branch}`,
1689
2084
  repoName: rootRepo.name,
@@ -1692,105 +2087,66 @@ async function workflowSave(helpers, input) {
1692
2087
  resumable: true
1693
2088
  }] : []
1694
2089
  ],
1695
- helpers.context
2090
+ autoResumeRun ? {
2091
+ ...helpers.context,
2092
+ workflow: {
2093
+ ...helpers.context.workflow ?? {},
2094
+ resumeRunId: autoResumeRun.runId
2095
+ }
2096
+ } : helpers.context
1696
2097
  );
2098
+ if (autoResumeRun) {
2099
+ helpers.write(`[workflow][resume] Resuming interrupted save ${autoResumeRun.runId} on ${branch}.`);
2100
+ }
1697
2101
  try {
1698
- if (recursiveWorkspace) {
1699
- assertWorkspaceVersionConsistency(root);
1700
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1701
- const report = findReportByName(packageReports, pkg.name);
1702
- if (!report) {
1703
- continue;
1704
- }
1705
- try {
1706
- const step = readWorkflowRunJournal(root, workflowRun.runId)?.steps.find((entry) => entry.id === `save-${report.name}`) ?? null;
1707
- const resumePendingSync = workflowRun.resumed && step?.status === "pending" && branchNeedsSync(report.path, branch);
1708
- if (!report.dirty && !resumePendingSync) {
1709
- report.skippedReason = "clean";
1710
- skipJournalStep(root, workflowRun.runId, `save-${report.name}`, {
1711
- skippedReason: "clean"
1712
- });
1713
- continue;
1714
- }
1715
- const savedReport = await executeJournalStep(root, workflowRun.runId, `save-${report.name}`, () => savePackageRepo(report, message, branch, input.verify !== false));
1716
- Object.assign(report, savedReport);
1717
- } catch (error) {
1718
- createSaveFailure(
1719
- `Treeseed save stopped while saving workspace package ${pkg.name}.`,
1720
- packageReports,
1721
- rootRepo,
1722
- report,
1723
- error
1724
- );
1725
- }
1726
- }
1727
- }
1728
- if (input.verify !== false) {
2102
+ const saveResult = await executeJournalStep(root, workflowRun.runId, "save-repositories", () => (async () => {
2103
+ unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
1729
2104
  try {
1730
- await executeJournalStep(root, workflowRun.runId, "verify-root", () => {
1731
- runWorkspaceSavePreflight({ cwd: root });
1732
- rootRepo.verified = true;
1733
- return {
1734
- verified: true
1735
- };
2105
+ return await runRepositorySaveOrchestrator({
2106
+ root,
2107
+ gitRoot,
2108
+ branch,
2109
+ message,
2110
+ bump: effectiveInput.bump ?? "patch",
2111
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2112
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2113
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2114
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2115
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2116
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2117
+ workflowRunId: workflowRun.runId,
2118
+ onProgress: (line, stream) => helpers.write(line, stream)
1736
2119
  });
1737
- } catch (error) {
1738
- createSaveFailure(
1739
- "Treeseed save stopped while verifying the market workspace.",
1740
- packageReports,
1741
- rootRepo,
1742
- null,
1743
- error
1744
- );
2120
+ } finally {
2121
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
1745
2122
  }
1746
- }
1747
- const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
1748
- let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
1749
- let commitCreated = false;
1750
- if (hadMeaningfulChanges) {
1751
- const commitResult = await executeJournalStep(root, workflowRun.runId, "commit-root", () => {
1752
- run("git", ["add", "-A"], { cwd: gitRoot });
1753
- run("git", ["commit", "-m", message], { cwd: gitRoot });
1754
- return {
1755
- commitSha: run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim()
1756
- };
1757
- });
1758
- head = String(commitResult?.commitSha ?? head);
1759
- commitCreated = true;
1760
- rootRepo.committed = true;
1761
- } else {
1762
- skipJournalStep(root, workflowRun.runId, "commit-root", {
1763
- skippedReason: "clean"
1764
- });
1765
- }
1766
- rootRepo.commitSha = head;
1767
- let branchSync;
1768
- try {
1769
- branchSync = await executeJournalStep(root, workflowRun.runId, "sync-root", () => syncCurrentBranchToOrigin("save", gitRoot, branch));
1770
- } catch (error) {
1771
- createSaveFailure(
1772
- "Treeseed save stopped while syncing the market repository.",
1773
- packageReports,
1774
- rootRepo,
1775
- rootRepo,
1776
- error
1777
- );
1778
- }
1779
- rootRepo.pushed = branchSync.pushed === true;
1780
- if (input.verify !== false) {
1781
- rootRepo.verified = true;
1782
- }
1783
- if (!hadMeaningfulChanges) {
1784
- rootRepo.skippedReason = "clean";
1785
- }
2123
+ })());
2124
+ const savedPackageReports = saveResult?.repos ?? packageReports;
2125
+ const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
2126
+ const head = savedRootRepo.commitSha ?? run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
2127
+ const commitCreated = savedRootRepo.committed === true;
2128
+ const branchSync = {
2129
+ ...savedRootRepo.publishWait ?? {},
2130
+ pushed: savedRootRepo.pushed === true
2131
+ };
2132
+ const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
2133
+ const commandReadiness = ensureTreeseedCommandReadiness(root);
2134
+ const lockfileValidation = {
2135
+ root: savedRootRepo.lockfileValidation,
2136
+ repos: savedPackageReports.map((repo) => ({
2137
+ name: repo.name,
2138
+ path: repo.path,
2139
+ lockfileValidation: repo.lockfileValidation
2140
+ }))
2141
+ };
1786
2142
  let previewAction = { status: "skipped" };
1787
2143
  if (beforeState.branchRole === "feature" && branch) {
1788
- if (input.preview === true) {
2144
+ if (effectiveInput.preview === true) {
1789
2145
  previewAction = {
1790
2146
  status: beforeState.preview.enabled ? "refreshed" : "created",
1791
2147
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
1792
2148
  };
1793
- } else if (input.refreshPreview !== false && beforeState.preview.enabled) {
2149
+ } else if (effectiveInput.refreshPreview !== false && beforeState.preview.enabled) {
1794
2150
  previewAction = {
1795
2151
  status: "refreshed",
1796
2152
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
@@ -1798,20 +2154,28 @@ async function workflowSave(helpers, input) {
1798
2154
  }
1799
2155
  }
1800
2156
  const payload = {
1801
- mode,
2157
+ mode: saveResult?.mode ?? mode,
1802
2158
  branch,
1803
2159
  scope,
1804
2160
  hotfix: optionsHotfix,
1805
2161
  message,
2162
+ resumed: workflowRun.resumed,
2163
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2164
+ autoResumed: autoResumeRun != null,
1806
2165
  commitSha: head,
1807
2166
  commitCreated,
1808
- noChanges: !hadMeaningfulChanges,
2167
+ noChanges: !commitCreated,
1809
2168
  branchSync,
1810
- repos: packageReports,
1811
- rootRepo,
2169
+ repos: savedPackageReports,
2170
+ rootRepo: savedRootRepo,
2171
+ waves: saveResult?.waves ?? [],
2172
+ plannedVersions: saveResult?.plannedVersions ?? {},
1812
2173
  partialFailure: null,
1813
2174
  previewAction,
1814
- mergeConflict: null
2175
+ mergeConflict: null,
2176
+ workspaceLinks,
2177
+ commandReadiness,
2178
+ lockfileValidation
1815
2179
  };
1816
2180
  completeWorkflowRun(root, workflowRun.runId, payload);
1817
2181
  return buildWorkflowResult(
@@ -1826,7 +2190,9 @@ async function workflowSave(helpers, input) {
1826
2190
  }
1827
2191
  );
1828
2192
  } catch (error) {
1829
- const failingRepo = packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
2193
+ const saveError = repositorySaveErrorDetails(error);
2194
+ const savedPartialFailure = saveError.details?.partialFailure;
2195
+ const failingRepo = savedPartialFailure?.repos.find((report) => report.name === savedPartialFailure.failingRepo) ?? packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
1830
2196
  const wrappedError = error instanceof TreeseedWorkflowError && error.details?.partialFailure != null ? error : new TreeseedWorkflowError(
1831
2197
  "save",
1832
2198
  error instanceof TreeseedWorkflowError ? error.code : "unsupported_state",
@@ -1834,7 +2200,8 @@ async function workflowSave(helpers, input) {
1834
2200
  {
1835
2201
  details: {
1836
2202
  ...error instanceof TreeseedWorkflowError ? error.details ?? {} : {},
1837
- partialFailure: {
2203
+ ...saveError.details ?? {},
2204
+ partialFailure: savedPartialFailure ?? {
1838
2205
  message: "Treeseed save stopped before the workspace could finish syncing.",
1839
2206
  failingRepo: failingRepo.name,
1840
2207
  repos: packageReports,
@@ -1842,7 +2209,7 @@ async function workflowSave(helpers, input) {
1842
2209
  error: error instanceof Error ? error.message : String(error)
1843
2210
  }
1844
2211
  },
1845
- exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : void 0
2212
+ exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : saveError.exitCode
1846
2213
  }
1847
2214
  );
1848
2215
  failWorkflowRun(root, workflowRun.runId, wrappedError, {
@@ -1888,7 +2255,8 @@ async function workflowClose(helpers, input) {
1888
2255
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
1889
2256
  id: `cleanup-${pkg.name}`,
1890
2257
  description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
1891
- }))
2258
+ })),
2259
+ { id: "workspace-link", description: "Restore local workspace links on the final branch" }
1892
2260
  ]
1893
2261
  },
1894
2262
  {
@@ -1899,10 +2267,12 @@ async function workflowClose(helpers, input) {
1899
2267
  }
1900
2268
  );
1901
2269
  }
2270
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1902
2271
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
1903
2272
  message,
1904
2273
  autoSave: input.autoSave
1905
2274
  });
2275
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1906
2276
  const activeSession = resolveTreeseedWorkflowSession(root);
1907
2277
  const featureBranch = assertFeatureBranch(root);
1908
2278
  const mode = activeSession.mode;
@@ -1967,6 +2337,7 @@ async function workflowClose(helpers, input) {
1967
2337
  }));
1968
2338
  Object.assign(report, cleanup);
1969
2339
  }
2340
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1970
2341
  const payload = {
1971
2342
  mode,
1972
2343
  branchName: featureBranch,
@@ -1979,7 +2350,8 @@ async function workflowClose(helpers, input) {
1979
2350
  previewCleanup,
1980
2351
  remoteDeleted: rootRepo.deletedRemote,
1981
2352
  localDeleted: rootRepo.deletedLocal,
1982
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH
2353
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2354
+ workspaceLinks
1983
2355
  };
1984
2356
  completeWorkflowRun(root, workflowRun.runId, payload);
1985
2357
  return buildWorkflowResult(
@@ -1994,6 +2366,7 @@ async function workflowClose(helpers, input) {
1994
2366
  }
1995
2367
  );
1996
2368
  } catch (error) {
2369
+ ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1997
2370
  failWorkflowRun(root, workflowRun.runId, error, {
1998
2371
  resumable: true,
1999
2372
  runId: workflowRun.runId,
@@ -2042,14 +2415,17 @@ async function workflowStage(helpers, input) {
2042
2415
  id: `merge-${pkg.name}`,
2043
2416
  description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
2044
2417
  })),
2418
+ { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
2045
2419
  { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
2420
+ { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
2046
2421
  { id: "wait-staging", description: "Wait for staging automation" },
2047
2422
  { id: "preview-cleanup", description: "Destroy preview resources" },
2048
2423
  { id: "cleanup-root", description: "Archive and delete the task branch from market" },
2049
2424
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
2050
2425
  id: `cleanup-${pkg.name}`,
2051
2426
  description: `Archive and delete the task branch from ${pkg.name}`
2052
- }))
2427
+ })),
2428
+ { id: "workspace-link", description: "Restore local workspace links on staging" }
2053
2429
  ]
2054
2430
  },
2055
2431
  {
@@ -2060,10 +2436,12 @@ async function workflowStage(helpers, input) {
2060
2436
  }
2061
2437
  );
2062
2438
  }
2439
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2063
2440
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
2064
2441
  message,
2065
2442
  autoSave: input.autoSave
2066
2443
  });
2444
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2067
2445
  const session = resolveTreeseedWorkflowSession(root);
2068
2446
  const featureBranch = assertFeatureBranch(root);
2069
2447
  const mode = session.mode;
@@ -2107,6 +2485,7 @@ async function workflowStage(helpers, input) {
2107
2485
  helpers.context
2108
2486
  );
2109
2487
  try {
2488
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2110
2489
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2111
2490
  const report = findReportByName(packageReports, pkg.name);
2112
2491
  if (!report) {
@@ -2134,14 +2513,21 @@ async function workflowStage(helpers, input) {
2134
2513
  });
2135
2514
  }
2136
2515
  }
2516
+ let rootMerge = null;
2137
2517
  try {
2138
- const rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", () => {
2518
+ rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
2139
2519
  assertCleanWorktree(root);
2140
2520
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2141
2521
  run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
2142
2522
  if (mode === "recursive-workspace") {
2143
2523
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2144
2524
  }
2525
+ const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
2526
+ root,
2527
+ gitRoot: repoDir,
2528
+ branch: STAGING_BRANCH,
2529
+ onProgress: (line, stream) => helpers.write(line, stream)
2530
+ });
2145
2531
  if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
2146
2532
  run("git", ["add", "-A"], { cwd: repoDir });
2147
2533
  run("git", ["commit", "-m", message], { cwd: repoDir });
@@ -2150,7 +2536,9 @@ async function workflowStage(helpers, input) {
2150
2536
  return {
2151
2537
  commitSha: headCommit(repoDir),
2152
2538
  branch: currentBranch(repoDir) || STAGING_BRANCH,
2153
- committed: hasMeaningfulChanges(repoDir) ? false : true
2539
+ committed: hasMeaningfulChanges(repoDir) ? false : true,
2540
+ lockfileValidation: lockfileSafety.lockfileValidation,
2541
+ lockfileInstall: lockfileSafety.install
2154
2542
  };
2155
2543
  });
2156
2544
  rootRepo.merged = true;
@@ -2196,6 +2584,7 @@ async function workflowStage(helpers, input) {
2196
2584
  }));
2197
2585
  Object.assign(report, cleanup);
2198
2586
  }
2587
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2199
2588
  const payload = {
2200
2589
  mode,
2201
2590
  branchName: featureBranch,
@@ -2209,9 +2598,12 @@ async function workflowStage(helpers, input) {
2209
2598
  rootRepo,
2210
2599
  stagingWait,
2211
2600
  previewCleanup,
2601
+ lockfileValidation: rootMerge?.lockfileValidation ?? null,
2602
+ lockfileInstall: rootMerge?.lockfileInstall ?? null,
2212
2603
  remoteDeleted: rootRepo.deletedRemote,
2213
2604
  localDeleted: rootRepo.deletedLocal,
2214
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH
2605
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2606
+ workspaceLinks
2215
2607
  };
2216
2608
  completeWorkflowRun(root, workflowRun.runId, payload);
2217
2609
  return buildWorkflowResult(
@@ -2227,6 +2619,7 @@ async function workflowStage(helpers, input) {
2227
2619
  }
2228
2620
  );
2229
2621
  } catch (error) {
2622
+ ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2230
2623
  failWorkflowRun(root, workflowRun.runId, error, {
2231
2624
  resumable: true,
2232
2625
  runId: workflowRun.runId,
@@ -2245,7 +2638,6 @@ async function workflowStage(helpers, input) {
2245
2638
  async function workflowRelease(helpers, input) {
2246
2639
  try {
2247
2640
  return await withContextEnv(helpers.context.env, async () => {
2248
- const level = input.bump ?? "patch";
2249
2641
  const root = resolveProjectRootOrThrow("release", helpers.cwd());
2250
2642
  const session = resolveTreeseedWorkflowSession(root);
2251
2643
  const gitRoot = session.gitRoot;
@@ -2253,67 +2645,58 @@ async function workflowRelease(helpers, input) {
2253
2645
  const executionMode = normalizeExecutionMode(input);
2254
2646
  const rootRepo = createWorkspaceRootRepoReport(root);
2255
2647
  const packageReports = createWorkspacePackageReports(root);
2648
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2649
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2650
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2651
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2652
+ const level = effectiveInput.bump ?? "patch";
2653
+ const isResume = Boolean(explicitResumeRunId || autoResumeRun);
2256
2654
  const packageSelection = session.packageSelection;
2257
2655
  const selectedPackageNames = new Set(packageSelection.selected);
2258
- const blockers = [];
2259
- if (session.branchName !== STAGING_BRANCH) {
2260
- blockers.push("Release must start from staging.");
2261
- }
2262
- if (mode === "recursive-workspace") {
2263
- try {
2264
- assertWorkspaceVersionConsistency(root);
2265
- validatePackageReleaseWorkflows(root, packageSelection.selected);
2266
- } catch (error) {
2267
- blockers.push(error instanceof Error ? error.message : String(error));
2268
- }
2269
- }
2270
- const versionPlan = planWorkspaceReleaseBump(level, root, mode === "recursive-workspace" ? { selectedPackageNames } : {});
2271
- const plannedVersions = Object.fromEntries(versionPlan.versions.entries());
2656
+ const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, packageSelection.selected);
2657
+ const plannedRelease = buildReleasePlanSnapshot({
2658
+ root,
2659
+ mode,
2660
+ level,
2661
+ packageSelection,
2662
+ packageReports,
2663
+ rootRepo,
2664
+ blockers
2665
+ });
2272
2666
  if (executionMode === "plan") {
2273
- return buildWorkflowResult(
2274
- "release",
2275
- root,
2276
- {
2277
- mode,
2278
- mergeStrategy: "merge-commit",
2279
- level,
2280
- stagingBranch: STAGING_BRANCH,
2281
- productionBranch: PRODUCTION_BRANCH,
2282
- packageSelection,
2283
- plannedVersions,
2284
- repos: packageReports,
2285
- rootRepo,
2286
- plannedSteps: [
2287
- ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
2288
- id: `release-${report.name}`,
2289
- description: `Release ${report.name} from staging to main and tag ${plannedVersions[report.name] ?? "(planned)"}`
2290
- })),
2291
- { id: "release-root", description: `Release market ${plannedVersions["@treeseed/market"] ?? "(planned)"}` }
2292
- ],
2293
- blockers
2294
- },
2295
- {
2296
- executionMode,
2297
- nextSteps: createNextSteps([
2298
- { operation: "release", reason: "Run without --plan to promote staging into production.", input: { bump: level } }
2299
- ])
2300
- }
2301
- );
2667
+ return buildWorkflowResult("release", root, {
2668
+ ...plannedRelease,
2669
+ autoResumeCandidate: planAutoResumeRun ? {
2670
+ runId: planAutoResumeRun.runId,
2671
+ branch: planAutoResumeRun.session.branchName,
2672
+ failure: planAutoResumeRun.failure
2673
+ } : null
2674
+ }, {
2675
+ executionMode,
2676
+ nextSteps: createNextSteps([
2677
+ { operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
2678
+ ])
2679
+ });
2302
2680
  }
2303
2681
  if (blockers.length > 0) {
2304
2682
  workflowError("release", "validation_failed", blockers.join("\n"), {
2305
2683
  details: { blockers }
2306
2684
  });
2307
2685
  }
2308
- assertSessionBranchSafety("release", session);
2309
- prepareReleaseBranches(root);
2310
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2311
- runWorkspaceSavePreflight({ cwd: root });
2312
2686
  const workflowRun = acquireWorkflowRun(
2313
2687
  "release",
2314
2688
  session,
2315
- { bump: level },
2689
+ {
2690
+ bump: level,
2691
+ devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
2692
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2693
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2694
+ workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2695
+ },
2316
2696
  [
2697
+ { id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2698
+ { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2699
+ ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
2317
2700
  ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
2318
2701
  id: `release-${report.name}`,
2319
2702
  description: `Release ${report.name}`,
@@ -2322,26 +2705,48 @@ async function workflowRelease(helpers, input) {
2322
2705
  branch: STAGING_BRANCH,
2323
2706
  resumable: true
2324
2707
  })),
2325
- { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
2708
+ { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2709
+ ...mode === "recursive-workspace" ? [{ id: "cleanup-dev-tags", description: "Clean replaced dev package tags", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : []
2326
2710
  ],
2327
- helpers.context
2711
+ autoResumeRun ? {
2712
+ ...helpers.context,
2713
+ workflow: {
2714
+ ...helpers.context.workflow ?? {},
2715
+ resumeRunId: autoResumeRun.runId
2716
+ }
2717
+ } : helpers.context
2328
2718
  );
2719
+ if (autoResumeRun) {
2720
+ helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
2721
+ }
2329
2722
  try {
2723
+ const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
2724
+ const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
2725
+ const effectiveSelectedPackageNames = new Set(effectivePackageSelection.selected);
2726
+ const effectiveVersions = releasePlanVersionMap(releasePlan.plannedVersions);
2727
+ const rootVersion = String(releasePlan.rootVersion);
2728
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2729
+ assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames);
2730
+ if (!isResume) {
2731
+ assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
2732
+ assertCleanWorktree(root);
2733
+ }
2734
+ prepareReleaseBranches(root);
2735
+ runWorkspaceSavePreflight({ cwd: root });
2736
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
2330
2737
  if (mode === "root-only") {
2331
2738
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2332
- applyWorkspaceVersionChanges(versionPlan);
2333
- const rootVersion = bumpRootPackageJson(root, level);
2739
+ setRootPackageJsonVersion(root, rootVersion);
2334
2740
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2335
- run("git", ["add", "-A"], { cwd: gitRoot });
2336
- run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2741
+ commitAllIfChanged(gitRoot, `release: ${level} bump`);
2337
2742
  pushBranch(gitRoot, STAGING_BRANCH);
2338
2743
  const released = mergeStagingIntoMain(root);
2339
- run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
2340
- run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2744
+ const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
2341
2745
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2342
2746
  return {
2343
2747
  rootVersion,
2344
- releasedCommit: released.commitSha
2748
+ releasedCommit: released.commitSha,
2749
+ tag
2345
2750
  };
2346
2751
  });
2347
2752
  rootRepo.committed = true;
@@ -2350,22 +2755,27 @@ async function workflowRelease(helpers, input) {
2350
2755
  rootRepo.branch = PRODUCTION_BRANCH;
2351
2756
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
2352
2757
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
2758
+ const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2353
2759
  const payload2 = {
2354
2760
  mode,
2355
2761
  mergeStrategy: "merge-commit",
2356
2762
  level,
2763
+ resumed: workflowRun.resumed,
2764
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2765
+ autoResumed: autoResumeRun != null,
2357
2766
  rootVersion: String(rootRelease2?.rootVersion ?? ""),
2358
2767
  releaseTag: String(rootRelease2?.rootVersion ?? ""),
2359
2768
  releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
2360
2769
  stagingBranch: STAGING_BRANCH,
2361
2770
  productionBranch: PRODUCTION_BRANCH,
2362
- touchedPackages: [...versionPlan.touched],
2771
+ touchedPackages: [],
2363
2772
  packageSelection: { changed: [], dependents: [], selected: [] },
2364
2773
  publishWait: [],
2365
2774
  repos: [],
2366
2775
  rootRepo,
2367
2776
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
2368
- pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true }
2777
+ pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
2778
+ workspaceLinks: workspaceLinks2
2369
2779
  };
2370
2780
  completeWorkflowRun(root, workflowRun.runId, payload2);
2371
2781
  return buildWorkflowResult("release", root, payload2, {
@@ -2375,18 +2785,39 @@ async function workflowRelease(helpers, input) {
2375
2785
  ])
2376
2786
  });
2377
2787
  }
2378
- assertWorkspaceVersionConsistency(root);
2379
- validatePackageReleaseWorkflows(root, packageSelection.selected);
2788
+ validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
2380
2789
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2381
- if (selectedPackageNames.has(pkg.name)) {
2790
+ if (effectiveSelectedPackageNames.has(pkg.name)) {
2382
2791
  prepareReleaseBranches(pkg.dir);
2383
2792
  }
2384
2793
  }
2385
- applyWorkspaceVersionChanges(versionPlan);
2794
+ const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
2795
+ const releasedPackageDevTags2 = Object.fromEntries(
2796
+ checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
2797
+ const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
2798
+ return [pkg.name, String(packageJson.version ?? "")];
2799
+ }).filter(([, version]) => version.includes("-dev."))
2800
+ );
2801
+ const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveVersions);
2802
+ applyStableWorkspaceVersionChanges(root, effectiveVersions);
2803
+ setRootPackageJsonVersion(root, rootVersion);
2804
+ const releaseInstalls2 = [
2805
+ { name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
2806
+ ];
2807
+ assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
2808
+ return {
2809
+ releasedPackageDevTags: releasedPackageDevTags2,
2810
+ replacedDevReferences: replacedDevReferences2,
2811
+ releaseInstalls: releaseInstalls2
2812
+ };
2813
+ }, { rerunCompleted: workflowRun.resumed });
2814
+ const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
2815
+ const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
2816
+ const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
2386
2817
  const publishWait = [];
2387
2818
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2388
2819
  const report = findReportByName(packageReports, pkg.name);
2389
- if (!report || !selectedPackageNames.has(pkg.name)) {
2820
+ if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
2390
2821
  if (report) {
2391
2822
  report.skippedReason = "unchanged";
2392
2823
  }
@@ -2394,9 +2825,14 @@ async function workflowRelease(helpers, input) {
2394
2825
  }
2395
2826
  const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
2396
2827
  checkoutBranch(pkg.dir, STAGING_BRANCH);
2828
+ releaseInstalls.push({
2829
+ name: pkg.name,
2830
+ ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2831
+ });
2832
+ assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
2397
2833
  if (hasMeaningfulChanges(pkg.dir)) {
2398
2834
  run("git", ["add", "-A"], { cwd: pkg.dir });
2399
- run("git", ["commit", "-m", `release: ${versionPlan.versions.get(pkg.name)}`], { cwd: pkg.dir });
2835
+ run("git", ["commit", "-m", `release: ${effectiveVersions.get(pkg.name)}`], { cwd: pkg.dir });
2400
2836
  }
2401
2837
  pushBranch(pkg.dir, STAGING_BRANCH);
2402
2838
  const mergeResult = mergeBranchIntoTarget(pkg.dir, {
@@ -2405,18 +2841,19 @@ async function workflowRelease(helpers, input) {
2405
2841
  message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
2406
2842
  pushTarget: true
2407
2843
  });
2408
- const tagName = String(versionPlan.versions.get(pkg.name));
2409
- run("git", ["tag", "-a", tagName, "-m", `release: ${tagName}`], { cwd: pkg.dir });
2410
- run("git", ["push", "origin", tagName], { cwd: pkg.dir });
2844
+ const tagName = String(effectiveVersions.get(pkg.name));
2845
+ const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
2411
2846
  const publish = await waitForGitHubWorkflowCompletion(pkg.dir, {
2412
2847
  workflow: "publish.yml",
2413
2848
  headSha: mergeResult.commitSha,
2414
- branch: PRODUCTION_BRANCH
2849
+ branch: tagName
2415
2850
  });
2851
+ assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
2416
2852
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
2417
2853
  return {
2418
2854
  commitSha: mergeResult.commitSha,
2419
2855
  tagName,
2856
+ tag,
2420
2857
  publish
2421
2858
  };
2422
2859
  });
@@ -2432,11 +2869,11 @@ async function workflowRelease(helpers, input) {
2432
2869
  ...releasedPackage?.publish ?? {}
2433
2870
  });
2434
2871
  }
2872
+ assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
2435
2873
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2436
- const rootVersion = bumpRootPackageJson(root, level);
2874
+ setRootPackageJsonVersion(root, rootVersion);
2437
2875
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2438
- run("git", ["add", "-A"], { cwd: gitRoot });
2439
- run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2876
+ commitAllIfChanged(gitRoot, `release: ${level} bump`);
2440
2877
  pushBranch(gitRoot, STAGING_BRANCH);
2441
2878
  const released = mergeBranchIntoTarget(root, {
2442
2879
  sourceBranch: STAGING_BRANCH,
@@ -2445,22 +2882,21 @@ async function workflowRelease(helpers, input) {
2445
2882
  pushTarget: false
2446
2883
  });
2447
2884
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2448
- if (selectedPackageNames.has(pkg.name)) {
2885
+ if (effectiveSelectedPackageNames.has(pkg.name)) {
2449
2886
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
2450
2887
  }
2451
2888
  }
2452
- run("git", ["add", "-A"], { cwd: gitRoot });
2453
- if (hasMeaningfulChanges(gitRoot)) {
2454
- run("git", ["commit", "-m", "release: sync package main heads"], { cwd: gitRoot });
2455
- }
2456
- run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
2889
+ commitAllIfChanged(gitRoot, "release: sync package main heads");
2890
+ const releasedCommit = headCommit(gitRoot);
2891
+ const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit);
2457
2892
  run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
2458
- run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2459
2893
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2460
2894
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2461
2895
  return {
2462
2896
  rootVersion,
2463
- releasedCommit: headCommit(gitRoot)
2897
+ releasedCommit,
2898
+ mergeCommit: released.commitSha,
2899
+ tag
2464
2900
  };
2465
2901
  });
2466
2902
  rootRepo.committed = true;
@@ -2469,17 +2905,48 @@ async function workflowRelease(helpers, input) {
2469
2905
  rootRepo.branch = PRODUCTION_BRANCH;
2470
2906
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
2471
2907
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
2908
+ const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
2909
+ 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", () => {
2910
+ const activeDevTags = collectActiveDevTagReferences(root);
2911
+ const byPackage = /* @__PURE__ */ new Map();
2912
+ for (const reference of replacedDevReferences) {
2913
+ const tagName = typeof reference.tagName === "string" ? reference.tagName : devTagFromDependencySpec(String(reference.from ?? ""));
2914
+ const packageName = typeof reference.packageName === "string" ? reference.packageName : null;
2915
+ if (!tagName || !packageName) continue;
2916
+ byPackage.set(packageName, [...byPackage.get(packageName) ?? [], tagName]);
2917
+ }
2918
+ for (const [packageName, tagName] of releasedPackageDevTags.entries()) {
2919
+ byPackage.set(packageName, [...byPackage.get(packageName) ?? [], tagName]);
2920
+ }
2921
+ const cleanupReports = [];
2922
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2923
+ const tagNames = byPackage.get(pkg.name) ?? [];
2924
+ if (tagNames.length === 0) continue;
2925
+ cleanupReports.push({
2926
+ name: pkg.name,
2927
+ ...cleanupDevTags(pkg.dir, tagNames, activeDevTags)
2928
+ });
2929
+ }
2930
+ return { status: "completed", repos: cleanupReports };
2931
+ });
2932
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2472
2933
  const payload = {
2473
2934
  mode,
2474
2935
  mergeStrategy: "merge-commit",
2475
2936
  level,
2937
+ resumed: workflowRun.resumed,
2938
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2939
+ autoResumed: autoResumeRun != null,
2476
2940
  rootVersion: String(rootRelease?.rootVersion ?? ""),
2477
2941
  releaseTag: String(rootRelease?.rootVersion ?? ""),
2478
2942
  releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
2479
2943
  stagingBranch: STAGING_BRANCH,
2480
2944
  productionBranch: PRODUCTION_BRANCH,
2481
- touchedPackages: packageSelection.selected,
2482
- packageSelection,
2945
+ touchedPackages: effectivePackageSelection.selected,
2946
+ packageSelection: effectivePackageSelection,
2947
+ replacedDevReferences,
2948
+ releaseInstalls,
2949
+ devTagCleanup,
2483
2950
  publishWait,
2484
2951
  repos: packageReports,
2485
2952
  rootRepo,
@@ -2488,21 +2955,18 @@ async function workflowRelease(helpers, input) {
2488
2955
  stagingPushed: true,
2489
2956
  productionPushed: true,
2490
2957
  tagPushed: true
2491
- }
2958
+ },
2959
+ workspaceLinks
2492
2960
  };
2493
2961
  completeWorkflowRun(root, workflowRun.runId, payload);
2494
- return buildWorkflowResult(
2495
- "release",
2496
- root,
2497
- payload,
2498
- {
2499
- runId: workflowRun.runId,
2500
- nextSteps: createNextSteps([
2501
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
2502
- ])
2503
- }
2504
- );
2962
+ return buildWorkflowResult("release", root, payload, {
2963
+ runId: workflowRun.runId,
2964
+ nextSteps: createNextSteps([
2965
+ { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
2966
+ ])
2967
+ });
2505
2968
  } catch (error) {
2969
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2506
2970
  failWorkflowRun(root, workflowRun.runId, error, {
2507
2971
  resumable: true,
2508
2972
  runId: workflowRun.runId,