@treeseed/sdk 0.6.7 → 0.6.8

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 (48) 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 +41 -20
  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 +314 -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/publish-package.js +5 -0
  36. package/dist/scripts/tenant-workflow-action.js +11 -2
  37. package/dist/verification.js +24 -12
  38. package/dist/workflow/operations.d.ts +381 -55
  39. package/dist/workflow/operations.js +718 -258
  40. package/dist/workflow-state.d.ts +40 -1
  41. package/dist/workflow-state.js +220 -17
  42. package/dist/workflow-support.d.ts +3 -0
  43. package/dist/workflow-support.js +34 -0
  44. package/dist/workflow.d.ts +19 -3
  45. package/dist/workflow.js +3 -3
  46. package/dist/wrangler-d1.js +6 -1
  47. package/package.json +17 -1
  48. package/templates/github/deploy.workflow.yml +24 -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,6 +708,57 @@ function workflowSessionSnapshot(session) {
555
708
  function nextPendingJournalStep(journal) {
556
709
  return journal.steps.find((step) => step.status === "pending") ?? null;
557
710
  }
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
+ if (releaseRunHasCompletedMutation(journal)) {
756
+ return true;
757
+ }
758
+ const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
759
+ return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
760
+ }) ?? null;
761
+ }
558
762
  async function executeJournalStep(root, runId, stepId, action) {
559
763
  const current = readWorkflowRunJournal(root, runId);
560
764
  const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
@@ -707,6 +911,149 @@ function validateStagingWorkflowContracts(root) {
707
911
  });
708
912
  }
709
913
  }
914
+ function shouldSkipReleaseInstall() {
915
+ return process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
916
+ }
917
+ function runReleaseNpmInstall(repoDir, options = {}) {
918
+ if (shouldSkipReleaseInstall()) {
919
+ return { status: "skipped", reason: "stubbed" };
920
+ }
921
+ const args = repoDir === options.workspaceRoot ? ["install"] : ["install", "--workspaces=false"];
922
+ run("npm", args, { cwd: repoDir });
923
+ return { status: "completed", reason: null };
924
+ }
925
+ function pathIsWithin(parent, candidate) {
926
+ const path = relative(parent, candidate);
927
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
928
+ }
929
+ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
930
+ const issues = collectInternalDevReferenceIssues(root, packageNames).filter((issue) => {
931
+ if (!pathIsWithin(repoDir, issue.filePath)) return false;
932
+ if (repoDir !== root) return true;
933
+ return !relative(root, issue.filePath).includes("/");
934
+ });
935
+ if (issues.length === 0) return;
936
+ const rendered = issues.map((issue) => `${issue.filePath}${issue.field ? ` ${issue.field}.${issue.dependencyName}` : ""}: ${issue.reason} ${issue.spec}`).join("\n");
937
+ throw new Error(`Stable release still contains internal Git/dev dependency references.
938
+ ${rendered}`);
939
+ }
940
+ function collectActiveDevTagReferences(root) {
941
+ return collectInternalDevReferenceIssues(root).map((issue) => devTagFromDependencySpec(issue.spec) ?? (issue.spec.includes("-dev.") ? issue.spec : null)).filter((value) => Boolean(value));
942
+ }
943
+ function releasePlanVersionMap(plannedVersions) {
944
+ return new Map(
945
+ Object.entries(plannedVersions).filter(([name]) => name !== "@treeseed/market").map(([name, version]) => [name, String(version)])
946
+ );
947
+ }
948
+ function releasePlanPackageSelection(value) {
949
+ const record = value && typeof value === "object" ? value : {};
950
+ return {
951
+ changed: Array.isArray(record.changed) ? record.changed.map(String) : [],
952
+ dependents: Array.isArray(record.dependents) ? record.dependents.map(String) : [],
953
+ selected: Array.isArray(record.selected) ? record.selected.map(String) : []
954
+ };
955
+ }
956
+ function buildReleasePlanSnapshot(input) {
957
+ const selectedPackageNames = new Set(input.packageSelection.selected);
958
+ const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames } : {});
959
+ const rootVersion = planRootPackageVersion(input.root, input.level);
960
+ const plannedVersions = {
961
+ "@treeseed/market": rootVersion,
962
+ ...Object.fromEntries(versionPlan.versions.entries())
963
+ };
964
+ const plannedDevReferenceRewrites = input.mode === "recursive-workspace" ? collectInternalDevReferenceIssues(input.root, selectedPackageNames) : [];
965
+ return {
966
+ mode: input.mode,
967
+ mergeStrategy: "merge-commit",
968
+ level: input.level,
969
+ rootVersion,
970
+ releaseTag: rootVersion,
971
+ stagingBranch: STAGING_BRANCH,
972
+ productionBranch: PRODUCTION_BRANCH,
973
+ packageSelection: input.packageSelection,
974
+ plannedVersions,
975
+ plannedDevReferenceRewrites,
976
+ plannedPublishWaits: input.packageSelection.selected.map((name) => ({
977
+ name,
978
+ workflow: "publish.yml",
979
+ branch: PRODUCTION_BRANCH,
980
+ status: "planned"
981
+ })),
982
+ touchedPackages: input.packageSelection.selected,
983
+ repos: input.packageReports,
984
+ rootRepo: input.rootRepo,
985
+ finalBranch: STAGING_BRANCH,
986
+ plannedSteps: [
987
+ { id: "release-plan", description: "Record immutable release plan and target versions" },
988
+ { id: "workspace-unlink", description: "Remove local workspace links before stable release install" },
989
+ { id: "prepare-release-metadata", description: "Rewrite package metadata and lockfiles to production dependency mode" },
990
+ ...input.packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
991
+ id: `release-${report.name}`,
992
+ description: `Release ${report.name} from staging to main and tag ${plannedVersions[report.name] ?? "(planned)"}`
993
+ })),
994
+ { id: "release-root", description: `Release market ${rootVersion}` },
995
+ { id: "cleanup-dev-tags", description: "Clean replaced Treeseed dev tags after stable release" },
996
+ { id: "workspace-link", description: "Restore local workspace links after release syncs back to staging" }
997
+ ],
998
+ blockers: input.blockers
999
+ };
1000
+ }
1001
+ function collectReleasePlanBlockers(session, mode, selectedPackageNames) {
1002
+ const blockers = [];
1003
+ if (session.branchName !== STAGING_BRANCH) {
1004
+ blockers.push("Release must start from staging.");
1005
+ }
1006
+ if (session.rootRepo.dirty) {
1007
+ blockers.push("@treeseed/market has uncommitted changes.");
1008
+ }
1009
+ if (!session.rootRepo.hasOriginRemote) {
1010
+ blockers.push("@treeseed/market is missing origin remote.");
1011
+ }
1012
+ if (mode === "recursive-workspace") {
1013
+ for (const repo of session.packageRepos) {
1014
+ if (!selectedPackageNames.includes(repo.name)) continue;
1015
+ if (repo.detached) blockers.push(`${repo.name} is detached.`);
1016
+ if (repo.branchName !== STAGING_BRANCH) blockers.push(`${repo.name} is on ${repo.branchName ?? "(detached)"} instead of staging.`);
1017
+ if (repo.dirty) blockers.push(`${repo.name} has uncommitted changes.`);
1018
+ if (!repo.hasOriginRemote) blockers.push(`${repo.name} is missing origin remote.`);
1019
+ }
1020
+ try {
1021
+ validatePackageReleaseWorkflows(session.root, selectedPackageNames);
1022
+ } catch (error) {
1023
+ blockers.push(error instanceof Error ? error.message : String(error));
1024
+ }
1025
+ }
1026
+ return blockers;
1027
+ }
1028
+ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
1029
+ if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1030
+ return;
1031
+ }
1032
+ createGitHubApiClient();
1033
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1034
+ if (!selectedPackageNames.has(pkg.name)) continue;
1035
+ resolveGitHubRepositorySlug(pkg.dir);
1036
+ }
1037
+ }
1038
+ function assertReleaseGitHubWorkflowSucceeded(packageName, workflow) {
1039
+ if (!workflow || workflow.status !== "completed") {
1040
+ return;
1041
+ }
1042
+ if (workflow.conclusion === "success") {
1043
+ return;
1044
+ }
1045
+ const workflowName = typeof workflow.workflow === "string" ? workflow.workflow : "publish.yml";
1046
+ const repository = typeof workflow.repository === "string" ? workflow.repository : packageName;
1047
+ const url = typeof workflow.url === "string" && workflow.url ? `
1048
+ ${workflow.url}` : "";
1049
+ const conclusion = typeof workflow.conclusion === "string" && workflow.conclusion ? workflow.conclusion : "unknown";
1050
+ workflowError("release", "github_workflow_failed", `${packageName} ${workflowName} completed with conclusion ${conclusion} in ${repository}.${url}`, {
1051
+ details: {
1052
+ packageName,
1053
+ workflow
1054
+ }
1055
+ });
1056
+ }
710
1057
  function assertSessionBranchSafety(operation, session, {
711
1058
  requireCleanPackages = false,
712
1059
  requireCurrentBranch = false,
@@ -1011,8 +1358,26 @@ function collectReleasePackageSelection(root) {
1011
1358
  function hasStagedChanges(repoDir) {
1012
1359
  return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
1013
1360
  }
1014
- async function workflowStatus(helpers) {
1015
- return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
1361
+ async function workflowStatus(helpers, input = {}) {
1362
+ return withContextEnv(helpers.context.env, async () => {
1363
+ const resolved = resolveTreeseedWorkflowPaths(helpers.cwd());
1364
+ if (resolved.tenantRoot) {
1365
+ try {
1366
+ await ensureTreeseedSecretSessionForConfig({
1367
+ tenantRoot: resolved.cwd,
1368
+ interactive: false,
1369
+ env: helpers.context.env,
1370
+ createIfMissing: false,
1371
+ allowMigration: false
1372
+ });
1373
+ } catch {
1374
+ }
1375
+ }
1376
+ return createStatusResult(helpers.cwd(), {
1377
+ ...input,
1378
+ env: input.env ?? helpers.context.env
1379
+ });
1380
+ });
1016
1381
  }
1017
1382
  async function workflowTasks(helpers) {
1018
1383
  return withContextEnv(helpers.context.env, () => createTasksResult(helpers.cwd()));
@@ -1034,6 +1399,21 @@ async function workflowConfig(helpers, input = {}) {
1034
1399
  const bootstrapSystemsInput = input.systems;
1035
1400
  const skipUnavailable = input.skipUnavailable;
1036
1401
  const bootstrapExecution = input.bootstrapExecution ?? "parallel";
1402
+ const dependencyInstall = await installTreeseedDependencies({
1403
+ tenantRoot,
1404
+ force: input.installMissingTooling === true,
1405
+ env: helpers.context.env,
1406
+ write: (line) => maybePrint(helpers.write, line)
1407
+ });
1408
+ if (!dependencyInstall.ok) {
1409
+ workflowError(
1410
+ "config",
1411
+ "validation_failed",
1412
+ `Treeseed dependency initialization failed:
1413
+ - ${formatTreeseedDependencyFailureDetails(dependencyInstall)}`,
1414
+ { details: { dependencies: dependencyInstall } }
1415
+ );
1416
+ }
1037
1417
  const repairs = input.repair === false ? [] : resolveTreeseedWorkflowState(tenantRoot).deployConfigPresent ? applyTreeseedSafeRepairs(tenantRoot) : [];
1038
1418
  const toolHealth = ensureTreeseedActVerificationTooling({
1039
1419
  tenantRoot,
@@ -1367,6 +1747,7 @@ async function workflowSwitch(helpers, input) {
1367
1747
  plannedSteps: [
1368
1748
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
1369
1749
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
1750
+ { id: "workspace-link", description: "Apply local workspace links for integrated development" },
1370
1751
  ...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
1371
1752
  ]
1372
1753
  },
@@ -1398,6 +1779,7 @@ async function workflowSwitch(helpers, input) {
1398
1779
  branch: branchName,
1399
1780
  resumable: true
1400
1781
  })),
1782
+ { id: "workspace-link", description: "Apply local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
1401
1783
  ...preview ? [{ id: "preview", description: `Provision or refresh preview ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true }] : []
1402
1784
  ],
1403
1785
  helpers.context
@@ -1437,6 +1819,7 @@ async function workflowSwitch(helpers, input) {
1437
1819
  report.commitSha = headCommit(pkg.dir);
1438
1820
  report.dirty = hasMeaningfulChanges(pkg.dir);
1439
1821
  }
1822
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto"));
1440
1823
  const stateAfterSwitch = resolveTreeseedWorkflowState(root);
1441
1824
  if (preview) {
1442
1825
  previewResult = await executeJournalStep(
@@ -1461,6 +1844,7 @@ async function workflowSwitch(helpers, input) {
1461
1844
  lastDeploymentTimestamp: state.preview.lastDeploymentTimestamp
1462
1845
  },
1463
1846
  previewResult,
1847
+ workspaceLinks,
1464
1848
  preconditions: {
1465
1849
  cleanWorktreeRequired: true,
1466
1850
  baseBranch: STAGING_BRANCH
@@ -1502,6 +1886,7 @@ async function workflowDev(helpers, input = {}) {
1502
1886
  workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
1503
1887
  }
1504
1888
  const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
1889
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(workspaceRoot(tenantRoot), helpers, input.workspaceLinks ?? "auto");
1505
1890
  const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
1506
1891
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
1507
1892
  assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
@@ -1537,7 +1922,8 @@ async function workflowDev(helpers, input = {}) {
1537
1922
  apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
1538
1923
  webUrl: "http://127.0.0.1:8787"
1539
1924
  },
1540
- readiness: readiness.readiness.local
1925
+ readiness: readiness.readiness.local,
1926
+ workspaceLinks
1541
1927
  });
1542
1928
  }
1543
1929
  const result = spawnSync(process.execPath, args, {
@@ -1558,7 +1944,8 @@ async function workflowDev(helpers, input = {}) {
1558
1944
  apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
1559
1945
  webUrl: "http://127.0.0.1:8787"
1560
1946
  },
1561
- readiness: readiness.readiness.local
1947
+ readiness: readiness.readiness.local,
1948
+ workspaceLinks
1562
1949
  });
1563
1950
  });
1564
1951
  } catch (error) {
@@ -1569,8 +1956,6 @@ async function workflowSave(helpers, input) {
1569
1956
  try {
1570
1957
  return await withContextEnv(helpers.context.env, async () => {
1571
1958
  const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
1572
- const message = ensureMessage("save", input.message, "a commit message");
1573
- const optionsHotfix = input.hotfix === true;
1574
1959
  const root = workspaceRoot(tenantRoot);
1575
1960
  const session = resolveTreeseedWorkflowSession(root);
1576
1961
  const gitRoot = session.gitRoot;
@@ -1580,6 +1965,12 @@ async function workflowSave(helpers, input) {
1580
1965
  const recursiveWorkspace = session.mode === "recursive-workspace";
1581
1966
  const mode = session.mode;
1582
1967
  const executionMode = normalizeExecutionMode(input);
1968
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
1969
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
1970
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableSaveRun(root, branch) : null;
1971
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
1972
+ const message = String(effectiveInput.message ?? "").trim();
1973
+ const optionsHotfix = effectiveInput.hotfix === true;
1583
1974
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
1584
1975
  if (!branch) {
1585
1976
  workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
@@ -1597,13 +1988,20 @@ async function workflowSave(helpers, input) {
1597
1988
  if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
1598
1989
  blockers.push("Main saves require --hotfix.");
1599
1990
  }
1600
- if (recursiveWorkspace) {
1601
- try {
1602
- assertWorkspaceVersionConsistency(root);
1603
- } catch (error) {
1604
- blockers.push(error instanceof Error ? error.message : String(error));
1605
- }
1606
- }
1991
+ const repositoryPlan = planRepositorySave({
1992
+ root,
1993
+ gitRoot,
1994
+ branch,
1995
+ message,
1996
+ bump: effectiveInput.bump ?? "patch",
1997
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
1998
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
1999
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2000
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2001
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2002
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
2003
+ });
2004
+ const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
1607
2005
  return buildWorkflowResult(
1608
2006
  "save",
1609
2007
  root,
@@ -1613,21 +2011,30 @@ async function workflowSave(helpers, input) {
1613
2011
  scope,
1614
2012
  hotfix: optionsHotfix,
1615
2013
  message,
1616
- repos: packageReports,
1617
- rootRepo,
2014
+ repos: repositoryPlan.repos,
2015
+ rootRepo: repositoryPlan.rootRepo,
1618
2016
  blockers,
2017
+ autoResumeCandidate: planAutoResumeRun ? {
2018
+ runId: planAutoResumeRun.runId,
2019
+ branch: planAutoResumeRun.session.branchName,
2020
+ failure: planAutoResumeRun.failure
2021
+ } : null,
2022
+ workspaceLinks,
2023
+ repositoryPlan,
2024
+ waves: repositoryPlan.waves,
2025
+ plannedVersions: repositoryPlan.plannedVersions,
1619
2026
  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}` }] : []
2027
+ { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
2028
+ ...repositoryPlan.plannedSteps,
2029
+ { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2030
+ { id: "workspace-link", description: "Restore local workspace links after save" },
2031
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
1625
2032
  ]
1626
2033
  },
1627
2034
  {
1628
2035
  executionMode,
1629
2036
  nextSteps: createNextSteps([
1630
- { operation: "save", reason: "Run without --plan to persist the workspace checkpoint.", input: { message, hotfix: optionsHotfix, preview: input.preview === true } }
2037
+ { 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
2038
  ])
1632
2039
  }
1633
2040
  );
@@ -1646,44 +2053,28 @@ async function workflowSave(helpers, input) {
1646
2053
  {
1647
2054
  message,
1648
2055
  hotfix: optionsHotfix,
1649
- preview: input.preview === true,
1650
- refreshPreview: input.refreshPreview !== false,
1651
- verify: input.verify !== false
2056
+ preview: effectiveInput.preview === true,
2057
+ refreshPreview: effectiveInput.refreshPreview !== false,
2058
+ verify: effectiveInput.verify !== false,
2059
+ bump: effectiveInput.bump ?? "patch",
2060
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2061
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2062
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2063
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2064
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2065
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2066
+ workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
1652
2067
  },
1653
2068
  [
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
2069
  {
1679
- id: "sync-root",
1680
- description: `Push ${branch} to origin`,
2070
+ id: "save-repositories",
2071
+ description: "Save dependency-ordered repositories",
1681
2072
  repoName: rootRepo.name,
1682
2073
  repoPath: rootRepo.path,
1683
2074
  branch,
1684
2075
  resumable: true
1685
2076
  },
1686
- ...beforeState.branchRole === "feature" && (input.preview === true || input.refreshPreview !== false && beforeState.preview.enabled) ? [{
2077
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && beforeState.preview.enabled) ? [{
1687
2078
  id: "preview",
1688
2079
  description: `Refresh preview ${branch}`,
1689
2080
  repoName: rootRepo.name,
@@ -1692,105 +2083,66 @@ async function workflowSave(helpers, input) {
1692
2083
  resumable: true
1693
2084
  }] : []
1694
2085
  ],
1695
- helpers.context
2086
+ autoResumeRun ? {
2087
+ ...helpers.context,
2088
+ workflow: {
2089
+ ...helpers.context.workflow ?? {},
2090
+ resumeRunId: autoResumeRun.runId
2091
+ }
2092
+ } : helpers.context
1696
2093
  );
2094
+ if (autoResumeRun) {
2095
+ helpers.write(`[workflow][resume] Resuming interrupted save ${autoResumeRun.runId} on ${branch}.`);
2096
+ }
1697
2097
  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) {
2098
+ const saveResult = await executeJournalStep(root, workflowRun.runId, "save-repositories", () => (async () => {
2099
+ unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
1729
2100
  try {
1730
- await executeJournalStep(root, workflowRun.runId, "verify-root", () => {
1731
- runWorkspaceSavePreflight({ cwd: root });
1732
- rootRepo.verified = true;
1733
- return {
1734
- verified: true
1735
- };
2101
+ return await runRepositorySaveOrchestrator({
2102
+ root,
2103
+ gitRoot,
2104
+ branch,
2105
+ message,
2106
+ bump: effectiveInput.bump ?? "patch",
2107
+ devVersionStrategy: effectiveInput.devVersionStrategy ?? "prerelease",
2108
+ devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2109
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2110
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2111
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2112
+ commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2113
+ workflowRunId: workflowRun.runId,
2114
+ onProgress: (line, stream) => helpers.write(line, stream)
1736
2115
  });
1737
- } catch (error) {
1738
- createSaveFailure(
1739
- "Treeseed save stopped while verifying the market workspace.",
1740
- packageReports,
1741
- rootRepo,
1742
- null,
1743
- error
1744
- );
2116
+ } finally {
2117
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
1745
2118
  }
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
- }
2119
+ })());
2120
+ const savedPackageReports = saveResult?.repos ?? packageReports;
2121
+ const savedRootRepo = saveResult?.rootRepo ?? rootRepo;
2122
+ const head = savedRootRepo.commitSha ?? run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
2123
+ const commitCreated = savedRootRepo.committed === true;
2124
+ const branchSync = {
2125
+ ...savedRootRepo.publishWait ?? {},
2126
+ pushed: savedRootRepo.pushed === true
2127
+ };
2128
+ const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
2129
+ const commandReadiness = ensureTreeseedCommandReadiness(root);
2130
+ const lockfileValidation = {
2131
+ root: savedRootRepo.lockfileValidation,
2132
+ repos: savedPackageReports.map((repo) => ({
2133
+ name: repo.name,
2134
+ path: repo.path,
2135
+ lockfileValidation: repo.lockfileValidation
2136
+ }))
2137
+ };
1786
2138
  let previewAction = { status: "skipped" };
1787
2139
  if (beforeState.branchRole === "feature" && branch) {
1788
- if (input.preview === true) {
2140
+ if (effectiveInput.preview === true) {
1789
2141
  previewAction = {
1790
2142
  status: beforeState.preview.enabled ? "refreshed" : "created",
1791
2143
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
1792
2144
  };
1793
- } else if (input.refreshPreview !== false && beforeState.preview.enabled) {
2145
+ } else if (effectiveInput.refreshPreview !== false && beforeState.preview.enabled) {
1794
2146
  previewAction = {
1795
2147
  status: "refreshed",
1796
2148
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
@@ -1798,20 +2150,28 @@ async function workflowSave(helpers, input) {
1798
2150
  }
1799
2151
  }
1800
2152
  const payload = {
1801
- mode,
2153
+ mode: saveResult?.mode ?? mode,
1802
2154
  branch,
1803
2155
  scope,
1804
2156
  hotfix: optionsHotfix,
1805
2157
  message,
2158
+ resumed: workflowRun.resumed,
2159
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2160
+ autoResumed: autoResumeRun != null,
1806
2161
  commitSha: head,
1807
2162
  commitCreated,
1808
- noChanges: !hadMeaningfulChanges,
2163
+ noChanges: !commitCreated,
1809
2164
  branchSync,
1810
- repos: packageReports,
1811
- rootRepo,
2165
+ repos: savedPackageReports,
2166
+ rootRepo: savedRootRepo,
2167
+ waves: saveResult?.waves ?? [],
2168
+ plannedVersions: saveResult?.plannedVersions ?? {},
1812
2169
  partialFailure: null,
1813
2170
  previewAction,
1814
- mergeConflict: null
2171
+ mergeConflict: null,
2172
+ workspaceLinks,
2173
+ commandReadiness,
2174
+ lockfileValidation
1815
2175
  };
1816
2176
  completeWorkflowRun(root, workflowRun.runId, payload);
1817
2177
  return buildWorkflowResult(
@@ -1826,7 +2186,9 @@ async function workflowSave(helpers, input) {
1826
2186
  }
1827
2187
  );
1828
2188
  } catch (error) {
1829
- const failingRepo = packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
2189
+ const saveError = repositorySaveErrorDetails(error);
2190
+ const savedPartialFailure = saveError.details?.partialFailure;
2191
+ const failingRepo = savedPartialFailure?.repos.find((report) => report.name === savedPartialFailure.failingRepo) ?? packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
1830
2192
  const wrappedError = error instanceof TreeseedWorkflowError && error.details?.partialFailure != null ? error : new TreeseedWorkflowError(
1831
2193
  "save",
1832
2194
  error instanceof TreeseedWorkflowError ? error.code : "unsupported_state",
@@ -1834,7 +2196,8 @@ async function workflowSave(helpers, input) {
1834
2196
  {
1835
2197
  details: {
1836
2198
  ...error instanceof TreeseedWorkflowError ? error.details ?? {} : {},
1837
- partialFailure: {
2199
+ ...saveError.details ?? {},
2200
+ partialFailure: savedPartialFailure ?? {
1838
2201
  message: "Treeseed save stopped before the workspace could finish syncing.",
1839
2202
  failingRepo: failingRepo.name,
1840
2203
  repos: packageReports,
@@ -1842,7 +2205,7 @@ async function workflowSave(helpers, input) {
1842
2205
  error: error instanceof Error ? error.message : String(error)
1843
2206
  }
1844
2207
  },
1845
- exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : void 0
2208
+ exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : saveError.exitCode
1846
2209
  }
1847
2210
  );
1848
2211
  failWorkflowRun(root, workflowRun.runId, wrappedError, {
@@ -1888,7 +2251,8 @@ async function workflowClose(helpers, input) {
1888
2251
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
1889
2252
  id: `cleanup-${pkg.name}`,
1890
2253
  description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
1891
- }))
2254
+ })),
2255
+ { id: "workspace-link", description: "Restore local workspace links on the final branch" }
1892
2256
  ]
1893
2257
  },
1894
2258
  {
@@ -1899,10 +2263,12 @@ async function workflowClose(helpers, input) {
1899
2263
  }
1900
2264
  );
1901
2265
  }
2266
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1902
2267
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
1903
2268
  message,
1904
2269
  autoSave: input.autoSave
1905
2270
  });
2271
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1906
2272
  const activeSession = resolveTreeseedWorkflowSession(root);
1907
2273
  const featureBranch = assertFeatureBranch(root);
1908
2274
  const mode = activeSession.mode;
@@ -1967,6 +2333,7 @@ async function workflowClose(helpers, input) {
1967
2333
  }));
1968
2334
  Object.assign(report, cleanup);
1969
2335
  }
2336
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1970
2337
  const payload = {
1971
2338
  mode,
1972
2339
  branchName: featureBranch,
@@ -1979,7 +2346,8 @@ async function workflowClose(helpers, input) {
1979
2346
  previewCleanup,
1980
2347
  remoteDeleted: rootRepo.deletedRemote,
1981
2348
  localDeleted: rootRepo.deletedLocal,
1982
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH
2349
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2350
+ workspaceLinks
1983
2351
  };
1984
2352
  completeWorkflowRun(root, workflowRun.runId, payload);
1985
2353
  return buildWorkflowResult(
@@ -1994,6 +2362,7 @@ async function workflowClose(helpers, input) {
1994
2362
  }
1995
2363
  );
1996
2364
  } catch (error) {
2365
+ ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
1997
2366
  failWorkflowRun(root, workflowRun.runId, error, {
1998
2367
  resumable: true,
1999
2368
  runId: workflowRun.runId,
@@ -2042,14 +2411,17 @@ async function workflowStage(helpers, input) {
2042
2411
  id: `merge-${pkg.name}`,
2043
2412
  description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
2044
2413
  })),
2414
+ { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
2045
2415
  { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
2416
+ { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
2046
2417
  { id: "wait-staging", description: "Wait for staging automation" },
2047
2418
  { id: "preview-cleanup", description: "Destroy preview resources" },
2048
2419
  { id: "cleanup-root", description: "Archive and delete the task branch from market" },
2049
2420
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
2050
2421
  id: `cleanup-${pkg.name}`,
2051
2422
  description: `Archive and delete the task branch from ${pkg.name}`
2052
- }))
2423
+ })),
2424
+ { id: "workspace-link", description: "Restore local workspace links on staging" }
2053
2425
  ]
2054
2426
  },
2055
2427
  {
@@ -2060,10 +2432,12 @@ async function workflowStage(helpers, input) {
2060
2432
  }
2061
2433
  );
2062
2434
  }
2435
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2063
2436
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
2064
2437
  message,
2065
2438
  autoSave: input.autoSave
2066
2439
  });
2440
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2067
2441
  const session = resolveTreeseedWorkflowSession(root);
2068
2442
  const featureBranch = assertFeatureBranch(root);
2069
2443
  const mode = session.mode;
@@ -2107,6 +2481,7 @@ async function workflowStage(helpers, input) {
2107
2481
  helpers.context
2108
2482
  );
2109
2483
  try {
2484
+ unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2110
2485
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2111
2486
  const report = findReportByName(packageReports, pkg.name);
2112
2487
  if (!report) {
@@ -2134,14 +2509,21 @@ async function workflowStage(helpers, input) {
2134
2509
  });
2135
2510
  }
2136
2511
  }
2512
+ let rootMerge = null;
2137
2513
  try {
2138
- const rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", () => {
2514
+ rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
2139
2515
  assertCleanWorktree(root);
2140
2516
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2141
2517
  run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
2142
2518
  if (mode === "recursive-workspace") {
2143
2519
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2144
2520
  }
2521
+ const lockfileSafety = await refreshAndValidateRootWorkspaceLockfileForSave({
2522
+ root,
2523
+ gitRoot: repoDir,
2524
+ branch: STAGING_BRANCH,
2525
+ onProgress: (line, stream) => helpers.write(line, stream)
2526
+ });
2145
2527
  if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
2146
2528
  run("git", ["add", "-A"], { cwd: repoDir });
2147
2529
  run("git", ["commit", "-m", message], { cwd: repoDir });
@@ -2150,7 +2532,9 @@ async function workflowStage(helpers, input) {
2150
2532
  return {
2151
2533
  commitSha: headCommit(repoDir),
2152
2534
  branch: currentBranch(repoDir) || STAGING_BRANCH,
2153
- committed: hasMeaningfulChanges(repoDir) ? false : true
2535
+ committed: hasMeaningfulChanges(repoDir) ? false : true,
2536
+ lockfileValidation: lockfileSafety.lockfileValidation,
2537
+ lockfileInstall: lockfileSafety.install
2154
2538
  };
2155
2539
  });
2156
2540
  rootRepo.merged = true;
@@ -2196,6 +2580,7 @@ async function workflowStage(helpers, input) {
2196
2580
  }));
2197
2581
  Object.assign(report, cleanup);
2198
2582
  }
2583
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2199
2584
  const payload = {
2200
2585
  mode,
2201
2586
  branchName: featureBranch,
@@ -2209,9 +2594,12 @@ async function workflowStage(helpers, input) {
2209
2594
  rootRepo,
2210
2595
  stagingWait,
2211
2596
  previewCleanup,
2597
+ lockfileValidation: rootMerge?.lockfileValidation ?? null,
2598
+ lockfileInstall: rootMerge?.lockfileInstall ?? null,
2212
2599
  remoteDeleted: rootRepo.deletedRemote,
2213
2600
  localDeleted: rootRepo.deletedLocal,
2214
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH
2601
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2602
+ workspaceLinks
2215
2603
  };
2216
2604
  completeWorkflowRun(root, workflowRun.runId, payload);
2217
2605
  return buildWorkflowResult(
@@ -2227,6 +2615,7 @@ async function workflowStage(helpers, input) {
2227
2615
  }
2228
2616
  );
2229
2617
  } catch (error) {
2618
+ ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2230
2619
  failWorkflowRun(root, workflowRun.runId, error, {
2231
2620
  resumable: true,
2232
2621
  runId: workflowRun.runId,
@@ -2245,7 +2634,6 @@ async function workflowStage(helpers, input) {
2245
2634
  async function workflowRelease(helpers, input) {
2246
2635
  try {
2247
2636
  return await withContextEnv(helpers.context.env, async () => {
2248
- const level = input.bump ?? "patch";
2249
2637
  const root = resolveProjectRootOrThrow("release", helpers.cwd());
2250
2638
  const session = resolveTreeseedWorkflowSession(root);
2251
2639
  const gitRoot = session.gitRoot;
@@ -2253,67 +2641,58 @@ async function workflowRelease(helpers, input) {
2253
2641
  const executionMode = normalizeExecutionMode(input);
2254
2642
  const rootRepo = createWorkspaceRootRepoReport(root);
2255
2643
  const packageReports = createWorkspacePackageReports(root);
2644
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2645
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2646
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2647
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2648
+ const level = effectiveInput.bump ?? "patch";
2649
+ const isResume = Boolean(explicitResumeRunId || autoResumeRun);
2256
2650
  const packageSelection = session.packageSelection;
2257
2651
  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());
2652
+ const blockers = isResume ? [] : collectReleasePlanBlockers(session, mode, packageSelection.selected);
2653
+ const plannedRelease = buildReleasePlanSnapshot({
2654
+ root,
2655
+ mode,
2656
+ level,
2657
+ packageSelection,
2658
+ packageReports,
2659
+ rootRepo,
2660
+ blockers
2661
+ });
2272
2662
  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
- );
2663
+ return buildWorkflowResult("release", root, {
2664
+ ...plannedRelease,
2665
+ autoResumeCandidate: planAutoResumeRun ? {
2666
+ runId: planAutoResumeRun.runId,
2667
+ branch: planAutoResumeRun.session.branchName,
2668
+ failure: planAutoResumeRun.failure
2669
+ } : null
2670
+ }, {
2671
+ executionMode,
2672
+ nextSteps: createNextSteps([
2673
+ { operation: "release", reason: planAutoResumeRun ? `Run without --plan to resume ${planAutoResumeRun.runId}.` : "Run without --plan to promote staging into production.", input: { bump: level } }
2674
+ ])
2675
+ });
2302
2676
  }
2303
2677
  if (blockers.length > 0) {
2304
2678
  workflowError("release", "validation_failed", blockers.join("\n"), {
2305
2679
  details: { blockers }
2306
2680
  });
2307
2681
  }
2308
- assertSessionBranchSafety("release", session);
2309
- prepareReleaseBranches(root);
2310
- applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2311
- runWorkspaceSavePreflight({ cwd: root });
2312
2682
  const workflowRun = acquireWorkflowRun(
2313
2683
  "release",
2314
2684
  session,
2315
- { bump: level },
2685
+ {
2686
+ bump: level,
2687
+ devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
2688
+ gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2689
+ gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2690
+ workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2691
+ },
2316
2692
  [
2693
+ { id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2694
+ { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2695
+ ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
2317
2696
  ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
2318
2697
  id: `release-${report.name}`,
2319
2698
  description: `Release ${report.name}`,
@@ -2322,26 +2701,48 @@ async function workflowRelease(helpers, input) {
2322
2701
  branch: STAGING_BRANCH,
2323
2702
  resumable: true
2324
2703
  })),
2325
- { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
2704
+ { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2705
+ ...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
2706
  ],
2327
- helpers.context
2707
+ autoResumeRun ? {
2708
+ ...helpers.context,
2709
+ workflow: {
2710
+ ...helpers.context.workflow ?? {},
2711
+ resumeRunId: autoResumeRun.runId
2712
+ }
2713
+ } : helpers.context
2328
2714
  );
2715
+ if (autoResumeRun) {
2716
+ helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
2717
+ }
2329
2718
  try {
2719
+ const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
2720
+ const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
2721
+ const effectiveSelectedPackageNames = new Set(effectivePackageSelection.selected);
2722
+ const effectiveVersions = releasePlanVersionMap(releasePlan.plannedVersions);
2723
+ const rootVersion = String(releasePlan.rootVersion);
2724
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2725
+ assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames);
2726
+ if (!isResume) {
2727
+ assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
2728
+ assertCleanWorktree(root);
2729
+ }
2730
+ prepareReleaseBranches(root);
2731
+ runWorkspaceSavePreflight({ cwd: root });
2732
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
2330
2733
  if (mode === "root-only") {
2331
2734
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2332
- applyWorkspaceVersionChanges(versionPlan);
2333
- const rootVersion = bumpRootPackageJson(root, level);
2735
+ setRootPackageJsonVersion(root, rootVersion);
2334
2736
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2335
- run("git", ["add", "-A"], { cwd: gitRoot });
2336
- run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2737
+ commitAllIfChanged(gitRoot, `release: ${level} bump`);
2337
2738
  pushBranch(gitRoot, STAGING_BRANCH);
2338
2739
  const released = mergeStagingIntoMain(root);
2339
- run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
2340
- run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2740
+ const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
2341
2741
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2342
2742
  return {
2343
2743
  rootVersion,
2344
- releasedCommit: released.commitSha
2744
+ releasedCommit: released.commitSha,
2745
+ tag
2345
2746
  };
2346
2747
  });
2347
2748
  rootRepo.committed = true;
@@ -2350,22 +2751,27 @@ async function workflowRelease(helpers, input) {
2350
2751
  rootRepo.branch = PRODUCTION_BRANCH;
2351
2752
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
2352
2753
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
2754
+ const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2353
2755
  const payload2 = {
2354
2756
  mode,
2355
2757
  mergeStrategy: "merge-commit",
2356
2758
  level,
2759
+ resumed: workflowRun.resumed,
2760
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2761
+ autoResumed: autoResumeRun != null,
2357
2762
  rootVersion: String(rootRelease2?.rootVersion ?? ""),
2358
2763
  releaseTag: String(rootRelease2?.rootVersion ?? ""),
2359
2764
  releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
2360
2765
  stagingBranch: STAGING_BRANCH,
2361
2766
  productionBranch: PRODUCTION_BRANCH,
2362
- touchedPackages: [...versionPlan.touched],
2767
+ touchedPackages: [],
2363
2768
  packageSelection: { changed: [], dependents: [], selected: [] },
2364
2769
  publishWait: [],
2365
2770
  repos: [],
2366
2771
  rootRepo,
2367
2772
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
2368
- pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true }
2773
+ pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
2774
+ workspaceLinks: workspaceLinks2
2369
2775
  };
2370
2776
  completeWorkflowRun(root, workflowRun.runId, payload2);
2371
2777
  return buildWorkflowResult("release", root, payload2, {
@@ -2375,18 +2781,39 @@ async function workflowRelease(helpers, input) {
2375
2781
  ])
2376
2782
  });
2377
2783
  }
2378
- assertWorkspaceVersionConsistency(root);
2379
- validatePackageReleaseWorkflows(root, packageSelection.selected);
2784
+ validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
2380
2785
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2381
- if (selectedPackageNames.has(pkg.name)) {
2786
+ if (effectiveSelectedPackageNames.has(pkg.name)) {
2382
2787
  prepareReleaseBranches(pkg.dir);
2383
2788
  }
2384
2789
  }
2385
- applyWorkspaceVersionChanges(versionPlan);
2790
+ const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
2791
+ const releasedPackageDevTags2 = Object.fromEntries(
2792
+ checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
2793
+ const packageJson = JSON.parse(readFileSync(resolve(pkg.dir, "package.json"), "utf8"));
2794
+ return [pkg.name, String(packageJson.version ?? "")];
2795
+ }).filter(([, version]) => version.includes("-dev."))
2796
+ );
2797
+ const replacedDevReferences2 = rewriteProjectInternalDependenciesToStableVersions(root, effectiveVersions);
2798
+ applyStableWorkspaceVersionChanges(root, effectiveVersions);
2799
+ setRootPackageJsonVersion(root, rootVersion);
2800
+ const releaseInstalls2 = [
2801
+ { name: "@treeseed/market", ...runReleaseNpmInstall(root, { workspaceRoot: root }) }
2802
+ ];
2803
+ assertNoInternalDevReferencesForRepo(root, root, effectiveSelectedPackageNames);
2804
+ return {
2805
+ releasedPackageDevTags: releasedPackageDevTags2,
2806
+ replacedDevReferences: replacedDevReferences2,
2807
+ releaseInstalls: releaseInstalls2
2808
+ };
2809
+ });
2810
+ const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
2811
+ const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
2812
+ const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
2386
2813
  const publishWait = [];
2387
2814
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2388
2815
  const report = findReportByName(packageReports, pkg.name);
2389
- if (!report || !selectedPackageNames.has(pkg.name)) {
2816
+ if (!report || !effectiveSelectedPackageNames.has(pkg.name)) {
2390
2817
  if (report) {
2391
2818
  report.skippedReason = "unchanged";
2392
2819
  }
@@ -2394,9 +2821,14 @@ async function workflowRelease(helpers, input) {
2394
2821
  }
2395
2822
  const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
2396
2823
  checkoutBranch(pkg.dir, STAGING_BRANCH);
2824
+ releaseInstalls.push({
2825
+ name: pkg.name,
2826
+ ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2827
+ });
2828
+ assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
2397
2829
  if (hasMeaningfulChanges(pkg.dir)) {
2398
2830
  run("git", ["add", "-A"], { cwd: pkg.dir });
2399
- run("git", ["commit", "-m", `release: ${versionPlan.versions.get(pkg.name)}`], { cwd: pkg.dir });
2831
+ run("git", ["commit", "-m", `release: ${effectiveVersions.get(pkg.name)}`], { cwd: pkg.dir });
2400
2832
  }
2401
2833
  pushBranch(pkg.dir, STAGING_BRANCH);
2402
2834
  const mergeResult = mergeBranchIntoTarget(pkg.dir, {
@@ -2405,18 +2837,19 @@ async function workflowRelease(helpers, input) {
2405
2837
  message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
2406
2838
  pushTarget: true
2407
2839
  });
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 });
2840
+ const tagName = String(effectiveVersions.get(pkg.name));
2841
+ const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
2411
2842
  const publish = await waitForGitHubWorkflowCompletion(pkg.dir, {
2412
2843
  workflow: "publish.yml",
2413
2844
  headSha: mergeResult.commitSha,
2414
2845
  branch: PRODUCTION_BRANCH
2415
2846
  });
2847
+ assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
2416
2848
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
2417
2849
  return {
2418
2850
  commitSha: mergeResult.commitSha,
2419
2851
  tagName,
2852
+ tag,
2420
2853
  publish
2421
2854
  };
2422
2855
  });
@@ -2432,11 +2865,11 @@ async function workflowRelease(helpers, input) {
2432
2865
  ...releasedPackage?.publish ?? {}
2433
2866
  });
2434
2867
  }
2868
+ assertNoInternalDevReferences(root, effectiveSelectedPackageNames);
2435
2869
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2436
- const rootVersion = bumpRootPackageJson(root, level);
2870
+ setRootPackageJsonVersion(root, rootVersion);
2437
2871
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2438
- run("git", ["add", "-A"], { cwd: gitRoot });
2439
- run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2872
+ commitAllIfChanged(gitRoot, `release: ${level} bump`);
2440
2873
  pushBranch(gitRoot, STAGING_BRANCH);
2441
2874
  const released = mergeBranchIntoTarget(root, {
2442
2875
  sourceBranch: STAGING_BRANCH,
@@ -2445,22 +2878,21 @@ async function workflowRelease(helpers, input) {
2445
2878
  pushTarget: false
2446
2879
  });
2447
2880
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2448
- if (selectedPackageNames.has(pkg.name)) {
2881
+ if (effectiveSelectedPackageNames.has(pkg.name)) {
2449
2882
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
2450
2883
  }
2451
2884
  }
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 });
2885
+ commitAllIfChanged(gitRoot, "release: sync package main heads");
2886
+ const releasedCommit = headCommit(gitRoot);
2887
+ const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit);
2457
2888
  run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
2458
- run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2459
2889
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2460
2890
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2461
2891
  return {
2462
2892
  rootVersion,
2463
- releasedCommit: headCommit(gitRoot)
2893
+ releasedCommit,
2894
+ mergeCommit: released.commitSha,
2895
+ tag
2464
2896
  };
2465
2897
  });
2466
2898
  rootRepo.committed = true;
@@ -2469,17 +2901,48 @@ async function workflowRelease(helpers, input) {
2469
2901
  rootRepo.branch = PRODUCTION_BRANCH;
2470
2902
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
2471
2903
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
2904
+ const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
2905
+ 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", () => {
2906
+ const activeDevTags = collectActiveDevTagReferences(root);
2907
+ const byPackage = /* @__PURE__ */ new Map();
2908
+ for (const reference of replacedDevReferences) {
2909
+ const tagName = typeof reference.tagName === "string" ? reference.tagName : devTagFromDependencySpec(String(reference.from ?? ""));
2910
+ const packageName = typeof reference.packageName === "string" ? reference.packageName : null;
2911
+ if (!tagName || !packageName) continue;
2912
+ byPackage.set(packageName, [...byPackage.get(packageName) ?? [], tagName]);
2913
+ }
2914
+ for (const [packageName, tagName] of releasedPackageDevTags.entries()) {
2915
+ byPackage.set(packageName, [...byPackage.get(packageName) ?? [], tagName]);
2916
+ }
2917
+ const cleanupReports = [];
2918
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2919
+ const tagNames = byPackage.get(pkg.name) ?? [];
2920
+ if (tagNames.length === 0) continue;
2921
+ cleanupReports.push({
2922
+ name: pkg.name,
2923
+ ...cleanupDevTags(pkg.dir, tagNames, activeDevTags)
2924
+ });
2925
+ }
2926
+ return { status: "completed", repos: cleanupReports };
2927
+ });
2928
+ const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2472
2929
  const payload = {
2473
2930
  mode,
2474
2931
  mergeStrategy: "merge-commit",
2475
2932
  level,
2933
+ resumed: workflowRun.resumed,
2934
+ resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
2935
+ autoResumed: autoResumeRun != null,
2476
2936
  rootVersion: String(rootRelease?.rootVersion ?? ""),
2477
2937
  releaseTag: String(rootRelease?.rootVersion ?? ""),
2478
2938
  releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
2479
2939
  stagingBranch: STAGING_BRANCH,
2480
2940
  productionBranch: PRODUCTION_BRANCH,
2481
- touchedPackages: packageSelection.selected,
2482
- packageSelection,
2941
+ touchedPackages: effectivePackageSelection.selected,
2942
+ packageSelection: effectivePackageSelection,
2943
+ replacedDevReferences,
2944
+ releaseInstalls,
2945
+ devTagCleanup,
2483
2946
  publishWait,
2484
2947
  repos: packageReports,
2485
2948
  rootRepo,
@@ -2488,21 +2951,18 @@ async function workflowRelease(helpers, input) {
2488
2951
  stagingPushed: true,
2489
2952
  productionPushed: true,
2490
2953
  tagPushed: true
2491
- }
2954
+ },
2955
+ workspaceLinks
2492
2956
  };
2493
2957
  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
- );
2958
+ return buildWorkflowResult("release", root, payload, {
2959
+ runId: workflowRun.runId,
2960
+ nextSteps: createNextSteps([
2961
+ { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
2962
+ ])
2963
+ });
2505
2964
  } catch (error) {
2965
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2506
2966
  failWorkflowRun(root, workflowRun.runId, error, {
2507
2967
  resumable: true,
2508
2968
  runId: workflowRun.runId,