@treeseed/sdk 0.12.44 → 0.12.45

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 (37) 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 +7 -4
  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 +12 -5
  19. package/dist/platform/desired-state.js +2 -3
  20. package/dist/reconcile/builtin-adapters.js +10 -2
  21. package/dist/reconcile/providers/railway-iac.d.ts +2 -0
  22. package/dist/reconcile/providers/railway-iac.js +31 -3
  23. package/dist/scenes/builtin-plugins.js +36 -5
  24. package/dist/scenes/device-matrix.js +2 -0
  25. package/dist/scenes/environment.js +1 -1
  26. package/dist/scenes/runner.js +28 -16
  27. package/dist/scenes/schema.js +31 -2
  28. package/dist/scenes/types.d.ts +25 -2
  29. package/dist/scenes/visual-audit-fixtures.js +9 -3
  30. package/dist/workflow/operations.d.ts +27 -92
  31. package/dist/workflow/operations.js +236 -144
  32. package/dist/workflow/runs.d.ts +1 -0
  33. package/dist/workflow/runs.js +57 -0
  34. package/dist/workflow-support.d.ts +1 -0
  35. package/dist/workflow-support.js +8 -0
  36. package/dist/workflow.d.ts +2 -0
  37. 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;
@@ -1360,7 +1338,7 @@ function defaultCiWorkflows(kind, branch) {
1360
1338
  return ["verify.yml"];
1361
1339
  }
1362
1340
  if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
1363
- return ["deploy-web.yml"];
1341
+ return ["deploy.yml"];
1364
1342
  }
1365
1343
  return ["verify.yml"];
1366
1344
  }
@@ -1710,34 +1688,50 @@ function findAutoResumableSaveRun(root, branch) {
1710
1688
  function workflowFileExists(repoPath, workflow) {
1711
1689
  return existsSync(resolve(repoPath, ".github", "workflows", workflow));
1712
1690
  }
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;
1691
+ function hostedWorkflowsForSavedRepository(root, repo, adapter) {
1692
+ const workflows = [];
1693
+ const addWorkflow = (workflow) => {
1694
+ if (!workflow) return;
1695
+ const normalized = workflow.trim().replace(/^\.github\/workflows\//u, "");
1696
+ if (normalized && !workflows.includes(normalized)) {
1697
+ workflows.push(normalized);
1698
+ }
1699
+ };
1717
1700
  if (repo.branch === STAGING_BRANCH && existsSync(resolve(repo.path, "treeseed.site.yaml")) && workflowFileExists(repo.path, "deploy.yml")) {
1718
- return "deploy.yml";
1701
+ addWorkflow("deploy.yml");
1702
+ } else {
1703
+ const fallbackAdapter = adapter ?? new Map(discoverTreeseedPackageAdapters(root).map((entry) => [resolve(entry.dir), entry])).get(resolve(repo.path));
1704
+ const adapterWorkflow = packageHostedVerifyWorkflow(fallbackAdapter);
1705
+ addWorkflow(adapterWorkflow);
1719
1706
  }
1720
- if (workflowFileExists(repo.path, "verify.yml")) return "verify.yml";
1721
- return null;
1707
+ if (workflows.length === 0 && workflowFileExists(repo.path, "verify.yml")) addWorkflow("verify.yml");
1708
+ return workflows;
1722
1709
  }
1723
1710
  function gatesForSavedRepositoryReports(root, reports) {
1711
+ const adapterByPath = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
1724
1712
  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];
1713
+ const adapter = adapterByPath.get(resolve(repo.path));
1714
+ return hostedWorkflowsForSavedRepository(root, repo, adapter).map((workflow) => {
1715
+ const gate = {
1716
+ name: repo.name,
1717
+ repoPath: repo.path,
1718
+ workflow,
1719
+ branch: String(repo.branch),
1720
+ headSha: String(repo.commitSha),
1721
+ ...packageHostedVerifyTimeoutSeconds(adapter) ? { timeoutSeconds: packageHostedVerifyTimeoutSeconds(adapter) } : {}
1722
+ };
1723
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
1724
+ });
1735
1725
  });
1736
1726
  }
1737
1727
  function packageHostedVerifyWorkflow(adapter) {
1738
1728
  const workflow = adapter?.metadata?.hostedVerifyWorkflow;
1739
1729
  return typeof workflow === "string" && workflow.trim() ? workflow.trim().replace(/^\.github\/workflows\//u, "") : null;
1740
1730
  }
1731
+ function packageHostedVerifyTimeoutSeconds(adapter) {
1732
+ const timeoutSeconds = adapter?.metadata?.hostedVerifyTimeoutSeconds;
1733
+ return typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) : null;
1734
+ }
1741
1735
  function gateForSavedRootReport(report, branch, scope) {
1742
1736
  if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
1743
1737
  return [];
@@ -2148,7 +2142,7 @@ function validateStagingWorkflowContracts(root) {
2148
2142
  return;
2149
2143
  }
2150
2144
  const missing = [];
2151
- for (const fileName of ["verify.yml", "deploy-web.yml"]) {
2145
+ for (const fileName of ["verify.yml", "deploy.yml"]) {
2152
2146
  if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
2153
2147
  missing.push(fileName);
2154
2148
  }
@@ -2615,7 +2609,17 @@ async function workflowReleaseCandidate(helpers, input = {}) {
2615
2609
  }
2616
2610
  }
2617
2611
  function buildReleasePlanSnapshot(input) {
2618
- const selectedPackageNames = new Set(input.packageSelection.selected);
2612
+ const publishablePackageNames = new Set(
2613
+ discoverTreeseedPackageAdapters(input.root).filter((adapter) => adapter.capabilities.publish).map((adapter) => adapter.id)
2614
+ );
2615
+ const selectedPackageNames = new Set(
2616
+ input.packageSelection.selected.filter((name) => publishablePackageNames.has(name))
2617
+ );
2618
+ const publishablePackageSelection = {
2619
+ changed: input.packageSelection.changed.filter((name) => selectedPackageNames.has(name)),
2620
+ dependents: input.packageSelection.dependents.filter((name) => selectedPackageNames.has(name)),
2621
+ selected: [...selectedPackageNames]
2622
+ };
2619
2623
  const applicationSelection = selectWorkflowApplications(input.root, { packageSelection: input.packageSelection });
2620
2624
  const versionPlan = planWorkspaceReleaseBump(input.level, input.root, input.mode === "recursive-workspace" ? { selectedPackageNames, repairVersionLine: input.repairVersionLine === true, targetVersionLine: input.targetVersionLine } : {});
2621
2625
  if (input.repairVersionLine !== true) {
@@ -2626,7 +2630,7 @@ function buildReleasePlanSnapshot(input) {
2626
2630
  }
2627
2631
  }
2628
2632
  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))));
2633
+ const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(publishablePackageSelection.changed.filter((name) => plannedSelected.includes(name))));
2630
2634
  const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
2631
2635
  const plannedPackageSelection = {
2632
2636
  changed: plannedChanged,
@@ -3008,6 +3012,9 @@ async function collectPublishedReleaseArtifactChecks(selectedVersions) {
3008
3012
  return checks;
3009
3013
  }
3010
3014
  async function verifyPublishedReleaseArtifacts(selectedVersions) {
3015
+ if (process.env.TREESEED_WORKFLOW_RELEASE_GATES_MODE === "skip") {
3016
+ return { checks: [] };
3017
+ }
3011
3018
  let checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
3012
3019
  const deadline = Date.now() + 5 * 60 * 1e3;
3013
3020
  while (checks.some((check) => !check.ok) && Date.now() < deadline) {
@@ -3766,8 +3773,9 @@ async function workflowSwitch(helpers, input) {
3766
3773
  });
3767
3774
  const session = resolveTreeseedWorkflowSession(root);
3768
3775
  const preview = input.preview === true;
3776
+ const adoptChanges = input.adoptChanges === true;
3769
3777
  const executionMode = normalizeExecutionMode(input);
3770
- if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
3778
+ if (executionMode !== "plan" && !adoptChanges && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
3771
3779
  const managed = ensureManagedWorkflowWorktree({
3772
3780
  root,
3773
3781
  branchName,
@@ -3820,7 +3828,7 @@ async function workflowSwitch(helpers, input) {
3820
3828
  previewRequested: preview,
3821
3829
  worktreeMode: input.worktreeMode ?? "auto",
3822
3830
  worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
3823
- blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
3831
+ blockers: !adoptChanges && dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
3824
3832
  plannedSteps: [
3825
3833
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
3826
3834
  ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
@@ -3837,7 +3845,17 @@ async function workflowSwitch(helpers, input) {
3837
3845
  }
3838
3846
  );
3839
3847
  }
3840
- if (mode === "recursive-workspace") {
3848
+ if (adoptChanges) {
3849
+ const reports = [rootRepo, ...packageReports];
3850
+ const existingTargets = reports.filter((report) => branchExists(report.path, branchName) || remoteBranchExists(report.path, branchName));
3851
+ if (existingTargets.length > 0) {
3852
+ workflowError("switch", "validation_failed", `--adopt-changes requires a new branch; ${branchName} already exists in ${existingTargets.map((report) => report.name).join(", ")}.`);
3853
+ }
3854
+ const unsafeDirtyRepos = reports.filter((report) => report.dirty && currentBranch(report.path) !== STAGING_BRANCH);
3855
+ if (unsafeDirtyRepos.length > 0) {
3856
+ workflowError("switch", "validation_failed", `--adopt-changes only accepts dirty staging repositories: ${unsafeDirtyRepos.map((report) => report.name).join(", ")}.`);
3857
+ }
3858
+ } else if (mode === "recursive-workspace") {
3841
3859
  assertWorkspaceClean(root);
3842
3860
  assertSessionBranchSafety("switch", session);
3843
3861
  } else {
@@ -3846,7 +3864,7 @@ async function workflowSwitch(helpers, input) {
3846
3864
  const workflowRun = acquireWorkflowRun(
3847
3865
  "switch",
3848
3866
  session,
3849
- { branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
3867
+ { branch: branchName, preview, adoptChanges, worktreeMode: input.worktreeMode ?? "auto" },
3850
3868
  [
3851
3869
  { id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
3852
3870
  ...packageReports.map((report) => ({
@@ -3867,7 +3885,7 @@ async function workflowSwitch(helpers, input) {
3867
3885
  root,
3868
3886
  workflowRun.runId,
3869
3887
  "switch-root",
3870
- () => checkoutTaskBranchFromStaging(repoDir, branchName, {
3888
+ () => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(repoDir, branchName, {
3871
3889
  createIfMissing: input.createIfMissing !== false,
3872
3890
  pushIfCreated: true
3873
3891
  })
@@ -3886,7 +3904,7 @@ async function workflowSwitch(helpers, input) {
3886
3904
  root,
3887
3905
  workflowRun.runId,
3888
3906
  `switch-${report.name}`,
3889
- () => checkoutTaskBranchFromStaging(managedRepo.dir, branchName, {
3907
+ () => (adoptChanges ? checkoutNewTaskBranchWithChanges : checkoutTaskBranchFromStaging)(managedRepo.dir, branchName, {
3890
3908
  createIfMissing: input.createIfMissing !== false,
3891
3909
  pushIfCreated: false
3892
3910
  })
@@ -3925,7 +3943,8 @@ async function workflowSwitch(helpers, input) {
3925
3943
  workspaceLinks,
3926
3944
  ...worktreePayload(root, input.worktreeMode),
3927
3945
  preconditions: {
3928
- cleanWorktreeRequired: true,
3946
+ cleanWorktreeRequired: !adoptChanges,
3947
+ adoptedDirtyStagingChanges: adoptChanges,
3929
3948
  baseBranch: STAGING_BRANCH
3930
3949
  }
3931
3950
  };
@@ -4427,8 +4446,8 @@ async function workflowSave(helpers, input) {
4427
4446
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "save", effectiveInput);
4428
4447
  const message = String(effectiveInput.message ?? "").trim();
4429
4448
  const saveLane = normalizeSaveLane(effectiveInput.lane);
4430
- const saveCiMode = normalizeSaveCiMode(effectiveInput.ciMode, branch, saveLane);
4431
- const releaseCandidateMode = normalizeReleaseCandidateMode(effectiveInput.releaseCandidate, "save", saveLane);
4449
+ const saveCiMode = "off";
4450
+ const releaseCandidateMode = "skip";
4432
4451
  const optionsHotfix = effectiveInput.hotfix === true;
4433
4452
  const previewInitialized = branchPreviewInitialized(root, branch);
4434
4453
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
@@ -4614,7 +4633,7 @@ async function workflowSave(helpers, input) {
4614
4633
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
4615
4634
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
4616
4635
  workflowRunId: workflowRun.runId,
4617
- deferPushUntilVerified: branch === STAGING_BRANCH,
4636
+ deferPushUntilVerified: true,
4618
4637
  onProgress: (line, stream) => helpers.write(line, stream),
4619
4638
  onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
4620
4639
  const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
@@ -4688,16 +4707,17 @@ async function workflowSave(helpers, input) {
4688
4707
  branch,
4689
4708
  headSha: savedRootRepo.commitSha
4690
4709
  })] : [],
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;
4710
+ ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).flatMap((repo) => {
4711
+ return hostedWorkflowsForSavedRepository(root, repo).map((workflow) => {
4712
+ const gate = {
4713
+ name: repo.name,
4714
+ repoPath: repo.path,
4715
+ workflow,
4716
+ branch: String(repo.branch),
4717
+ headSha: String(repo.commitSha)
4718
+ };
4719
+ return /^deploy(?:[-.]|$)/u.test(workflow) ? hostedDeployGate(gate) : gate;
4720
+ });
4701
4721
  })
4702
4722
  ], "hosted", {
4703
4723
  root,
@@ -4724,7 +4744,7 @@ async function workflowSave(helpers, input) {
4724
4744
  }
4725
4745
  return proof;
4726
4746
  }) : 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", () => {
4747
+ const releaseCandidate = branch === STAGING_BRANCH && releaseCandidateMode !== "skip" && process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE !== "skip" ? await executeJournalStep(root, workflowRun.runId, "release-candidate", () => {
4728
4748
  helpers.write(`[save][workflow] Running staging release-candidate proof checks (${releaseCandidateMode}).`);
4729
4749
  const releaseSession = resolveTreeseedWorkflowSession(root);
4730
4750
  const stagingReleasePlan = buildReleasePlanSnapshot({
@@ -4741,6 +4761,10 @@ async function workflowSave(helpers, input) {
4741
4761
  lane: saveLane,
4742
4762
  write: (line, stream) => helpers.write(line, stream)
4743
4763
  });
4764
+ }) : branch === STAGING_BRANCH && releaseCandidateMode !== "skip" ? (skipJournalStep(root, workflowRun.runId, "release-candidate", { mode: releaseCandidateMode, status: "skipped" }), {
4765
+ mode: releaseCandidateMode,
4766
+ status: "skipped",
4767
+ reason: "release candidate rehearsal disabled"
4744
4768
  }) : null;
4745
4769
  let previewAction = { status: "skipped" };
4746
4770
  if (beforeState.branchRole === "feature" && branch) {
@@ -5060,12 +5084,50 @@ async function workflowClose(helpers, input) {
5060
5084
  toError("close", error);
5061
5085
  }
5062
5086
  }
5087
+ function stagingCandidateWorkflowGates(root, manifest) {
5088
+ const gates = [];
5089
+ const add = (name, repoPath, headSha, workflow, deploy = false) => {
5090
+ if (!workflowFileExists(repoPath, workflow)) return;
5091
+ const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
5092
+ gates.push(deploy ? hostedDeployGate(gate) : gate);
5093
+ };
5094
+ for (const pkg of manifest.packages) {
5095
+ const repoPath = resolve(root, pkg.path);
5096
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
5097
+ add(pkg.name, repoPath, pkg.commit, "verify.yml");
5098
+ }
5099
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5100
+ add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
5101
+ }
5102
+ }
5103
+ const marketRoot = repoRoot(root);
5104
+ add("@treeseed/market", marketRoot, manifest.root.commit, "verify.yml");
5105
+ add("@treeseed/market", marketRoot, manifest.root.commit, "deploy.yml", true);
5106
+ return gates;
5107
+ }
5063
5108
  function normalizeStageVerifyMode(value) {
5064
5109
  return value === "local" || value === "none" ? value : "action";
5065
5110
  }
5066
5111
  function normalizeStageCiMode(input) {
5067
- if (input.ciMode === "hosted" || input.waitForStaging === true) return "hosted";
5068
- return "off";
5112
+ if (input.async === true || input.ciMode === "off") return "off";
5113
+ return "hosted";
5114
+ }
5115
+ function sha256File(filePath) {
5116
+ return existsSync(filePath) ? createHash("sha256").update(readFileSync(filePath)).digest("hex") : null;
5117
+ }
5118
+ function internalPackageDependencies(repoPath) {
5119
+ const packageJsonPath = resolve(repoPath, "package.json");
5120
+ if (!existsSync(packageJsonPath)) return [];
5121
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
5122
+ const names = /* @__PURE__ */ new Set();
5123
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
5124
+ const values = packageJson[field];
5125
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
5126
+ for (const name of Object.keys(values)) {
5127
+ if (name.startsWith("@treeseed/")) names.add(name);
5128
+ }
5129
+ }
5130
+ return [...names].sort();
5069
5131
  }
5070
5132
  function normalizeStageCleanupMode(input) {
5071
5133
  if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
@@ -5077,6 +5139,25 @@ function stageCandidateManifestPath(root, runId) {
5077
5139
  run: resolve(root, ".treeseed", "workflow", "runs", runId, "stage-candidate.json")
5078
5140
  };
5079
5141
  }
5142
+ function readJsonFile(filePath) {
5143
+ if (!existsSync(filePath)) return null;
5144
+ try {
5145
+ return JSON.parse(readFileSync(filePath, "utf8"));
5146
+ } catch {
5147
+ return null;
5148
+ }
5149
+ }
5150
+ function stageCandidateAttestationBlockers(root) {
5151
+ const manifest = readJsonFile(stageCandidateManifestPath(root, "unused").latest);
5152
+ if (!manifest) return ["No staging candidate manifest is available. Run `trsd stage` and wait for staging verification and deployment workflows."];
5153
+ const blockers = [];
5154
+ if (manifest.root.commit !== headCommit(repoRoot(root))) blockers.push("The local Market staging head no longer matches the latest staged candidate.");
5155
+ for (const pkg of manifest.packages) {
5156
+ const repoPath = resolve(root, pkg.path);
5157
+ if (!existsSync(repoPath) || headCommit(repoPath) !== pkg.commit) blockers.push(`${pkg.name} no longer matches staged commit ${pkg.commit}.`);
5158
+ }
5159
+ return blockers;
5160
+ }
5080
5161
  function writeStageCandidateManifest(root, runId, manifest) {
5081
5162
  const paths = stageCandidateManifestPath(root, runId);
5082
5163
  for (const filePath of [paths.latest, paths.run]) {
@@ -5198,17 +5279,24 @@ function stageConflictError(message, details) {
5198
5279
  }
5199
5280
  function createStageCandidateManifest(root, runId, branchName, plan, verification) {
5200
5281
  const gitRoot = repoRoot(root);
5201
- const packageRepos = plan.repos.filter((repo) => repo.kind === "managed" && repo.remoteSourceExists);
5282
+ const packageRepos = plan.repos.filter((repo) => repo.kind === "managed");
5283
+ const rootCommit = headCommit(gitRoot);
5284
+ const submodules = packageRepos.map((repo) => `${relative(root, repo.path).replaceAll("\\", "/")}:${headCommit(repo.path)}`).sort();
5285
+ const candidateId = createHash("sha256").update(JSON.stringify({
5286
+ rootSha: rootCommit,
5287
+ submodules
5288
+ })).digest("hex");
5202
5289
  return {
5203
- schemaVersion: 1,
5290
+ schemaVersion: 2,
5204
5291
  kind: "treeseed.stage-candidate",
5292
+ candidateId,
5205
5293
  runId,
5206
5294
  branchName,
5207
5295
  targetBranch: STAGING_BRANCH,
5208
5296
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5209
5297
  root: {
5210
5298
  repo: "@treeseed/market",
5211
- commit: headCommit(gitRoot),
5299
+ commit: rootCommit,
5212
5300
  verified: verification.status === "passed" || verification.status === "skipped"
5213
5301
  },
5214
5302
  packages: packageRepos.map((repo) => ({
@@ -5216,6 +5304,8 @@ function createStageCandidateManifest(root, runId, branchName, plan, verificatio
5216
5304
  path: repo.path,
5217
5305
  repoKind: repo.repoKind,
5218
5306
  commit: headCommit(repo.path),
5307
+ lockfileHash: sha256File(resolve(repo.path, "package-lock.json")),
5308
+ dependencies: internalPackageDependencies(repo.path),
5219
5309
  remote: (() => {
5220
5310
  try {
5221
5311
  return originRemoteUrl(repo.path);
@@ -5507,8 +5597,17 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
5507
5597
  refs["@treeseed/market"] = rootObserved;
5508
5598
  return { status: "verified", refs };
5509
5599
  });
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" });
5600
+ const hostedCi = ciMode === "hosted" ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates(
5601
+ "stage",
5602
+ stagingCandidateWorkflowGates(root, typedManifest),
5603
+ "hosted",
5604
+ { root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
5605
+ )) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
5511
5606
  const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
5607
+ for (const repo of checkedOutStagePromotionRepos(root)) {
5608
+ syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
5609
+ }
5610
+ syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
5512
5611
  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
5612
  const payload = {
5514
5613
  ...basePayload,
@@ -5521,10 +5620,11 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
5521
5620
  promotion,
5522
5621
  stagingRefs,
5523
5622
  hostedCi,
5623
+ stagingGuarantees: null,
5524
5624
  cleanup,
5525
5625
  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."
5626
+ finalBranch: STAGING_BRANCH,
5627
+ 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
5628
  };
5529
5629
  completeWorkflowRun(root, workflowRun.runId, payload);
5530
5630
  return buildWorkflowResult("stage", root, payload, {
@@ -5564,7 +5664,7 @@ async function runReleaseGateReconcileFacade(operation, helpers, root, target, i
5564
5664
  resourceKind: ["release-gate"],
5565
5665
  provider: ["treeseed"]
5566
5666
  };
5567
- const desiredGraph = compileTreeseedDesiredResourceGraph({ tenantRoot: root, target });
5667
+ const desiredGraph = await withContextEnv(reconcileEnv, () => compileTreeseedDesiredResourceGraph({ tenantRoot: root, target }));
5568
5668
  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
5669
  const unitsWithReleaseImageRefs = appendReleaseImageRefGitHubVariableBindings(rawUnits, input.releaseImageRefs ?? {});
5570
5670
  const rawUnitIds = new Set(unitsWithReleaseImageRefs.map((unit) => unit.unitId));
@@ -5676,33 +5776,23 @@ function appendReleaseImageRefGitHubVariableBindings(units, releaseImageRefs) {
5676
5776
  async function workflowRelease(helpers, input) {
5677
5777
  try {
5678
5778
  return await withContextEnv(helpers.context.env, async () => {
5679
- const traceReleaseStartup = (phase) => {
5680
- console.error(`[release][startup] ${phase}`);
5681
- };
5682
- traceReleaseStartup("resolve workspace");
5683
5779
  const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
5684
- traceReleaseStartup("resolve session");
5685
5780
  const session = resolveTreeseedWorkflowSession(root);
5686
5781
  const executionMode = normalizeExecutionMode(input);
5687
- traceReleaseStartup("inspect root repo");
5688
5782
  const rootRepo = createWorkspaceRootRepoReport(root);
5689
- traceReleaseStartup("inspect package repos");
5690
5783
  const packageReports = createWorkspacePackageReports(root);
5691
5784
  const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
5692
5785
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5693
- traceReleaseStartup("check auto resume");
5694
5786
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
5695
5787
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
5696
5788
  const effectiveInput = autoResumeRun ? {
5697
5789
  ...autoResumeRun.input,
5698
5790
  ciMode: input.ciMode ?? autoResumeRun.input.ciMode
5699
5791
  } : input;
5700
- traceReleaseStartup("local cleanup");
5701
5792
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "release", effectiveInput);
5702
5793
  const level = effectiveInput.bump ?? "patch";
5703
5794
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
5704
5795
  const packageSelection = session.packageSelection;
5705
- traceReleaseStartup("build release plan");
5706
5796
  const plannedRelease = buildReleasePlanSnapshot({
5707
5797
  root,
5708
5798
  mode: session.mode,
@@ -5715,21 +5805,20 @@ async function workflowRelease(helpers, input) {
5715
5805
  blockers: []
5716
5806
  });
5717
5807
  const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5718
- traceReleaseStartup("collect blockers");
5719
5808
  const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
5720
5809
  level,
5721
5810
  repairVersionLine: effectiveInput.repairVersionLine === true
5722
5811
  });
5723
5812
  blockers.push(...collectReleaseHelperRepoBlockers(root));
5813
+ blockers.push(...stageCandidateAttestationBlockers(root));
5724
5814
  const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
5725
5815
  const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
5726
5816
  const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
5727
- traceReleaseStartup("collect production readiness");
5728
- const plannedReadiness = collectTreeseedDeploymentReadiness({
5817
+ const plannedReadiness = await withContextEnv({ ...helpers.context.env, ...releaseImageRefs }, () => collectTreeseedDeploymentReadiness({
5729
5818
  tenantRoot: root,
5730
5819
  environment: "prod",
5731
5820
  appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
5732
- });
5821
+ }));
5733
5822
  blockers.push(...plannedReadiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`));
5734
5823
  plannedRelease.blockers = blockers;
5735
5824
  const releaseBasePayload = {
@@ -5769,16 +5858,13 @@ async function workflowRelease(helpers, input) {
5769
5858
  releaseBasePayload
5770
5859
  );
5771
5860
  }
5772
- traceReleaseStartup("validate blockers");
5773
5861
  if (blockers.length > 0) {
5774
5862
  workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
5775
5863
  ${blockers.join("\n")}`, {
5776
5864
  details: { blockers, releasePlan: plannedRelease }
5777
5865
  });
5778
5866
  }
5779
- traceReleaseStartup("prepare fresh release");
5780
5867
  const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
5781
- traceReleaseStartup("compute versions");
5782
5868
  const stableVersions = new Map([
5783
5869
  ...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
5784
5870
  ...selectedVersions.entries()
@@ -5788,7 +5874,6 @@ ${blockers.join("\n")}`, {
5788
5874
  ...stableVersions.entries()
5789
5875
  ]);
5790
5876
  const selectedPackageSet = new Set(selectedPackageNames);
5791
- traceReleaseStartup("acquire workflow run");
5792
5877
  const workflowRun = acquireWorkflowRun(
5793
5878
  "release",
5794
5879
  session,
@@ -5824,12 +5909,8 @@ ${blockers.join("\n")}`, {
5824
5909
  },
5825
5910
  { 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
5911
  { 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
5912
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5830
5913
  { 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
5914
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5834
5915
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5835
5916
  ],
@@ -5960,8 +6041,6 @@ ${rendered}`);
5960
6041
  onProgress: (line, stream) => helpers.write(line, stream)
5961
6042
  }).then((workflowGates) => ({ workflowGates }));
5962
6043
  });
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
6044
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
5966
6045
  const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5967
6046
  const changelog = updateReleaseChangelog(repoRoot(root), {
@@ -5997,14 +6076,9 @@ ${rendered}`);
5997
6076
  hostedDeployGate({
5998
6077
  name: "@treeseed/market",
5999
6078
  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
- }
6079
+ workflow: "deploy.yml",
6080
+ branch: plannedRelease.releaseTag,
6081
+ headSha: String(rootRelease.commit.commitSha ?? "")
6008
6082
  })
6009
6083
  ].filter((gate) => gate.headSha);
6010
6084
  const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
@@ -6012,8 +6086,6 @@ ${rendered}`);
6012
6086
  runId: workflowRun.runId,
6013
6087
  onProgress: (line, stream) => helpers.write(line, stream)
6014
6088
  }).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
6089
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
6018
6090
  const packageBackMerges = selectedPackageNames.map((name) => packageRepoByName.get(name)).filter((pkg) => Boolean(pkg)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
6019
6091
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -6042,10 +6114,6 @@ ${rendered}`);
6042
6114
  publishWait: publishWait.workflowGates,
6043
6115
  publishedArtifacts,
6044
6116
  productionPackageDeployWorkflows,
6045
- productionHosting,
6046
- productionApiGuarantees,
6047
- productionWebVerification,
6048
- productionFinalGuarantees,
6049
6117
  backMerge,
6050
6118
  workspaceLinks,
6051
6119
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
@@ -6173,6 +6241,30 @@ async function workflowRecover(helpers, input = {}) {
6173
6241
  inspection: inspectWorkflowLock(root, { scope })
6174
6242
  }));
6175
6243
  const lock = locks.find((entry) => entry.inspection.active)?.inspection ?? locks.find((entry) => entry.inspection.stale)?.inspection ?? locks[0].inspection;
6244
+ const hasActiveLock = locks.some((entry) => entry.inspection.active);
6245
+ const orphanedRunningRuns = hasActiveLock ? [] : listWorkflowRunJournals(root).filter((journal) => journal.status === "running");
6246
+ const prunedOrphanedRuns = input.pruneStale === true ? orphanedRunningRuns.map((journal) => {
6247
+ const classification = {
6248
+ state: "stale",
6249
+ reasons: ["workflow journal was left running without an active workflow lock"],
6250
+ classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
6251
+ };
6252
+ archiveWorkflowRun(root, journal.runId, classification);
6253
+ return { runId: journal.runId, command: journal.command, status: journal.status, classification };
6254
+ }) : orphanedRunningRuns.map((journal) => {
6255
+ updateWorkflowRunJournal(root, journal.runId, (current) => ({
6256
+ ...current,
6257
+ status: "failed",
6258
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6259
+ failure: {
6260
+ code: "interrupted",
6261
+ message: "Workflow process ended without finalizing its journal.",
6262
+ details: { recovery: { resumable: current.resumable, runId: current.runId, resumeCommand: `treeseed resume ${current.runId}` } },
6263
+ at: (/* @__PURE__ */ new Date()).toISOString()
6264
+ }
6265
+ }));
6266
+ return null;
6267
+ }).filter((entry) => entry !== null);
6176
6268
  const journals = listWorkflowRunJournals(root);
6177
6269
  const session = resolveTreeseedWorkflowSession(root);
6178
6270
  const currentHeads = Object.fromEntries(
@@ -6248,7 +6340,7 @@ async function workflowRecover(helpers, input = {}) {
6248
6340
  interruptedRuns,
6249
6341
  staleRuns,
6250
6342
  obsoleteRuns,
6251
- prunedRuns,
6343
+ prunedRuns: [...prunedOrphanedRuns, ...prunedRuns],
6252
6344
  markedObsoleteRun,
6253
6345
  selectedRun,
6254
6346
  runCount: journals.length