@treeseed/sdk 0.12.44 → 0.12.46

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 (40) hide show
  1. package/dist/guarantees/index.d.ts +83 -0
  2. package/dist/guarantees/index.js +688 -86
  3. package/dist/hosting/graph.js +40 -6
  4. package/dist/operations/services/git-workflow.d.ts +16 -1
  5. package/dist/operations/services/git-workflow.js +65 -5
  6. package/dist/operations/services/github-api.js +0 -19
  7. package/dist/operations/services/hosted-service-checks.js +17 -1
  8. package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
  9. package/dist/operations/services/live-hosted-service-checks.js +8 -5
  10. package/dist/operations/services/local-cleanup.js +3 -7
  11. package/dist/operations/services/package-adapters.d.ts +14 -0
  12. package/dist/operations/services/package-adapters.js +36 -3
  13. package/dist/operations/services/package-artifacts.d.ts +37 -0
  14. package/dist/operations/services/package-artifacts.js +99 -0
  15. package/dist/operations/services/railway-deploy.js +78 -18
  16. package/dist/operations/services/railway-source-policy.d.ts +19 -0
  17. package/dist/operations/services/railway-source-policy.js +66 -0
  18. package/dist/operations/services/repository-save-orchestrator.js +88 -19
  19. package/dist/operations/services/workspace-dependency-mode.js +4 -0
  20. package/dist/platform/desired-state.js +3 -3
  21. package/dist/reconcile/builtin-adapters.js +10 -2
  22. package/dist/reconcile/providers/railway-iac.d.ts +2 -0
  23. package/dist/reconcile/providers/railway-iac.js +31 -3
  24. package/dist/reconcile/providers/release-private.d.ts +10 -0
  25. package/dist/reconcile/providers/release-private.js +45 -1
  26. package/dist/scenes/builtin-plugins.js +36 -5
  27. package/dist/scenes/device-matrix.js +2 -0
  28. package/dist/scenes/environment.js +1 -1
  29. package/dist/scenes/runner.js +28 -16
  30. package/dist/scenes/schema.js +31 -2
  31. package/dist/scenes/types.d.ts +25 -2
  32. package/dist/scenes/visual-audit-fixtures.js +9 -3
  33. package/dist/workflow/operations.d.ts +27 -92
  34. package/dist/workflow/operations.js +252 -144
  35. package/dist/workflow/runs.d.ts +1 -0
  36. package/dist/workflow/runs.js +57 -0
  37. package/dist/workflow-support.d.ts +1 -0
  38. package/dist/workflow-support.js +8 -0
  39. package/dist/workflow.d.ts +2 -0
  40. package/package.json +4 -1
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
4
5
  import {
5
6
  compileTreeseedDesiredResourceGraph,
6
7
  compileTreeseedDesiredUnitsFromGraph
@@ -55,6 +56,7 @@ import {
55
56
  assertFeatureBranch,
56
57
  branchExists,
57
58
  checkoutBranch,
59
+ checkoutNewTaskBranchWithChanges,
58
60
  checkoutTaskBranchFromStaging,
59
61
  createDeprecatedTaskTag,
60
62
  deleteLocalBranch,
@@ -71,8 +73,7 @@ import {
71
73
  remoteHeadCommit,
72
74
  remoteBranchExists,
73
75
  STAGING_BRANCH,
74
- syncBranchWithOrigin,
75
- waitForStagingAutomation
76
+ syncBranchWithOrigin
76
77
  } from "../operations/services/git-workflow.js";
77
78
  import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
78
79
  import { resolveGitHubCredentialForRepository } from "../operations/services/github-credentials.js";
@@ -340,9 +341,10 @@ function normalizeSceneArtifactsMode(value) {
340
341
  return value === "screenshots" ? "screenshots" : "full";
341
342
  }
342
343
  function maybeRunLocalWorkflowCleanup(helpers, root, operation, input) {
344
+ if (operation !== "release") return null;
343
345
  if (normalizeExecutionMode(input) === "plan" || input.skipCleanup === true) return null;
344
- helpers.write(`Treeseed ${operation} cleanup: pruning local caches, generated evidence, npm cache, and Docker artifacts before long verification.`, "stderr");
345
- return runTreeseedLocalCleanup({ root, mode: "aggressive" });
346
+ helpers.write("Treeseed release cleanup: pruning disposable local build state while preserving package caches and release evidence.", "stderr");
347
+ return runTreeseedLocalCleanup({ root, mode: "standard", docker: false, npmCache: false });
346
348
  }
347
349
  function normalizeSaveCiMode(mode, branch, lane = "fast") {
348
350
  if (mode === "hosted" || mode === "off") return mode;
@@ -372,11 +374,13 @@ function normalizeReleaseCandidateMode(mode, operation, lane = "fast") {
372
374
  if (value === "hybrid" || value === "strict" || value === "skip") {
373
375
  return value;
374
376
  }
375
- if (operation === "save" && lane === "promotion") return "strict";
376
- return operation === "save" ? "hybrid" : "strict";
377
+ return operation === "save" ? "skip" : "strict";
377
378
  }
378
379
  function shouldUseHostedSaveCi(input, branch, lane = normalizeSaveLane(input.lane)) {
379
- return normalizeSaveCiMode(input.ciMode, branch, lane) === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
380
+ void input;
381
+ void branch;
382
+ void lane;
383
+ return false;
380
384
  }
381
385
  function worktreePayload(root, requestedMode) {
382
386
  const metadata = managedWorkflowWorktreeMetadata(root);
@@ -643,6 +647,9 @@ ${liveFailures.join("\n")}`, {
643
647
  };
644
648
  }
645
649
  async function runReleaseWebLiveVerification(root, environment, helpers, operation) {
650
+ if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
651
+ return { status: "skipped", environment, reason: "release gates disabled" };
652
+ }
646
653
  const env = {
647
654
  ...helpers.context.env,
648
655
  ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
@@ -705,65 +712,35 @@ function productionReleaseImageRefEnv(selectedVersions) {
705
712
  function productionReleaseImageRefVersions(root, selectedVersions) {
706
713
  const versions = new Map(selectedVersions);
707
714
  for (const adapter of discoverTreeseedPackageAdapters(root)) {
708
- if (adapter.id !== "treedx" && adapter.id !== "@treeseed/treedx") continue;
709
715
  const prodSource = stringRecord(adapter.metadata.deploymentSource)?.prod;
710
- if (prodSource !== "image") continue;
716
+ const imageBackedPackage = ["@treeseed/api", "@treeseed/agent", "treedx", "@treeseed/treedx"].includes(adapter.id);
717
+ if (prodSource !== "image" && !imageBackedPackage) continue;
711
718
  if (versions.has(adapter.id) || !adapter.version) continue;
712
719
  const line = stableVersionLine(adapter.version);
713
720
  const stableVersion = (line ? highestStableGitTagOnLine(adapter.dir, line) : null) ?? adapter.version;
714
721
  versions.set(adapter.id, stableVersion);
715
722
  }
723
+ for (const [packageName, relativePath] of [["@treeseed/api", "packages/api"], ["@treeseed/agent", "packages/agent"]]) {
724
+ if (versions.has(packageName)) continue;
725
+ const packageRoot = resolve(root, relativePath);
726
+ const packageJsonPath = resolve(packageRoot, "package.json");
727
+ if (!existsSync(packageJsonPath)) continue;
728
+ const version = stringRecord(JSON.parse(readFileSync(packageJsonPath, "utf8"))).version;
729
+ if (typeof version !== "string") continue;
730
+ const line = stableVersionLine(version);
731
+ versions.set(packageName, (line ? highestStableGitTagOnLine(packageRoot, line) : null) ?? version);
732
+ }
716
733
  return versions;
717
734
  }
718
735
  function stableVersionLine(version) {
719
- const match = version.match(/^(\d+\.\d+)\.\d+$/u);
736
+ const match = version.match(/^(\d+\.\d+)\.\d+(?:-[0-9A-Za-z.-]+)?$/u);
720
737
  return match?.[1] ?? null;
721
738
  }
722
- async function runReleaseApiGuarantees(root, environment, helpers, operation, sceneArtifacts) {
723
- const env = {
724
- ...helpers.context.env,
725
- ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
726
- };
727
- env.TREESEED_ACCEPTANCE_SERVICE_ID ??= env.TREESEED_API_WEB_SERVICE_ID ?? env.TREESEED_WEB_SERVICE_ID;
728
- env.TREESEED_ACCEPTANCE_SERVICE_SECRET ??= env.TREESEED_API_WEB_SERVICE_SECRET ?? env.TREESEED_WEB_SERVICE_SECRET;
729
- if (!env.TREESEED_ACCEPTANCE_SERVICE_ID || !env.TREESEED_ACCEPTANCE_SERVICE_SECRET) {
730
- workflowError(operation, "release_gate_failed", `${environment} API release guarantees cannot run because API acceptance service credentials are missing.`, {
731
- details: {
732
- environment,
733
- missing: [
734
- !env.TREESEED_ACCEPTANCE_SERVICE_ID ? "TREESEED_ACCEPTANCE_SERVICE_ID" : null,
735
- !env.TREESEED_ACCEPTANCE_SERVICE_SECRET ? "TREESEED_ACCEPTANCE_SERVICE_SECRET" : null
736
- ].filter((value) => Boolean(value))
737
- }
738
- });
739
- }
740
- helpers.write(`[${operation}][workflow] Running ${environment} API release guarantees before root deployment.`);
741
- return await withContextEnv(env, async () => {
742
- const report = await runTreeseedGuarantees({
743
- workspaceRoot: root,
744
- filter: { ownerPackage: "@treeseed/api" },
745
- environment,
746
- evidenceTarget: "release",
747
- sceneArtifacts
748
- });
749
- if (!report.ok) {
750
- const diagnostics = report.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20).map((entry) => `${entry.code}: ${entry.message}${entry.sourcePath ? ` (${entry.sourcePath})` : ""}`);
751
- workflowError(operation, "release_gate_failed", `API release guarantees for ${environment} failed:
752
- ${diagnostics.join("\n") || `See ${report.outputRoot}`}`, {
753
- details: { environment, outputRoot: report.outputRoot, counts: report.counts, diagnostics: report.diagnostics }
754
- });
755
- }
756
- return {
757
- ok: report.ok,
758
- environment: report.environment,
759
- runId: report.runId,
760
- outputRoot: report.outputRoot,
761
- counts: report.counts
762
- };
763
- });
764
- }
765
739
  async function runReleaseProductionGuarantees(root, helpers, operation, sceneArtifacts) {
766
740
  const environment = "prod";
741
+ if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
742
+ return { ok: true, status: "skipped", environment, reason: "release gates disabled" };
743
+ }
767
744
  const env = {
768
745
  ...helpers.context.env,
769
746
  ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
@@ -785,6 +762,7 @@ async function runReleaseProductionGuarantees(root, helpers, operation, sceneArt
785
762
  return await withContextEnv(env, async () => {
786
763
  const report = await runTreeseedGuarantees({
787
764
  workspaceRoot: root,
765
+ filter: { gate: "smoke", status: "active" },
788
766
  environment,
789
767
  evidenceTarget: "release",
790
768
  sceneArtifacts
@@ -819,7 +797,7 @@ function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGa
819
797
  { scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
820
798
  { scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
821
799
  ]) {
822
- const gate = gates.find((candidate) => candidate.workflow === "deploy-web.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
800
+ const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
823
801
  const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
824
802
  if (!gate || !timestamp) {
825
803
  continue;
@@ -1244,6 +1222,14 @@ function remoteTagCommit(repoDir, tagName) {
1244
1222
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
1245
1223
  return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
1246
1224
  }
1225
+ function releaseTagExists(repoDir, tagName) {
1226
+ if (gitObjectCommit(repoDir, tagName)) return true;
1227
+ try {
1228
+ return remoteTagCommit(repoDir, tagName) !== null;
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ }
1247
1233
  function ensureReleaseTag(repoDir, tagName, commitSha, message) {
1248
1234
  const localCommit = gitObjectCommit(repoDir, tagName);
1249
1235
  if (localCommit && localCommit !== commitSha) {
@@ -1360,7 +1346,7 @@ function defaultCiWorkflows(kind, branch) {
1360
1346
  return ["verify.yml"];
1361
1347
  }
1362
1348
  if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
1363
- return ["deploy-web.yml"];
1349
+ return ["deploy.yml"];
1364
1350
  }
1365
1351
  return ["verify.yml"];
1366
1352
  }
@@ -1710,34 +1696,50 @@ function findAutoResumableSaveRun(root, branch) {
1710
1696
  function workflowFileExists(repoPath, workflow) {
1711
1697
  return existsSync(resolve(repoPath, ".github", "workflows", workflow));
1712
1698
  }
1713
- function hostedWorkflowForSavedRepository(root, repo) {
1714
- const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
1715
- const adapterWorkflow = packageHostedVerifyWorkflow(adapterByPath.get(resolve(repo.path)));
1716
- if (adapterWorkflow) return adapterWorkflow;
1699
+ function hostedWorkflowsForSavedRepository(root, repo, adapter) {
1700
+ const workflows = [];
1701
+ const addWorkflow = (workflow) => {
1702
+ if (!workflow) return;
1703
+ const normalized = workflow.trim().replace(/^\.github\/workflows\//u, "");
1704
+ if (normalized && !workflows.includes(normalized)) {
1705
+ workflows.push(normalized);
1706
+ }
1707
+ };
1717
1708
  if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
1718
- return "deploy.yml";
1709
+ addWorkflow("deploy.yml");
1710
+ } else {
1711
+ const fallbackAdapter = adapter ?? new Map(discoverTreeseedPackageAdapters(root).map((entry) => [resolve(entry.dir), entry])).get(resolve(repo.path));
1712
+ const adapterWorkflow = packageHostedVerifyWorkflow(fallbackAdapter);
1713
+ addWorkflow(adapterWorkflow);
1719
1714
  }
1720
- if (workflowFileExists(repo.path, "verify.yml")) return "verify.yml";
1721
- return null;
1715
+ if (workflows.length === 0 && workflowFileExists(repo.path, "verify.yml")) addWorkflow("verify.yml");
1716
+ return workflows;
1722
1717
  }
1723
1718
  function gatesForSavedRepositoryReports(root, reports) {
1719
+ const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
1724
1720
  return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch && (repo.committed || repo.tagName)).flatMap((repo) => {
1725
- const workflow = hostedWorkflowForSavedRepository(root, repo);
1726
- if (!workflow) return [];
1727
- const gate = {
1728
- name: repo.name,
1729
- repoPath: repo.path,
1730
- workflow,
1731
- branch: String(repo.branch),
1732
- headSha: String(repo.commitSha)
1733
- };
1734
- return [/^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate];
1721
+ const adapter = adapterByPath.get(resolve(repo.path));
1722
+ return hostedWorkflowsForSavedRepository(root, repo, adapter).map((workflow) => {
1723
+ const gate = {
1724
+ name: repo.name,
1725
+ repoPath: repo.path,
1726
+ workflow,
1727
+ branch: String(repo.branch),
1728
+ headSha: String(repo.commitSha),
1729
+ ...packageHostedVerifyTimeoutSeconds(adapter) ? { timeoutSeconds: packageHostedVerifyTimeoutSeconds(adapter) } : {}
1730
+ };
1731
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
1732
+ });
1735
1733
  });
1736
1734
  }
1737
1735
  function packageHostedVerifyWorkflow(adapter) {
1738
1736
  const workflow = adapter?.metadata?.hostedVerifyWorkflow;
1739
1737
  return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
1740
1738
  }
1739
+ function packageHostedVerifyTimeoutSeconds(adapter) {
1740
+ const timeoutSeconds = adapter?.metadata?.hostedVerifyTimeoutSeconds;
1741
+ return typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) : null;
1742
+ }
1741
1743
  function gateForSavedRootReport(report, branch, scope) {
1742
1744
  if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
1743
1745
  return [];
@@ -2148,7 +2150,7 @@ function validateStagingWorkflowContracts(root) {
2148
2150
  return;
2149
2151
  }
2150
2152
  const missing = [];
2151
- for (const fileName of ["verify.yml", "deploy-web.yml"]) {
2153
+ for (const fileName of ["verify.yml", "deploy.yml"]) {
2152
2154
  if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
2153
2155
  missing.push(fileName);
2154
2156
  }
@@ -2615,7 +2617,17 @@ async function workflowReleaseCandidate(helpers, input = {}) {
2615
2617
  }
2616
2618
  }
2617
2619
  function buildReleasePlanSnapshot(input) {
2618
- const selectedPackageNames = new Set(input.packageSelection.selected);
2620
+ const publishablePackageNames = new Set(
2621
+ discoverTreeseedPackageAdapters(input.root).filter((adapter) => adapter.capabilities.publish).map((adapter) => adapter.id)
2622
+ );
2623
+ const selectedPackageNames = new Set(
2624
+ input.packageSelection.selected.filter((name) => publishablePackageNames.has(name))
2625
+ );
2626
+ const publishablePackageSelection = {
2627
+ changed: input.packageSelection.changed.filter((name) => selectedPackageNames.has(name)),
2628
+ dependents: input.packageSelection.dependents.filter((name) => selectedPackageNames.has(name)),
2629
+ selected: [...selectedPackageNames]
2630
+ };
2619
2631
  const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
2620
2632
  const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
2621
2633
  if (input.repairVersionLine !== true) {
@@ -2625,8 +2637,16 @@ function buildReleasePlanSnapshot(input) {
2625
2637
  versionPlan.versions.set(adapter.id, incrementVersion(adapter.version, input.level));
2626
2638
  }
2627
2639
  }
2640
+ for (const adapter of discoverTreeseedPackageAdapters(input.root)) {
2641
+ let version = versionPlan.versions.get(adapter.id);
2642
+ if (!version) continue;
2643
+ while (releaseTagExists(adapter.dir, version)) {
2644
+ version = incrementVersion(version, input.level);
2645
+ }
2646
+ versionPlan.versions.set(adapter.id, version);
2647
+ }
2628
2648
  const plannedSelected = orderReleasePackageNames([...versionPlan.selected].filter((name) => versionPlan.versions.has(name)));
2629
- const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(input.packageSelection.changed.filter((name) => plannedSelected.includes(name))));
2649
+ const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(publishablePackageSelection.changed.filter((name) => plannedSelected.includes(name))));
2630
2650
  const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
2631
2651
  const plannedPackageSelection = {
2632
2652
  changed: plannedChanged,
@@ -3008,6 +3028,9 @@ async function collectPublishedReleaseArtifactChecks(selectedVersions) {
3008
3028
  return checks;
3009
3029
  }
3010
3030
  async function verifyPublishedReleaseArtifacts(selectedVersions) {
3031
+ if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
3032
+ return { checks: [] };
3033
+ }
3011
3034
  let checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
3012
3035
  const deadline = Date.now() + 5 * 60 * 1e3;
3013
3036
  while (checks.some((check) => !check.ok) && Date.now() < deadline) {
@@ -3766,8 +3789,9 @@ async function workflowSwitch(helpers, input) {
3766
3789
  });
3767
3790
  const session = resolveTreeseedWorkflowSession(root);
3768
3791
  const preview = input.preview === true;
3792
+ const adoptChanges = input.adoptChanges === true;
3769
3793
  const executionMode = normalizeExecutionMode(input);
3770
- if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
3794
+ if (executionMode !== "plan" && !adoptChanges && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
3771
3795
  const managed = ensureManagedWorkflowWorktree({
3772
3796
  root,
3773
3797
  branchName,
@@ -3820,7 +3844,7 @@ async function workflowSwitch(helpers, input) {
3820
3844
  previewRequested: preview,
3821
3845
  worktreeMode: input.worktreeMode ?? "auto",
3822
3846
  worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
3823
- blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
3847
+ blockers: !adoptChanges && dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
3824
3848
  plannedSteps: [
3825
3849
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
3826
3850
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
@@ -3837,7 +3861,17 @@ async function workflowSwitch(helpers, input) {
3837
3861
  }
3838
3862
  );
3839
3863
  }
3840
- if (mode === "recursive-workspace") {
3864
+ if (adoptChanges) {
3865
+ const reports = [rootRepo, ...packageReports];
3866
+ const existingTargets = reports.filter((report) => branchExists(report.path, branchName) || remoteBranchExists(report.path, branchName));
3867
+ if (existingTargets.length > 0) {
3868
+ workflowError("switch", "validation_failed", `--adopt-changes requires a new branch; ${branchName} already exists in ${existingTargets.map((report) => report.name).join(", ")}.`);
3869
+ }
3870
+ const unsafeDirtyRepos = reports.filter((report) => report.dirty && currentBranch(report.path) !== STAGING_BRANCH);
3871
+ if (unsafeDirtyRepos.length > 0) {
3872
+ workflowError("switch", "validation_failed", `--adopt-changes only accepts dirty staging repositories: ${unsafeDirtyRepos.map((report) => report.name).join(", ")}.`);
3873
+ }
3874
+ } else if (mode === "recursive-workspace") {
3841
3875
  assertWorkspaceClean(root);
3842
3876
  assertSessionBranchSafety("switch", session);
3843
3877
  } else {
@@ -3846,7 +3880,7 @@ async function workflowSwitch(helpers, input) {
3846
3880
  const workflowRun = acquireWorkflowRun(
3847
3881
  "switch",
3848
3882
  session,
3849
- { branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
3883
+ { branch: branchName, preview, adoptChanges, worktreeMode: input.worktreeMode ?? "auto" },
3850
3884
  [
3851
3885
  { id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
3852
3886
  ...packageReports.map((report) => ({
@@ -3867,7 +3901,7 @@ async function workflowSwitch(helpers, input) {
3867
3901
  root,
3868
3902
  workflowRun.runId,
3869
3903
  "switch-root",
3870
- () => checkoutTaskBranchFromStaging(repoDir, branchName, {
3904
+ () => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(repoDir, branchName, {
3871
3905
  createIfMissing: input.createIfMissing !== false,
3872
3906
  pushIfCreated: true
3873
3907
  })
@@ -3886,7 +3920,7 @@ async function workflowSwitch(helpers, input) {
3886
3920
  root,
3887
3921
  workflowRun.runId,
3888
3922
  `switch-${report.name}`,
3889
- () => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
3923
+ () => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(managedRepo.dir, branchName, {
3890
3924
  createIfMissing: input.createIfMissing !== false,
3891
3925
  pushIfCreated: false
3892
3926
  })
@@ -3925,7 +3959,8 @@ async function workflowSwitch(helpers, input) {
3925
3959
  workspaceLinks,
3926
3960
  ...worktreePayload(root, input.worktreeMode),
3927
3961
  preconditions: {
3928
- cleanWorktreeRequired: true,
3962
+ cleanWorktreeRequired: !adoptChanges,
3963
+ adoptedDirtyStagingChanges: adoptChanges,
3929
3964
  baseBranch: STAGING_BRANCH
3930
3965
  }
3931
3966
  };
@@ -4427,8 +4462,8 @@ async function workflowSave(helpers, input) {
4427
4462
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "save", effectiveInput);
4428
4463
  const message = String(effectiveInput.message ?? "").trim();
4429
4464
  const saveLane = normalizeSaveLane(effectiveInput.lane);
4430
- const saveCiMode = normalizeSaveCiMode(effectiveInput.ciMode, branch, saveLane);
4431
- const releaseCandidateMode = normalizeReleaseCandidateMode(effectiveInput.releaseCandidate, "save", saveLane);
4465
+ const saveCiMode = "off";
4466
+ const releaseCandidateMode = "skip";
4432
4467
  const optionsHotfix = effectiveInput.hotfix === true;
4433
4468
  const previewInitialized = branchPreviewInitialized(root, branch);
4434
4469
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
@@ -4614,7 +4649,7 @@ async function workflowSave(helpers, input) {
4614
4649
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
4615
4650
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
4616
4651
  workflowRunId: workflowRun.runId,
4617
- deferPushUntilVerified: branch === STAGING_BRANCH,
4652
+ deferPushUntilVerified: false,
4618
4653
  onProgress: (line, stream) => helpers.write(line, stream),
4619
4654
  onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
4620
4655
  const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
@@ -4688,16 +4723,17 @@ async function workflowSave(helpers, input) {
4688
4723
  branch,
4689
4724
  headSha: savedRootRepo.commitSha
4690
4725
  })] : [],
4691
- ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => {
4692
- const workflow = hostedWorkflowForSavedRepository(root, repo);
4693
- const gate = {
4694
- name: repo.name,
4695
- repoPath: repo.path,
4696
- workflow,
4697
- branch: String(repo.branch),
4698
- headSha: String(repo.commitSha)
4699
- };
4700
- return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
4726
+ ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).flatMap((repo) => {
4727
+ return hostedWorkflowsForSavedRepository(root, repo).map((workflow) => {
4728
+ const gate = {
4729
+ name: repo.name,
4730
+ repoPath: repo.path,
4731
+ workflow,
4732
+ branch: String(repo.branch),
4733
+ headSha: String(repo.commitSha)
4734
+ };
4735
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
4736
+ });
4701
4737
  })
4702
4738
  ], "hosted", {
4703
4739
  root,
@@ -4724,7 +4760,7 @@ async function workflowSave(helpers, input) {
4724
4760
  }
4725
4761
  return proof;
4726
4762
  }) : saveLane === "promotion" ? (skipJournalStep(root, workflowRun.runId, "release-proof", { skippedReason: "disabled" }), { skipped: true, reason: "disabled" }) : null;
4727
- const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
4763
+ const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" && process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
4728
4764
  helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
4729
4765
  const releaseSession = resolveTreeseedWorkflowSession(root);
4730
4766
  const stagingReleasePlan = buildReleasePlanSnapshot({
@@ -4741,6 +4777,10 @@ async function workflowSave(helpers, input) {
4741
4777
  lane: saveLane,
4742
4778
  write: (line, stream) => helpers.write(line, stream)
4743
4779
  });
4780
+ }) : branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? (skipJournalStep(root, workflowRun.runId, "release-candidate", { mode: releaseCandidateMode, status: "skipped" }), {
4781
+ mode: releaseCandidateMode,
4782
+ status: "skipped",
4783
+ reason: "release candidate rehearsal disabled"
4744
4784
  }) : null;
4745
4785
  let previewAction = { status: "skipped" };
4746
4786
  if (beforeState.branchRole === "feature" && branch) {
@@ -5060,12 +5100,50 @@ async function workflowClose(helpers, input) {
5060
5100
  toError("close", error);
5061
5101
  }
5062
5102
  }
5103
+ function stagingCandidateWorkflowGates(root, manifest) {
5104
+ const gates = [];
5105
+ const add = (name, repoPath, headSha, workflow, deploy = false) => {
5106
+ if (!workflowFileExists(repoPath, workflow)) return;
5107
+ const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
5108
+ gates.push(deploy ? hostedDeployGate(gate) : gate);
5109
+ };
5110
+ for (const pkg of manifest.packages) {
5111
+ const repoPath = resolve(root, pkg.path);
5112
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
5113
+ add(pkg.name, repoPath, pkg.commit, "verify.yml");
5114
+ }
5115
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5116
+ add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
5117
+ }
5118
+ }
5119
+ const marketRoot = repoRoot(root);
5120
+ add("@treeseed/market", marketRoot, manifest.root.commit, "verify.yml");
5121
+ add("@treeseed/market", marketRoot, manifest.root.commit, "deploy.yml", true);
5122
+ return gates;
5123
+ }
5063
5124
  function normalizeStageVerifyMode(value) {
5064
5125
  return value === "local" || value === "none" ? value : "action";
5065
5126
  }
5066
5127
  function normalizeStageCiMode(input) {
5067
- if (input.ciMode === "hosted" || input.waitForStaging === true) return "hosted";
5068
- return "off";
5128
+ if (input.async === true || input.ciMode === "off") return "off";
5129
+ return "hosted";
5130
+ }
5131
+ function sha256File(filePath) {
5132
+ return existsSync(filePath) ? createHash("sha256").update(readFileSync(filePath)).digest("hex") : null;
5133
+ }
5134
+ function internalPackageDependencies(repoPath) {
5135
+ const packageJsonPath = resolve(repoPath, "package.json");
5136
+ if (!existsSync(packageJsonPath)) return [];
5137
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
5138
+ const names = /* @__PURE__ */ new Set();
5139
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
5140
+ const values = packageJson[field];
5141
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
5142
+ for (const name of Object.keys(values)) {
5143
+ if (name.startsWith("@treeseed/")) names.add(name);
5144
+ }
5145
+ }
5146
+ return [...names].sort();
5069
5147
  }
5070
5148
  function normalizeStageCleanupMode(input) {
5071
5149
  if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
@@ -5077,6 +5155,25 @@ function stageCandidateManifestPath(root, runId) {
5077
5155
  run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
5078
5156
  };
5079
5157
  }
5158
+ function readJsonFile(filePath) {
5159
+ if (!existsSync(filePath)) return null;
5160
+ try {
5161
+ return JSON.parse(readFileSync(filePath, "utf8"));
5162
+ } catch {
5163
+ return null;
5164
+ }
5165
+ }
5166
+ function stageCandidateAttestationBlockers(root) {
5167
+ const manifest = readJsonFile(stageCandidateManifestPath(root, "unused").latest);
5168
+ if (!manifest) return ["No staging candidate manifest is available. Run `trsd stage` and wait for staging verification and deployment workflows."];
5169
+ const blockers = [];
5170
+ if (manifest.root.commit !== headCommit(repoRoot(root))) blockers.push("The local Market staging head no longer matches the latest staged candidate.");
5171
+ for (const pkg of manifest.packages) {
5172
+ const repoPath = resolve(root, pkg.path);
5173
+ if (!existsSync(repoPath) || headCommit(repoPath) !== pkg.commit) blockers.push(`${pkg.name} no longer matches staged commit ${pkg.commit}.`);
5174
+ }
5175
+ return blockers;
5176
+ }
5080
5177
  function writeStageCandidateManifest(root, runId, manifest) {
5081
5178
  const paths = stageCandidateManifestPath(root, runId);
5082
5179
  for (const filePath of [paths.latest, paths.run]) {
@@ -5198,17 +5295,24 @@ function stageConflictError(message, details) {
5198
5295
  }
5199
5296
  function createStageCandidateManifest(root, runId, branchName, plan, verification) {
5200
5297
  const gitRoot = repoRoot(root);
5201
- const packageRepos = plan.repos.filter((repo) => repo.kind === "managed" && repo.remoteSourceExists);
5298
+ const packageRepos = plan.repos.filter((repo) => repo.kind === "managed");
5299
+ const rootCommit = headCommit(gitRoot);
5300
+ const submodules = packageRepos.map((repo) => `${relative(root, repo.path).replaceAll("\\", "/")}:${headCommit(repo.path)}`).sort();
5301
+ const candidateId = createHash("sha256").update(JSON.stringify({
5302
+ rootSha: rootCommit,
5303
+ submodules
5304
+ })).digest("hex");
5202
5305
  return {
5203
- schemaVersion: 1,
5306
+ schemaVersion: 2,
5204
5307
  kind: "treeseed.stage-candidate",
5308
+ candidateId,
5205
5309
  runId,
5206
5310
  branchName,
5207
5311
  targetBranch: STAGING_BRANCH,
5208
5312
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5209
5313
  root: {
5210
5314
  repo: "@treeseed/market",
5211
- commit: headCommit(gitRoot),
5315
+ commit: rootCommit,
5212
5316
  verified: verification.status === "passed" || verification.status === "skipped"
5213
5317
  },
5214
5318
  packages: packageRepos.map((repo) => ({
@@ -5216,6 +5320,8 @@ function createStageCandidateManifest(root, runId, branchName, plan, verificatio
5216
5320
  path: repo.path,
5217
5321
  repoKind: repo.repoKind,
5218
5322
  commit: headCommit(repo.path),
5323
+ lockfileHash: sha256File(resolve(repo.path, "package-lock.json")),
5324
+ dependencies: internalPackageDependencies(repo.path),
5219
5325
  remote: (() => {
5220
5326
  try {
5221
5327
  return originRemoteUrl(repo.path);
@@ -5507,8 +5613,17 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
5507
5613
  refs["@treeseed/market"] = rootObserved;
5508
5614
  return { status: "verified", refs };
5509
5615
  });
5510
- const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForStagingAutomation(repoRoot(root))) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
5616
+ const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates(
5617
+ "stage",
5618
+ stagingCandidateWorkflowGates(root, typedManifest),
5619
+ "hosted",
5620
+ { root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
5621
+ )) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
5511
5622
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
5623
+ for (const repo of checkedOutStagePromotionRepos(root)) {
5624
+ syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
5625
+ }
5626
+ syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
5512
5627
  const cleanup = cleanupMode === "success" ? await executeJournalStep(root, workflowRun.runId, "cleanup-source", () => cleanupStageSourceBranches(root, featureBranch, typedManifest)) : (skipJournalStep(root, workflowRun.runId, "cleanup-source", { skippedReason: "manual cleanup selected" }), { status: "skipped", reason: "manual cleanup selected" });
5513
5628
  const payload = {
5514
5629
  ...basePayload,
@@ -5521,10 +5636,11 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
5521
5636
  promotion,
5522
5637
  stagingRefs,
5523
5638
  hostedCi,
5639
+ stagingGuarantees: null,
5524
5640
  cleanup,
5525
5641
  workspaceLinks,
5526
- finalBranch: cleanupMode === "success" ? STAGING_BRANCH : currentBranch(repoRoot(root)) || featureBranch,
5527
- summary: "Stage promoted refs only. Hosted CI/CD was not waited on by default. Run `npx trsd ci --branch staging --json` or assign the staging release agent for CI/CD repair."
5642
+ finalBranch: STAGING_BRANCH,
5643
+ summary: ciMode === "hosted" ? `Staging candidate ${typedManifest.candidateId} passed all exact-SHA verification and deployment workflows.` : `Staging candidate ${typedManifest.candidateId} was promoted asynchronously; hosted verification is pending.`
5528
5644
  };
5529
5645
  completeWorkflowRun(root, workflowRun.runId, payload);
5530
5646
  return buildWorkflowResult("stage", root, payload, {
@@ -5564,7 +5680,7 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
5564
5680
  resourceKind: ["release-gate"],
5565
5681
  provider: ["treeseed"]
5566
5682
  };
5567
- const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
5683
+ const desiredGraph = await withContextEnv(reconcileEnv, () => compileTreeseedDesiredResourceGraph({ tenantRoot: root, target }));
5568
5684
  const rawUnits = compileTreeseedDesiredUnitsFromGraph(desiredGraph).filter((unit) => unit.provider === "treeseed" && (unit.unitType === "package-manifest" || unit.unitType.startsWith("release-gate:")) && unit.unitType !== "release-gate:npm-publish" && unit.unitType !== "release-gate:image-publish" && (includeHostedReleaseGates || unit.unitType !== "release-gate:hosted-reconcile" && unit.unitType !== "release-gate:live-verify") || unit.provider === "github" && (unit.unitType === "github-environment" || unit.unitType === "github-secret-binding" || unit.unitType === "github-variable-binding"));
5569
5685
  const unitsWithReleaseImageRefs = appendReleaseImageRefGitHubVariableBindings(rawUnits, input.releaseImageRefs ?? {});
5570
5686
  const rawUnitIds = new Set(unitsWithReleaseImageRefs.map((unit) => unit.unitId));
@@ -5676,33 +5792,23 @@ function appendReleaseImageRefGitHubVariableBindings(units, releaseImageRefs) {
5676
5792
  async function workflowRelease(helpers, input) {
5677
5793
  try {
5678
5794
  return await withContextEnv(helpers.context.env, async () => {
5679
- const traceReleaseStartup = (phase) => {
5680
- console.error(`[release][startup] ${phase}`);
5681
- };
5682
- traceReleaseStartup("resolve workspace");
5683
5795
  const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
5684
- traceReleaseStartup("resolve session");
5685
5796
  const session = resolveTreeseedWorkflowSession(root);
5686
5797
  const executionMode = normalizeExecutionMode(input);
5687
- traceReleaseStartup("inspect root repo");
5688
5798
  const rootRepo = createWorkspaceRootRepoReport(root);
5689
- traceReleaseStartup("inspect package repos");
5690
5799
  const packageReports = createWorkspacePackageReports(root);
5691
5800
  const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
5692
5801
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5693
- traceReleaseStartup("check auto resume");
5694
5802
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
5695
5803
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
5696
5804
  const effectiveInput = autoResumeRun ? {
5697
5805
  ...autoResumeRun.input,
5698
5806
  ciMode: input.ciMode ?? autoResumeRun.input.ciMode
5699
5807
  } : input;
5700
- traceReleaseStartup("local cleanup");
5701
5808
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "release", effectiveInput);
5702
5809
  const level = effectiveInput.bump ?? "patch";
5703
5810
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
5704
5811
  const packageSelection = session.packageSelection;
5705
- traceReleaseStartup("build release plan");
5706
5812
  const plannedRelease = buildReleasePlanSnapshot({
5707
5813
  root,
5708
5814
  mode: session.mode,
@@ -5715,21 +5821,20 @@ async function workflowRelease(helpers, input) {
5715
5821
  blockers: []
5716
5822
  });
5717
5823
  const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5718
- traceReleaseStartup("collect blockers");
5719
5824
  const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
5720
5825
  level,
5721
5826
  repairVersionLine: effectiveInput.repairVersionLine === true
5722
5827
  });
5723
5828
  blockers.push(...collectReleaseHelperRepoBlockers(root));
5829
+ blockers.push(...stageCandidateAttestationBlockers(root));
5724
5830
  const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
5725
5831
  const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
5726
5832
  const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
5727
- traceReleaseStartup("collect production readiness");
5728
- const plannedReadiness = collectTreeseedDeploymentReadiness({
5833
+ const plannedReadiness = await withContextEnv({ ...helpers.context.env, ...releaseImageRefs }, () => collectTreeseedDeploymentReadiness({
5729
5834
  tenantRoot: root,
5730
5835
  environment: "prod",
5731
5836
  appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
5732
- });
5837
+ }));
5733
5838
  blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
5734
5839
  plannedRelease.blockers = blockers;
5735
5840
  const releaseBasePayload = {
@@ -5769,16 +5874,13 @@ async function workflowRelease(helpers, input) {
5769
5874
  releaseBasePayload
5770
5875
  );
5771
5876
  }
5772
- traceReleaseStartup("validate blockers");
5773
5877
  if (blockers.length > 0) {
5774
5878
  workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
5775
5879
  ${blockers.join("\n")}`, {
5776
5880
  details: { blockers, releasePlan: plannedRelease }
5777
5881
  });
5778
5882
  }
5779
- traceReleaseStartup("prepare fresh release");
5780
5883
  const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
5781
- traceReleaseStartup("compute versions");
5782
5884
  const stableVersions = new Map([
5783
5885
  ...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
5784
5886
  ...selectedVersions.entries()
@@ -5788,7 +5890,6 @@ ${blockers.join("\n")}`, {
5788
5890
  ...stableVersions.entries()
5789
5891
  ]);
5790
5892
  const selectedPackageSet = new Set(selectedPackageNames);
5791
- traceReleaseStartup("acquire workflow run");
5792
5893
  const workflowRun = acquireWorkflowRun(
5793
5894
  "release",
5794
5895
  session,
@@ -5824,12 +5925,8 @@ ${blockers.join("\n")}`, {
5824
5925
  },
5825
5926
  { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5826
5927
  { id: "production-package-deploy-workflows", description: "Wait for production package deploy workflows before live verification", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5827
- { id: "production-hosting", description: "Reconcile and live-verify production hosted resources before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5828
- { id: "production-api-guarantees", description: "Run production API release guarantees before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5829
5928
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5830
5929
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5831
- { id: "production-web-live-verification", description: "Run production web live verification after root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5832
- { id: "production-final-guarantees", description: "Run final production release guarantees after all production deploys", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5833
5930
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5834
5931
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5835
5932
  ],
@@ -5960,8 +6057,6 @@ ${rendered}`);
5960
6057
  onProgress: (line, stream) => helpers.write(line, stream)
5961
6058
  }).then((workflowGates) => ({ workflowGates }));
5962
6059
  });
5963
- const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", releaseImageRefs, { liveAppId: "api" }));
5964
- const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
5965
6060
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
5966
6061
  const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5967
6062
  const changelog = updateReleaseChangelog(repoRoot(root), {
@@ -5997,14 +6092,9 @@ ${rendered}`);
5997
6092
  hostedDeployGate({
5998
6093
  name: "@treeseed/market",
5999
6094
  repoPath: repoRoot(root),
6000
- workflow: "deploy-web.yml",
6001
- branch: PRODUCTION_BRANCH,
6002
- headSha: String(rootRelease.commit.commitSha ?? ""),
6003
- dispatchIfMissing: true,
6004
- dispatchInputs: {
6005
- environment: "prod",
6006
- action_kind: "deploy_web"
6007
- }
6095
+ workflow: "deploy.yml",
6096
+ branch: plannedRelease.releaseTag,
6097
+ headSha: String(rootRelease.commit.commitSha ?? "")
6008
6098
  })
6009
6099
  ].filter((gate) => gate.headSha);
6010
6100
  const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
@@ -6012,8 +6102,6 @@ ${rendered}`);
6012
6102
  runId: workflowRun.runId,
6013
6103
  onProgress: (line, stream) => helpers.write(line, stream)
6014
6104
  }).then((workflowGates) => ({ workflowGates })));
6015
- const productionWebVerification = await executeJournalStep(root, workflowRun.runId, "production-web-live-verification", () => runReleaseWebLiveVerification(root, "prod", helpers, "release"));
6016
- const productionFinalGuarantees = await executeJournalStep(root, workflowRun.runId, "production-final-guarantees", () => runReleaseProductionGuarantees(root, helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
6017
6105
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
6018
6106
  const packageBackMerges = selectedPackageNames.map((name) => packageRepoByName.get(name)).filter((pkg) => Boolean(pkg)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
6019
6107
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -6042,10 +6130,6 @@ ${rendered}`);
6042
6130
  publishWait: publishWait.workflowGates,
6043
6131
  publishedArtifacts,
6044
6132
  productionPackageDeployWorkflows,
6045
- productionHosting,
6046
- productionApiGuarantees,
6047
- productionWebVerification,
6048
- productionFinalGuarantees,
6049
6133
  backMerge,
6050
6134
  workspaceLinks,
6051
6135
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
@@ -6173,6 +6257,30 @@ async function workflowRecover(helpers, input = {}) {
6173
6257
  inspection: inspectWorkflowLock(root, { scope })
6174
6258
  }));
6175
6259
  const lock = locks.find((entry) => entry.inspection.active)?.inspection ?? locks.find((entry) => entry.inspection.stale)?.inspection ?? locks[0].inspection;
6260
+ const hasActiveLock = locks.some((entry) => entry.inspection.active);
6261
+ const orphanedRunningRuns = hasActiveLock ? [] : listWorkflowRunJournals(root).filter((journal) => journal.status === "running");
6262
+ const prunedOrphanedRuns = input.pruneStale === true ? orphanedRunningRuns.map((journal) => {
6263
+ const classification = {
6264
+ state: "stale",
6265
+ reasons: ["workflow journal was left running without an active workflow lock"],
6266
+ classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
6267
+ };
6268
+ archiveWorkflowRun(root, journal.runId, classification);
6269
+ return { runId: journal.runId, command: journal.command, status: journal.status, classification };
6270
+ }) : orphanedRunningRuns.map((journal) => {
6271
+ updateWorkflowRunJournal(root, journal.runId, (current) => ({
6272
+ ...current,
6273
+ status: "failed",
6274
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6275
+ failure: {
6276
+ code: "interrupted",
6277
+ message: "Workflow process ended without finalizing its journal.",
6278
+ details: { recovery: { resumable: current.resumable, runId: current.runId, resumeCommand: `treeseed resume ${current.runId}` } },
6279
+ at: (/* @__PURE__ */ new Date()).toISOString()
6280
+ }
6281
+ }));
6282
+ return null;
6283
+ }).filter((entry) => entry !== null);
6176
6284
  const journals = listWorkflowRunJournals(root);
6177
6285
  const session = resolveTreeseedWorkflowSession(root);
6178
6286
  const currentHeads = Object.fromEntries(
@@ -6248,7 +6356,7 @@ async function workflowRecover(helpers, input = {}) {
6248
6356
  interruptedRuns,
6249
6357
  staleRuns,
6250
6358
  obsoleteRuns,
6251
- prunedRuns,
6359
+ prunedRuns: [...prunedOrphanedRuns, ...prunedRuns],
6252
6360
  markedObsoleteRun,
6253
6361
  selectedRun,
6254
6362
  runCount: journals.length