@treeseed/sdk 0.6.8 → 0.6.10

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 (33) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/managed-dependencies.d.ts +23 -0
  4. package/dist/managed-dependencies.js +133 -12
  5. package/dist/operations/providers/default.js +35 -0
  6. package/dist/operations/services/config-runtime.js +25 -6
  7. package/dist/operations/services/deploy.js +14 -1
  8. package/dist/operations/services/export-runtime.js +4 -0
  9. package/dist/operations/services/git-workflow.d.ts +2 -0
  10. package/dist/operations/services/git-workflow.js +39 -4
  11. package/dist/operations/services/github-api.d.ts +10 -0
  12. package/dist/operations/services/github-api.js +20 -1
  13. package/dist/operations/services/github-automation.d.ts +3 -0
  14. package/dist/operations/services/package-reference-policy.js +19 -3
  15. package/dist/operations/services/repository-save-orchestrator.js +10 -4
  16. package/dist/operations/services/workspace-dependency-mode.js +10 -18
  17. package/dist/operations-registry.js +1 -0
  18. package/dist/scripts/patch-starlight-content-path.js +2 -1
  19. package/dist/scripts/prepare.js +14 -0
  20. package/dist/verification.js +22 -1
  21. package/dist/workflow/operations.d.ts +259 -429
  22. package/dist/workflow/operations.js +662 -78
  23. package/dist/workflow/runs.d.ts +38 -0
  24. package/dist/workflow/runs.js +182 -15
  25. package/dist/workflow/worktrees.d.ts +39 -0
  26. package/dist/workflow/worktrees.js +224 -0
  27. package/dist/workflow-state.d.ts +13 -0
  28. package/dist/workflow-state.js +35 -2
  29. package/dist/workflow-support.d.ts +1 -1
  30. package/dist/workflow-support.js +2 -0
  31. package/dist/workflow.d.ts +14 -1
  32. package/package.json +2 -2
  33. package/templates/github/deploy.workflow.yml +135 -5
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { isAbsolute, relative, resolve } from "node:path";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import {
5
5
  applyTreeseedEnvironmentToProcess,
@@ -23,7 +23,7 @@ import {
23
23
  setTreeseedRemoteSession,
24
24
  writeTreeseedMachineConfig
25
25
  } from "../operations/services/config-runtime.js";
26
- import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
26
+ import { collectTreeseedToolStatus, formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
27
27
  import { ControlPlaneClient } from "../control-plane-client.js";
28
28
  import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
29
29
  import {
@@ -46,6 +46,7 @@ import {
46
46
  assertFeatureBranch,
47
47
  branchExists,
48
48
  checkoutBranch,
49
+ checkoutDetachedOriginBranch,
49
50
  checkoutTaskBranchFromStaging,
50
51
  createDeprecatedTaskTag,
51
52
  deleteLocalBranch,
@@ -59,14 +60,13 @@ import {
59
60
  prepareReleaseBranches,
60
61
  PRODUCTION_BRANCH,
61
62
  pushBranch,
63
+ pushHeadToBranch,
62
64
  remoteBranchExists,
63
65
  STAGING_BRANCH,
64
66
  squashMergeBranchIntoStaging,
65
- syncBranchWithOrigin,
66
- waitForStagingAutomation
67
+ syncBranchWithOrigin
67
68
  } from "../operations/services/git-workflow.js";
68
69
  import { getGitHubAutomationMode, resolveGitHubRepositorySlug, waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
69
- import { createGitHubApiClient } from "../operations/services/github-api.js";
70
70
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
71
71
  import { runTenantDeployPreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
72
72
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
@@ -111,8 +111,13 @@ import { resolveTreeseedWorkflowState } from "../workflow-state.js";
111
111
  import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, filterTreeseedDesiredUnitsByBootstrapSystems, planTreeseedReconciliation, resolveTreeseedBootstrapSelection, reconcileTreeseedTarget } from "../reconcile/index.js";
112
112
  import {
113
113
  acquireWorkflowLock,
114
+ archiveWorkflowRun,
115
+ cacheWorkflowGateResult,
116
+ classifyWorkflowRunJournal,
117
+ classifyWorkflowRunJournals,
114
118
  createWorkflowRunJournal,
115
119
  generateWorkflowRunId,
120
+ getCachedSuccessfulWorkflowGate,
116
121
  inspectWorkflowLock,
117
122
  listInterruptedWorkflowRuns,
118
123
  listWorkflowRunJournals,
@@ -129,6 +134,14 @@ import {
129
134
  classifyTreeseedBranchRole,
130
135
  resolveTreeseedWorkflowPaths
131
136
  } from "./policy.js";
137
+ import {
138
+ effectiveWorkflowWorktreeMode,
139
+ ensureManagedWorkflowWorktree,
140
+ isManagedWorkflowWorktree,
141
+ managedWorkflowWorktreeMetadata,
142
+ plannedManagedWorkflowWorktreePath,
143
+ removeManagedWorkflowWorktree
144
+ } from "./worktrees.js";
132
145
  class TreeseedWorkflowError extends Error {
133
146
  code;
134
147
  operation;
@@ -161,8 +174,64 @@ function ensureWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
161
174
  if (report.created.length > 0) {
162
175
  helpers.write(`[workspace][link] Linked ${report.created.length} local workspace package paths.`);
163
176
  }
177
+ ensureWorkflowWorkspacePackageArtifacts(root, helpers);
178
+ ensureWorkflowCommandBins(root, helpers);
164
179
  return report;
165
180
  }
181
+ function readPackageScript(root, packageDir, scriptName) {
182
+ try {
183
+ const packageJson = JSON.parse(readFileSync(resolve(root, packageDir, "package.json"), "utf8"));
184
+ const scripts = packageJson.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : null;
185
+ const script = scripts?.[scriptName];
186
+ return typeof script === "string" && script.trim() ? script : null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+ function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
192
+ const packages = [
193
+ { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
194
+ { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/api.js", "dist/plugin-default.js"] },
195
+ { name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
196
+ ];
197
+ for (const entry of packages) {
198
+ const packageDir = resolve(root, entry.dir);
199
+ if (!existsSync(resolve(packageDir, "package.json"))) continue;
200
+ if (!readPackageScript(root, entry.dir, "build:dist")) continue;
201
+ const missing = entry.artifacts.filter((artifact) => !existsSync(resolve(packageDir, artifact)));
202
+ if (missing.length === 0) continue;
203
+ helpers.write(`[workspace][build] Building ${entry.name} artifacts for local workspace links.`);
204
+ run("npm", ["--prefix", packageDir, "run", "build:dist"], { cwd: root });
205
+ }
206
+ }
207
+ function ensureWorkflowCommandBins(root, helpers) {
208
+ const cliBin = resolve(root, "node_modules/@treeseed/cli/dist/cli/main.js");
209
+ if (!existsSync(cliBin)) return;
210
+ const binDir = resolve(root, "node_modules/.bin");
211
+ mkdirSync(binDir, { recursive: true });
212
+ for (const name of ["trsd", "treeseed"]) {
213
+ const linkPath = resolve(binDir, name);
214
+ const target = relative(dirname(linkPath), cliBin) || cliBin;
215
+ try {
216
+ const stat = lstatSync(linkPath);
217
+ if (stat.isSymbolicLink()) {
218
+ const currentTarget = readlinkSync(linkPath);
219
+ if (currentTarget === target || resolve(dirname(linkPath), currentTarget) === cliBin) {
220
+ continue;
221
+ }
222
+ rmSync(linkPath, { force: true });
223
+ } else {
224
+ continue;
225
+ }
226
+ } catch (error) {
227
+ if (error.code !== "ENOENT") {
228
+ throw error;
229
+ }
230
+ }
231
+ symlinkSync(target, linkPath);
232
+ helpers.write(`[workspace][link] Linked ${name} command shim.`);
233
+ }
234
+ }
166
235
  function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
167
236
  if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
168
237
  return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
@@ -173,6 +242,163 @@ function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
173
242
  }
174
243
  return report;
175
244
  }
245
+ function normalizeCiMode(mode, operation) {
246
+ if (mode === "hosted" || mode === "off") return mode;
247
+ return operation === "save" ? "off" : "hosted";
248
+ }
249
+ function normalizeSaveVerifyMode(mode) {
250
+ switch (mode) {
251
+ case "skip":
252
+ case "fast":
253
+ case void 0:
254
+ return "skip";
255
+ case "local":
256
+ case "local-only":
257
+ return "local-only";
258
+ case "hosted":
259
+ return "skip";
260
+ case "both":
261
+ case "action-first":
262
+ return "action-first";
263
+ default:
264
+ return "skip";
265
+ }
266
+ }
267
+ function shouldUseHostedSaveCi(input) {
268
+ return normalizeCiMode(input.ciMode, "save") === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both";
269
+ }
270
+ function worktreePayload(root, requestedMode) {
271
+ const metadata = managedWorkflowWorktreeMetadata(root);
272
+ return {
273
+ worktreeMode: requestedMode ?? "auto",
274
+ managedWorktree: metadata,
275
+ worktreePath: metadata?.worktreePath ?? null,
276
+ primaryRoot: metadata?.primaryRoot ?? null
277
+ };
278
+ }
279
+ function helpersForCwd(helpers, cwd) {
280
+ return {
281
+ ...helpers,
282
+ context: {
283
+ ...helpers.context,
284
+ cwd
285
+ },
286
+ cwd: () => cwd
287
+ };
288
+ }
289
+ function shouldDispatchSwitchToManagedWorktree(root, input, env) {
290
+ return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
291
+ }
292
+ function skippedWorkflowGate(gate, reason) {
293
+ return {
294
+ name: gate.name,
295
+ repository: gate.repository ?? null,
296
+ workflow: gate.workflow,
297
+ branch: gate.branch,
298
+ headSha: gate.headSha,
299
+ status: "skipped",
300
+ reason,
301
+ conclusion: null,
302
+ runId: null,
303
+ url: null
304
+ };
305
+ }
306
+ function workflowGateFailureMessage(gate, result) {
307
+ const repository = String(result.repository ?? gate.repository ?? gate.name);
308
+ const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
309
+ const url = typeof result.url === "string" && result.url ? `
310
+ ${result.url}` : "";
311
+ const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => stringRecord(job)?.name).filter((name) => typeof name === "string" && name.length > 0) : [];
312
+ const jobLine = failedJobs.length > 0 ? `
313
+ Failed jobs: ${failedJobs.join(", ")}` : "";
314
+ const command = runId ? `
315
+ Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
316
+ return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
317
+ }
318
+ function assertHostedGitHubWorkflowAuthReady(operation, root) {
319
+ if (getGitHubAutomationMode() === "stub") {
320
+ return null;
321
+ }
322
+ const tools = collectTreeseedToolStatus({
323
+ tenantRoot: root,
324
+ env: process.env
325
+ });
326
+ const github = tools.auth.github;
327
+ if (!github.authenticated) {
328
+ const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
329
+ workflowError(
330
+ operation,
331
+ "github_auth_unavailable",
332
+ [
333
+ "Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
334
+ github.detail,
335
+ `Managed gh check: ${command}`,
336
+ "Remediation:",
337
+ ...github.remediation.map((item) => `- ${item}`)
338
+ ].join("\n"),
339
+ {
340
+ details: {
341
+ toolsHome: tools.toolsHome,
342
+ ghConfigDir: tools.ghConfigDir,
343
+ github,
344
+ tools: tools.tools
345
+ }
346
+ }
347
+ );
348
+ }
349
+ return tools;
350
+ }
351
+ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
352
+ if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
353
+ return gates.map((gate) => skippedWorkflowGate(gate, ciMode === "off" ? "disabled" : "stubbed"));
354
+ }
355
+ if (gates.length === 0) {
356
+ return [];
357
+ }
358
+ assertHostedGitHubWorkflowAuthReady(operation, options.root ?? gates[0].repoPath);
359
+ const results = [];
360
+ for (const gate of gates) {
361
+ if (options.root && options.runId) {
362
+ const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
363
+ repository: gate.repository ?? null,
364
+ workflow: gate.workflow,
365
+ headSha: gate.headSha,
366
+ branch: gate.branch
367
+ });
368
+ if (cached) {
369
+ results.push({
370
+ ...cached.result,
371
+ name: gate.name,
372
+ cached: true
373
+ });
374
+ continue;
375
+ }
376
+ }
377
+ const result = await waitForGitHubWorkflowCompletion(gate.repoPath, {
378
+ repository: gate.repository,
379
+ workflow: gate.workflow,
380
+ headSha: gate.headSha,
381
+ branch: gate.branch
382
+ });
383
+ const normalized = {
384
+ name: gate.name,
385
+ ...result,
386
+ workflow: String(result.workflow ?? gate.workflow),
387
+ branch: String(result.branch ?? gate.branch),
388
+ headSha: String(result.headSha ?? gate.headSha)
389
+ };
390
+ if (normalized.status === "completed" && normalized.conclusion !== "success") {
391
+ workflowError(operation, "github_workflow_failed", workflowGateFailureMessage(gate, normalized), {
392
+ details: { gate, workflow: normalized }
393
+ });
394
+ }
395
+ if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
396
+ cacheWorkflowGateResult(options.root, options.runId, normalized);
397
+ }
398
+ results.push(normalized);
399
+ }
400
+ return results;
401
+ }
176
402
  function ensureTreeseedCommandReadiness(root) {
177
403
  if (getGitHubAutomationMode() === "stub") {
178
404
  return {
@@ -295,7 +521,8 @@ function createRepoReport(name, path, branch, dirty) {
295
521
  tagName: null,
296
522
  commitSha: branch ? headCommit(path) : null,
297
523
  skippedReason: null,
298
- publishWait: null
524
+ publishWait: null,
525
+ workflowGates: []
299
526
  };
300
527
  }
301
528
  function createWorkspaceRootRepoReport(root) {
@@ -712,6 +939,10 @@ function findAutoResumableSaveRun(root, branch) {
712
939
  if (!branch) return null;
713
940
  return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
714
941
  }
942
+ function findAutoResumableTaskRun(root, command, branch) {
943
+ if (!branch) return null;
944
+ return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
945
+ }
715
946
  function stringRecord(value) {
716
947
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
717
948
  }
@@ -752,20 +983,24 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
752
983
  if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
753
984
  return false;
754
985
  }
986
+ const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
987
+ const nextStep = nextPendingJournalStep(journal);
755
988
  if (releaseRunHasCompletedMutation(journal)) {
989
+ if (nextStep?.id === "release-root" && releasePlanHead(releasePlan ?? {}, rootRepo.name) !== rootRepo.commitSha) {
990
+ return false;
991
+ }
756
992
  return true;
757
993
  }
758
- const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
759
994
  return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
760
995
  }) ?? null;
761
996
  }
762
- async function executeJournalStep(root, runId, stepId, action) {
997
+ async function executeJournalStep(root, runId, stepId, action, options = {}) {
763
998
  const current = readWorkflowRunJournal(root, runId);
764
999
  const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
765
1000
  if (!current || !step) {
766
1001
  throw new Error(`Unknown workflow step "${stepId}" for run ${runId}.`);
767
1002
  }
768
- if (step.status === "completed") {
1003
+ if (step.status === "completed" && !options.rerunCompleted) {
769
1004
  return step.data ?? null;
770
1005
  }
771
1006
  const data = await Promise.resolve(action());
@@ -918,7 +1153,7 @@ function runReleaseNpmInstall(repoDir, options = {}) {
918
1153
  if (shouldSkipReleaseInstall()) {
919
1154
  return { status: "skipped", reason: "stubbed" };
920
1155
  }
921
- const args = repoDir === options.workspaceRoot ? ["install"] : ["install", "--workspaces=false"];
1156
+ const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
922
1157
  run("npm", args, { cwd: repoDir });
923
1158
  return { status: "completed", reason: null };
924
1159
  }
@@ -976,7 +1211,7 @@ function buildReleasePlanSnapshot(input) {
976
1211
  plannedPublishWaits: input.packageSelection.selected.map((name) => ({
977
1212
  name,
978
1213
  workflow: "publish.yml",
979
- branch: PRODUCTION_BRANCH,
1214
+ branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
980
1215
  status: "planned"
981
1216
  })),
982
1217
  touchedPackages: input.packageSelection.selected,
@@ -1029,7 +1264,7 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
1029
1264
  if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1030
1265
  return;
1031
1266
  }
1032
- createGitHubApiClient();
1267
+ assertHostedGitHubWorkflowAuthReady("release", root);
1033
1268
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1034
1269
  if (!selectedPackageNames.has(pkg.name)) continue;
1035
1270
  resolveGitHubRepositorySlug(pkg.dir);
@@ -1097,6 +1332,14 @@ function previewStateFor(tenantRoot, branchName) {
1097
1332
  target: createBranchPreviewDeployTarget(branchName)
1098
1333
  });
1099
1334
  }
1335
+ function branchPreviewInitialized(tenantRoot, branchName) {
1336
+ if (!branchName) return false;
1337
+ try {
1338
+ return previewStateFor(tenantRoot, branchName).readiness?.initialized === true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1100
1343
  async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
1101
1344
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
1102
1345
  assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
@@ -1706,7 +1949,10 @@ async function workflowExport(helpers, input = {}) {
1706
1949
  return await withContextEnv(helpers.context.env, async () => {
1707
1950
  const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
1708
1951
  const exported = await exportTreeseedCodebase({ directory });
1709
- return buildWorkflowResult("export", exported.tenantRoot, exported);
1952
+ return buildWorkflowResult("export", exported.tenantRoot, {
1953
+ ...exported,
1954
+ ...worktreePayload(exported.tenantRoot, input.worktreeMode)
1955
+ });
1710
1956
  });
1711
1957
  }
1712
1958
  async function workflowSwitch(helpers, input) {
@@ -1721,6 +1967,27 @@ async function workflowSwitch(helpers, input) {
1721
1967
  }
1722
1968
  const preview = input.preview === true;
1723
1969
  const executionMode = normalizeExecutionMode(input);
1970
+ if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
1971
+ const managed = ensureManagedWorkflowWorktree({
1972
+ root,
1973
+ branchName,
1974
+ mode: input.worktreeMode,
1975
+ env: helpers.context.env
1976
+ });
1977
+ const result = await workflowSwitch(helpersForCwd(helpers, managed.worktreePath), {
1978
+ ...input,
1979
+ worktreeMode: "off"
1980
+ });
1981
+ return {
1982
+ ...result,
1983
+ payload: {
1984
+ ...result.payload,
1985
+ worktreeMode: input.worktreeMode ?? "auto",
1986
+ worktreePath: managed.worktreePath,
1987
+ managedWorktree: managed
1988
+ }
1989
+ };
1990
+ }
1724
1991
  const mode = session.mode;
1725
1992
  const repoDir = session.gitRoot;
1726
1993
  const rootRepo = createWorkspaceRootRepoReport(root);
@@ -1743,6 +2010,8 @@ async function workflowSwitch(helpers, input) {
1743
2010
  rootRepo,
1744
2011
  repos: packageReports,
1745
2012
  previewRequested: preview,
2013
+ worktreeMode: input.worktreeMode ?? "auto",
2014
+ worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
1746
2015
  blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
1747
2016
  plannedSteps: [
1748
2017
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
@@ -1768,7 +2037,7 @@ async function workflowSwitch(helpers, input) {
1768
2037
  const workflowRun = acquireWorkflowRun(
1769
2038
  "switch",
1770
2039
  session,
1771
- { branch: branchName, preview },
2040
+ { branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
1772
2041
  [
1773
2042
  { id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
1774
2043
  ...packageReports.map((report) => ({
@@ -1845,6 +2114,7 @@ async function workflowSwitch(helpers, input) {
1845
2114
  },
1846
2115
  previewResult,
1847
2116
  workspaceLinks,
2117
+ ...worktreePayload(root, input.worktreeMode),
1848
2118
  preconditions: {
1849
2119
  cleanWorktreeRequired: true,
1850
2120
  baseBranch: STAGING_BRANCH
@@ -1971,6 +2241,7 @@ async function workflowSave(helpers, input) {
1971
2241
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
1972
2242
  const message = String(effectiveInput.message ?? "").trim();
1973
2243
  const optionsHotfix = effectiveInput.hotfix === true;
2244
+ const previewInitialized = branchPreviewInitialized(root, branch);
1974
2245
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
1975
2246
  if (!branch) {
1976
2247
  workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
@@ -1998,7 +2269,7 @@ async function workflowSave(helpers, input) {
1998
2269
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
1999
2270
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2000
2271
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2001
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2272
+ verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2002
2273
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
2003
2274
  });
2004
2275
  const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
@@ -2020,6 +2291,9 @@ async function workflowSave(helpers, input) {
2020
2291
  failure: planAutoResumeRun.failure
2021
2292
  } : null,
2022
2293
  workspaceLinks,
2294
+ ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2295
+ verifyMode: effectiveInput.verifyMode ?? "fast",
2296
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2023
2297
  repositoryPlan,
2024
2298
  waves: repositoryPlan.waves,
2025
2299
  plannedVersions: repositoryPlan.plannedVersions,
@@ -2028,7 +2302,7 @@ async function workflowSave(helpers, input) {
2028
2302
  ...repositoryPlan.plannedSteps,
2029
2303
  { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2030
2304
  { id: "workspace-link", description: "Restore local workspace links after save" },
2031
- ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2305
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2032
2306
  ]
2033
2307
  },
2034
2308
  {
@@ -2061,7 +2335,9 @@ async function workflowSave(helpers, input) {
2061
2335
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2062
2336
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2063
2337
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2064
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2338
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
2339
+ ciMode: effectiveInput.ciMode ?? "auto",
2340
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2065
2341
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2066
2342
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2067
2343
  },
@@ -2074,7 +2350,15 @@ async function workflowSave(helpers, input) {
2074
2350
  branch,
2075
2351
  resumable: true
2076
2352
  },
2077
- ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && beforeState.preview.enabled) ? [{
2353
+ ...shouldUseHostedSaveCi(effectiveInput) ? [{
2354
+ id: "hosted-ci",
2355
+ description: `Wait for hosted save workflows on ${branch}`,
2356
+ repoName: rootRepo.name,
2357
+ repoPath: rootRepo.path,
2358
+ branch,
2359
+ resumable: true
2360
+ }] : [],
2361
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && previewInitialized) ? [{
2078
2362
  id: "preview",
2079
2363
  description: `Refresh preview ${branch}`,
2080
2364
  repoName: rootRepo.name,
@@ -2108,7 +2392,7 @@ async function workflowSave(helpers, input) {
2108
2392
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2109
2393
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2110
2394
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2111
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2395
+ verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2112
2396
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2113
2397
  workflowRunId: workflowRun.runId,
2114
2398
  onProgress: (line, stream) => helpers.write(line, stream)
@@ -2135,14 +2419,30 @@ async function workflowSave(helpers, input) {
2135
2419
  lockfileValidation: repo.lockfileValidation
2136
2420
  }))
2137
2421
  };
2422
+ const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates("save", [
2423
+ ...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
2424
+ name: savedRootRepo.name,
2425
+ repoPath: savedRootRepo.path,
2426
+ workflow: "verify.yml",
2427
+ branch,
2428
+ headSha: savedRootRepo.commitSha
2429
+ }] : [],
2430
+ ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
2431
+ name: repo.name,
2432
+ repoPath: repo.path,
2433
+ workflow: "verify.yml",
2434
+ branch: String(repo.branch),
2435
+ headSha: String(repo.commitSha)
2436
+ }))
2437
+ ], "hosted", { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates }))) : { workflowGates: [] };
2138
2438
  let previewAction = { status: "skipped" };
2139
2439
  if (beforeState.branchRole === "feature" && branch) {
2140
2440
  if (effectiveInput.preview === true) {
2141
2441
  previewAction = {
2142
- status: beforeState.preview.enabled ? "refreshed" : "created",
2143
- details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
2442
+ status: previewInitialized ? "refreshed" : "created",
2443
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
2144
2444
  };
2145
- } else if (effectiveInput.refreshPreview !== false && beforeState.preview.enabled) {
2445
+ } else if (effectiveInput.refreshPreview !== false && previewInitialized) {
2146
2446
  previewAction = {
2147
2447
  status: "refreshed",
2148
2448
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
@@ -2171,7 +2471,11 @@ async function workflowSave(helpers, input) {
2171
2471
  mergeConflict: null,
2172
2472
  workspaceLinks,
2173
2473
  commandReadiness,
2174
- lockfileValidation
2474
+ lockfileValidation,
2475
+ ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2476
+ verifyMode: effectiveInput.verifyMode ?? "fast",
2477
+ workflowGates: saveWorkflowGates?.workflowGates ?? [],
2478
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2175
2479
  };
2176
2480
  completeWorkflowRun(root, workflowRun.runId, payload);
2177
2481
  return buildWorkflowResult(
@@ -2228,9 +2532,13 @@ async function workflowClose(helpers, input) {
2228
2532
  return await withContextEnv(helpers.context.env, async () => {
2229
2533
  const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
2230
2534
  const root = workspaceRoot(tenantRoot);
2231
- const message = ensureMessage("close", input.message, "a close reason");
2232
2535
  const executionMode = normalizeExecutionMode(input);
2233
2536
  const session = resolveTreeseedWorkflowSession(root);
2537
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2538
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
2539
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
2540
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2541
+ const message = ensureMessage("close", effectiveInput.message, "a close reason");
2234
2542
  if (executionMode === "plan") {
2235
2543
  const branchName = session.branchName;
2236
2544
  const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
@@ -2241,18 +2549,26 @@ async function workflowClose(helpers, input) {
2241
2549
  mode: session.mode,
2242
2550
  branchName,
2243
2551
  message,
2552
+ autoResumeCandidate: planAutoResumeRun ? {
2553
+ runId: planAutoResumeRun.runId,
2554
+ branch: planAutoResumeRun.session.branchName,
2555
+ failure: planAutoResumeRun.failure
2556
+ } : null,
2557
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2244
2558
  autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
2245
2559
  repos: createWorkspacePackageReports(root),
2246
2560
  rootRepo: createWorkspaceRootRepoReport(root),
2247
2561
  blockers,
2248
2562
  plannedSteps: [
2563
+ { id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
2249
2564
  { id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
2250
2565
  { id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
2251
2566
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
2252
2567
  id: `cleanup-${pkg.name}`,
2253
2568
  description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
2254
2569
  })),
2255
- { id: "workspace-link", description: "Restore local workspace links on the final branch" }
2570
+ { id: "workspace-link", description: "Restore local workspace links on the final branch" },
2571
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
2256
2572
  ]
2257
2573
  },
2258
2574
  {
@@ -2263,12 +2579,10 @@ async function workflowClose(helpers, input) {
2263
2579
  }
2264
2580
  );
2265
2581
  }
2266
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2267
2582
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
2268
2583
  message,
2269
- autoSave: input.autoSave
2584
+ autoSave: effectiveInput.autoSave
2270
2585
  });
2271
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2272
2586
  const activeSession = resolveTreeseedWorkflowSession(root);
2273
2587
  const featureBranch = assertFeatureBranch(root);
2274
2588
  const mode = activeSession.mode;
@@ -2284,8 +2598,14 @@ async function workflowClose(helpers, input) {
2284
2598
  const workflowRun = acquireWorkflowRun(
2285
2599
  "close",
2286
2600
  activeSession,
2287
- { message, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
2601
+ {
2602
+ message,
2603
+ deletePreview: effectiveInput.deletePreview !== false,
2604
+ deleteBranch: effectiveInput.deleteBranch !== false,
2605
+ worktreeMode: effectiveInput.worktreeMode ?? "auto"
2606
+ },
2288
2607
  [
2608
+ { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2289
2609
  { id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2290
2610
  { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2291
2611
  ...packageReports.map((report) => ({
@@ -2295,23 +2615,35 @@ async function workflowClose(helpers, input) {
2295
2615
  repoPath: report.path,
2296
2616
  branch: featureBranch,
2297
2617
  resumable: true
2298
- }))
2618
+ })),
2619
+ { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2620
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
2299
2621
  ],
2300
- helpers.context
2622
+ autoResumeRun ? {
2623
+ ...helpers.context,
2624
+ workflow: {
2625
+ ...helpers.context.workflow ?? {},
2626
+ resumeRunId: autoResumeRun.runId
2627
+ }
2628
+ } : helpers.context
2301
2629
  );
2630
+ if (autoResumeRun) {
2631
+ helpers.write(`[workflow][resume] Resuming interrupted close ${autoResumeRun.runId} on ${featureBranch}.`);
2632
+ }
2302
2633
  try {
2303
- const previewCleanup = input.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
2634
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2635
+ const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
2304
2636
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
2305
2637
  const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
2306
- const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2638
+ const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2307
2639
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2308
- if (input.deleteBranch !== false) {
2640
+ if (effectiveInput.deleteBranch !== false) {
2309
2641
  deleteLocalBranch(repoDir, featureBranch);
2310
2642
  }
2311
2643
  return {
2312
2644
  deprecatedTag,
2313
2645
  deletedRemote,
2314
- deletedLocal: input.deleteBranch !== false,
2646
+ deletedLocal: effectiveInput.deleteBranch !== false,
2315
2647
  branch: currentBranch(repoDir) || STAGING_BRANCH,
2316
2648
  dirty: hasMeaningfulChanges(repoDir)
2317
2649
  };
@@ -2328,12 +2660,15 @@ async function workflowClose(helpers, input) {
2328
2660
  continue;
2329
2661
  }
2330
2662
  const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
2331
- deleteBranch: input.deleteBranch !== false,
2663
+ deleteBranch: effectiveInput.deleteBranch !== false,
2332
2664
  targetBranch: STAGING_BRANCH
2333
2665
  }));
2334
2666
  Object.assign(report, cleanup);
2335
2667
  }
2336
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2668
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
2669
+ const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
2670
+ const managedWorktree = managedWorkflowWorktreeMetadata(root);
2671
+ const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
2337
2672
  const payload = {
2338
2673
  mode,
2339
2674
  branchName: featureBranch,
@@ -2346,8 +2681,13 @@ async function workflowClose(helpers, input) {
2346
2681
  previewCleanup,
2347
2682
  remoteDeleted: rootRepo.deletedRemote,
2348
2683
  localDeleted: rootRepo.deletedLocal,
2349
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2350
- workspaceLinks
2684
+ finalBranch,
2685
+ workspaceLinks,
2686
+ worktreeCleanup,
2687
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2688
+ managedWorktree,
2689
+ worktreePath: managedWorktree?.worktreePath ?? null,
2690
+ primaryRoot: managedWorktree?.primaryRoot ?? null
2351
2691
  };
2352
2692
  completeWorkflowRun(root, workflowRun.runId, payload);
2353
2693
  return buildWorkflowResult(
@@ -2362,7 +2702,7 @@ async function workflowClose(helpers, input) {
2362
2702
  }
2363
2703
  );
2364
2704
  } catch (error) {
2365
- ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2705
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2366
2706
  failWorkflowRun(root, workflowRun.runId, error, {
2367
2707
  resumable: true,
2368
2708
  runId: workflowRun.runId,
@@ -2383,11 +2723,19 @@ async function workflowStage(helpers, input) {
2383
2723
  return await withContextEnv(helpers.context.env, async () => {
2384
2724
  const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
2385
2725
  const root = workspaceRoot(tenantRoot);
2386
- const message = ensureMessage("stage", input.message, "a resolution message");
2387
2726
  const executionMode = normalizeExecutionMode(input);
2388
2727
  const initialSession = resolveTreeseedWorkflowSession(root);
2728
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2729
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
2730
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
2731
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2732
+ const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
2733
+ const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
2389
2734
  if (executionMode === "plan") {
2390
2735
  const blockers = [];
2736
+ if (initialSession.branchRole !== "feature") {
2737
+ blockers.push("Stage only applies to task branches.");
2738
+ }
2391
2739
  try {
2392
2740
  validateStagingWorkflowContracts(root);
2393
2741
  } catch (error) {
@@ -2402,6 +2750,13 @@ async function workflowStage(helpers, input) {
2402
2750
  mergeTarget: STAGING_BRANCH,
2403
2751
  mergeStrategy: "squash",
2404
2752
  message,
2753
+ ciMode,
2754
+ autoResumeCandidate: planAutoResumeRun ? {
2755
+ runId: planAutoResumeRun.runId,
2756
+ branch: planAutoResumeRun.session.branchName,
2757
+ failure: planAutoResumeRun.failure
2758
+ } : null,
2759
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2405
2760
  autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
2406
2761
  blockers,
2407
2762
  rootRepo: createWorkspaceRootRepoReport(root),
@@ -2414,7 +2769,7 @@ async function workflowStage(helpers, input) {
2414
2769
  { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
2415
2770
  { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
2416
2771
  { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
2417
- { id: "wait-staging", description: "Wait for staging automation" },
2772
+ { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
2418
2773
  { id: "preview-cleanup", description: "Destroy preview resources" },
2419
2774
  { id: "cleanup-root", description: "Archive and delete the task branch from market" },
2420
2775
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
@@ -2432,12 +2787,10 @@ async function workflowStage(helpers, input) {
2432
2787
  }
2433
2788
  );
2434
2789
  }
2435
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2436
2790
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
2437
2791
  message,
2438
- autoSave: input.autoSave
2792
+ autoSave: effectiveInput.autoSave
2439
2793
  });
2440
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2441
2794
  const session = resolveTreeseedWorkflowSession(root);
2442
2795
  const featureBranch = assertFeatureBranch(root);
2443
2796
  const mode = session.mode;
@@ -2447,6 +2800,8 @@ async function workflowStage(helpers, input) {
2447
2800
  } else {
2448
2801
  assertCleanWorktree(root);
2449
2802
  }
2803
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2804
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2450
2805
  validateStagingWorkflowContracts(root);
2451
2806
  runWorkspaceSavePreflight({ cwd: root });
2452
2807
  const repoDir = session.gitRoot;
@@ -2455,8 +2810,16 @@ async function workflowStage(helpers, input) {
2455
2810
  const workflowRun = acquireWorkflowRun(
2456
2811
  "stage",
2457
2812
  session,
2458
- { message, waitForStaging: input.waitForStaging !== false, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
2813
+ {
2814
+ message,
2815
+ waitForStaging: effectiveInput.waitForStaging !== false,
2816
+ deletePreview: effectiveInput.deletePreview !== false,
2817
+ deleteBranch: effectiveInput.deleteBranch !== false,
2818
+ ciMode,
2819
+ worktreeMode: effectiveInput.worktreeMode ?? "auto"
2820
+ },
2459
2821
  [
2822
+ { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2460
2823
  ...packageReports.map((report) => ({
2461
2824
  id: `merge-${report.name}`,
2462
2825
  description: `Merge ${featureBranch} into ${report.name} staging`,
@@ -2466,7 +2829,7 @@ async function workflowStage(helpers, input) {
2466
2829
  resumable: true
2467
2830
  })),
2468
2831
  { id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2469
- { id: "wait-staging", description: "Wait for staging automation", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2832
+ { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2470
2833
  { id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2471
2834
  { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2472
2835
  ...packageReports.map((report) => ({
@@ -2476,12 +2839,23 @@ async function workflowStage(helpers, input) {
2476
2839
  repoPath: report.path,
2477
2840
  branch: featureBranch,
2478
2841
  resumable: true
2479
- }))
2842
+ })),
2843
+ { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2844
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
2480
2845
  ],
2481
- helpers.context
2846
+ autoResumeRun ? {
2847
+ ...helpers.context,
2848
+ workflow: {
2849
+ ...helpers.context.workflow ?? {},
2850
+ resumeRunId: autoResumeRun.runId
2851
+ }
2852
+ } : helpers.context
2482
2853
  );
2854
+ if (autoResumeRun) {
2855
+ helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
2856
+ }
2483
2857
  try {
2484
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2858
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2485
2859
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2486
2860
  const report = findReportByName(packageReports, pkg.name);
2487
2861
  if (!report) {
@@ -2513,7 +2887,11 @@ async function workflowStage(helpers, input) {
2513
2887
  try {
2514
2888
  rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
2515
2889
  assertCleanWorktree(root);
2516
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2890
+ if (isManagedWorkflowWorktree(root)) {
2891
+ checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
2892
+ } else {
2893
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2894
+ }
2517
2895
  run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
2518
2896
  if (mode === "recursive-workspace") {
2519
2897
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
@@ -2528,10 +2906,14 @@ async function workflowStage(helpers, input) {
2528
2906
  run("git", ["add", "-A"], { cwd: repoDir });
2529
2907
  run("git", ["commit", "-m", message], { cwd: repoDir });
2530
2908
  }
2531
- pushBranch(repoDir, STAGING_BRANCH);
2909
+ if (isManagedWorkflowWorktree(root)) {
2910
+ pushHeadToBranch(repoDir, STAGING_BRANCH);
2911
+ } else {
2912
+ pushBranch(repoDir, STAGING_BRANCH);
2913
+ }
2532
2914
  return {
2533
2915
  commitSha: headCommit(repoDir),
2534
- branch: currentBranch(repoDir) || STAGING_BRANCH,
2916
+ branch: STAGING_BRANCH,
2535
2917
  committed: hasMeaningfulChanges(repoDir) ? false : true,
2536
2918
  lockfileValidation: lockfileSafety.lockfileValidation,
2537
2919
  lockfileInstall: lockfileSafety.install
@@ -2549,18 +2931,47 @@ async function workflowStage(helpers, input) {
2549
2931
  exitCode: 12
2550
2932
  });
2551
2933
  }
2552
- const stagingWait = input.waitForStaging === false ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForStagingAutomation(repoDir));
2553
- const previewCleanup = input.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
2934
+ const stageWorkflowGateResult = effectiveInput.waitForStaging === false ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForWorkflowGates("stage", [
2935
+ {
2936
+ name: rootRepo.name,
2937
+ repoPath: rootRepo.path,
2938
+ workflow: "verify.yml",
2939
+ branch: STAGING_BRANCH,
2940
+ headSha: rootRepo.commitSha
2941
+ },
2942
+ {
2943
+ name: rootRepo.name,
2944
+ repoPath: rootRepo.path,
2945
+ workflow: "deploy.yml",
2946
+ branch: STAGING_BRANCH,
2947
+ headSha: rootRepo.commitSha
2948
+ },
2949
+ ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
2950
+ name: report.name,
2951
+ repoPath: report.path,
2952
+ workflow: "verify.yml",
2953
+ branch: STAGING_BRANCH,
2954
+ headSha: String(report.commitSha)
2955
+ }))
2956
+ ], ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({
2957
+ status: "completed",
2958
+ workflowGates
2959
+ })));
2960
+ const stagingWait = {
2961
+ status: String(stageWorkflowGateResult?.status ?? "completed"),
2962
+ workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
2963
+ };
2964
+ const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
2554
2965
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
2555
2966
  const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
2556
- const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2557
- if (input.deleteBranch !== false) {
2967
+ const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2968
+ if (effectiveInput.deleteBranch !== false) {
2558
2969
  deleteLocalBranch(repoDir, featureBranch);
2559
2970
  }
2560
2971
  return {
2561
2972
  deprecatedTag,
2562
2973
  deletedRemote,
2563
- deletedLocal: input.deleteBranch !== false,
2974
+ deletedLocal: effectiveInput.deleteBranch !== false,
2564
2975
  branch: currentBranch(repoDir) || STAGING_BRANCH
2565
2976
  };
2566
2977
  });
@@ -2575,12 +2986,15 @@ async function workflowStage(helpers, input) {
2575
2986
  continue;
2576
2987
  }
2577
2988
  const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
2578
- deleteBranch: input.deleteBranch !== false,
2989
+ deleteBranch: effectiveInput.deleteBranch !== false,
2579
2990
  targetBranch: STAGING_BRANCH
2580
2991
  }));
2581
2992
  Object.assign(report, cleanup);
2582
2993
  }
2583
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2994
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
2995
+ const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
2996
+ const managedWorktree = managedWorkflowWorktreeMetadata(root);
2997
+ const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
2584
2998
  const payload = {
2585
2999
  mode,
2586
3000
  branchName: featureBranch,
@@ -2598,8 +3012,15 @@ async function workflowStage(helpers, input) {
2598
3012
  lockfileInstall: rootMerge?.lockfileInstall ?? null,
2599
3013
  remoteDeleted: rootRepo.deletedRemote,
2600
3014
  localDeleted: rootRepo.deletedLocal,
2601
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2602
- workspaceLinks
3015
+ finalBranch,
3016
+ workspaceLinks,
3017
+ ciMode,
3018
+ workflowGates: stagingWait.workflowGates,
3019
+ worktreeCleanup,
3020
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
3021
+ managedWorktree,
3022
+ worktreePath: managedWorktree?.worktreePath ?? null,
3023
+ primaryRoot: managedWorktree?.primaryRoot ?? null
2603
3024
  };
2604
3025
  completeWorkflowRun(root, workflowRun.runId, payload);
2605
3026
  return buildWorkflowResult(
@@ -2615,7 +3036,7 @@ async function workflowStage(helpers, input) {
2615
3036
  }
2616
3037
  );
2617
3038
  } catch (error) {
2618
- ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
3039
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2619
3040
  failWorkflowRun(root, workflowRun.runId, error, {
2620
3041
  resumable: true,
2621
3042
  runId: workflowRun.runId,
@@ -2646,6 +3067,7 @@ async function workflowRelease(helpers, input) {
2646
3067
  const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2647
3068
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2648
3069
  const level = effectiveInput.bump ?? "patch";
3070
+ const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
2649
3071
  const isResume = Boolean(explicitResumeRunId || autoResumeRun);
2650
3072
  const packageSelection = session.packageSelection;
2651
3073
  const selectedPackageNames = new Set(packageSelection.selected);
@@ -2662,6 +3084,8 @@ async function workflowRelease(helpers, input) {
2662
3084
  if (executionMode === "plan") {
2663
3085
  return buildWorkflowResult("release", root, {
2664
3086
  ...plannedRelease,
3087
+ ciMode,
3088
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2665
3089
  autoResumeCandidate: planAutoResumeRun ? {
2666
3090
  runId: planAutoResumeRun.runId,
2667
3091
  branch: planAutoResumeRun.session.branchName,
@@ -2687,6 +3111,8 @@ async function workflowRelease(helpers, input) {
2687
3111
  devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
2688
3112
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2689
3113
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3114
+ ciMode,
3115
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2690
3116
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2691
3117
  },
2692
3118
  [
@@ -2702,6 +3128,7 @@ async function workflowRelease(helpers, input) {
2702
3128
  resumable: true
2703
3129
  })),
2704
3130
  { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3131
+ { id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
2705
3132
  ...mode === "recursive-workspace" ? [{ id: "cleanup-dev-tags", description: "Clean replaced dev package tags", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : []
2706
3133
  ],
2707
3134
  autoResumeRun ? {
@@ -2728,19 +3155,22 @@ async function workflowRelease(helpers, input) {
2728
3155
  assertCleanWorktree(root);
2729
3156
  }
2730
3157
  prepareReleaseBranches(root);
3158
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2731
3159
  runWorkspaceSavePreflight({ cwd: root });
2732
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3160
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2733
3161
  if (mode === "root-only") {
2734
3162
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2735
3163
  setRootPackageJsonVersion(root, rootVersion);
2736
3164
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2737
3165
  commitAllIfChanged(gitRoot, `release: ${level} bump`);
2738
3166
  pushBranch(gitRoot, STAGING_BRANCH);
3167
+ const stagingCommit = headCommit(gitRoot);
2739
3168
  const released = mergeStagingIntoMain(root);
2740
3169
  const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
2741
3170
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2742
3171
  return {
2743
3172
  rootVersion,
3173
+ stagingCommit,
2744
3174
  releasedCommit: released.commitSha,
2745
3175
  tag
2746
3176
  };
@@ -2751,6 +3181,43 @@ async function workflowRelease(helpers, input) {
2751
3181
  rootRepo.branch = PRODUCTION_BRANCH;
2752
3182
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
2753
3183
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3184
+ const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3185
+ {
3186
+ name: rootRepo.name,
3187
+ repoPath: rootRepo.path,
3188
+ workflow: "verify.yml",
3189
+ branch: STAGING_BRANCH,
3190
+ headSha: String(rootRelease2?.stagingCommit ?? "")
3191
+ },
3192
+ {
3193
+ name: rootRepo.name,
3194
+ repoPath: rootRepo.path,
3195
+ workflow: "deploy.yml",
3196
+ branch: STAGING_BRANCH,
3197
+ headSha: String(rootRelease2?.stagingCommit ?? "")
3198
+ },
3199
+ {
3200
+ name: rootRepo.name,
3201
+ repoPath: rootRepo.path,
3202
+ workflow: "verify.yml",
3203
+ branch: rootVersion,
3204
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3205
+ },
3206
+ {
3207
+ name: rootRepo.name,
3208
+ repoPath: rootRepo.path,
3209
+ workflow: "verify.yml",
3210
+ branch: PRODUCTION_BRANCH,
3211
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3212
+ },
3213
+ {
3214
+ name: rootRepo.name,
3215
+ repoPath: rootRepo.path,
3216
+ workflow: "deploy.yml",
3217
+ branch: PRODUCTION_BRANCH,
3218
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3219
+ }
3220
+ ].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
2754
3221
  const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2755
3222
  const payload2 = {
2756
3223
  mode,
@@ -2771,7 +3238,10 @@ async function workflowRelease(helpers, input) {
2771
3238
  rootRepo,
2772
3239
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
2773
3240
  pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
2774
- workspaceLinks: workspaceLinks2
3241
+ workspaceLinks: workspaceLinks2,
3242
+ ciMode,
3243
+ workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
3244
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2775
3245
  };
2776
3246
  completeWorkflowRun(root, workflowRun.runId, payload2);
2777
3247
  return buildWorkflowResult("release", root, payload2, {
@@ -2806,7 +3276,7 @@ async function workflowRelease(helpers, input) {
2806
3276
  replacedDevReferences: replacedDevReferences2,
2807
3277
  releaseInstalls: releaseInstalls2
2808
3278
  };
2809
- });
3279
+ }, { rerunCompleted: workflowRun.resumed });
2810
3280
  const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
2811
3281
  const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
2812
3282
  const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
@@ -2839,18 +3309,38 @@ async function workflowRelease(helpers, input) {
2839
3309
  });
2840
3310
  const tagName = String(effectiveVersions.get(pkg.name));
2841
3311
  const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
2842
- const publish = await waitForGitHubWorkflowCompletion(pkg.dir, {
2843
- workflow: "publish.yml",
2844
- headSha: mergeResult.commitSha,
2845
- branch: PRODUCTION_BRANCH
2846
- });
3312
+ const workflowGates = await waitForWorkflowGates("release", [
3313
+ {
3314
+ name: pkg.name,
3315
+ repoPath: pkg.dir,
3316
+ workflow: "publish.yml",
3317
+ headSha: mergeResult.commitSha,
3318
+ branch: tagName
3319
+ },
3320
+ {
3321
+ name: pkg.name,
3322
+ repoPath: pkg.dir,
3323
+ workflow: "verify.yml",
3324
+ headSha: mergeResult.commitSha,
3325
+ branch: tagName
3326
+ },
3327
+ {
3328
+ name: pkg.name,
3329
+ repoPath: pkg.dir,
3330
+ workflow: "verify.yml",
3331
+ headSha: mergeResult.commitSha,
3332
+ branch: PRODUCTION_BRANCH
3333
+ }
3334
+ ], ciMode, { root, runId: workflowRun.runId });
3335
+ const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
2847
3336
  assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
2848
3337
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
2849
3338
  return {
2850
3339
  commitSha: mergeResult.commitSha,
2851
3340
  tagName,
2852
3341
  tag,
2853
- publish
3342
+ publish,
3343
+ workflowGates
2854
3344
  };
2855
3345
  });
2856
3346
  report.committed = true;
@@ -2859,6 +3349,7 @@ async function workflowRelease(helpers, input) {
2859
3349
  report.tagName = String(releasedPackage?.tagName ?? "");
2860
3350
  report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
2861
3351
  report.publishWait = releasedPackage?.publish ?? null;
3352
+ report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
2862
3353
  report.branch = STAGING_BRANCH;
2863
3354
  publishWait.push({
2864
3355
  name: report.name,
@@ -2871,6 +3362,7 @@ async function workflowRelease(helpers, input) {
2871
3362
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2872
3363
  commitAllIfChanged(gitRoot, `release: ${level} bump`);
2873
3364
  pushBranch(gitRoot, STAGING_BRANCH);
3365
+ const stagingCommit = headCommit(gitRoot);
2874
3366
  const released = mergeBranchIntoTarget(root, {
2875
3367
  sourceBranch: STAGING_BRANCH,
2876
3368
  targetBranch: PRODUCTION_BRANCH,
@@ -2890,6 +3382,7 @@ async function workflowRelease(helpers, input) {
2890
3382
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2891
3383
  return {
2892
3384
  rootVersion,
3385
+ stagingCommit,
2893
3386
  releasedCommit,
2894
3387
  mergeCommit: released.commitSha,
2895
3388
  tag
@@ -2901,6 +3394,43 @@ async function workflowRelease(helpers, input) {
2901
3394
  rootRepo.branch = PRODUCTION_BRANCH;
2902
3395
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
2903
3396
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
3397
+ const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3398
+ {
3399
+ name: rootRepo.name,
3400
+ repoPath: rootRepo.path,
3401
+ workflow: "verify.yml",
3402
+ branch: STAGING_BRANCH,
3403
+ headSha: String(rootRelease?.stagingCommit ?? "")
3404
+ },
3405
+ {
3406
+ name: rootRepo.name,
3407
+ repoPath: rootRepo.path,
3408
+ workflow: "deploy.yml",
3409
+ branch: STAGING_BRANCH,
3410
+ headSha: String(rootRelease?.stagingCommit ?? "")
3411
+ },
3412
+ {
3413
+ name: rootRepo.name,
3414
+ repoPath: rootRepo.path,
3415
+ workflow: "verify.yml",
3416
+ branch: rootVersion,
3417
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3418
+ },
3419
+ {
3420
+ name: rootRepo.name,
3421
+ repoPath: rootRepo.path,
3422
+ workflow: "verify.yml",
3423
+ branch: PRODUCTION_BRANCH,
3424
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3425
+ },
3426
+ {
3427
+ name: rootRepo.name,
3428
+ repoPath: rootRepo.path,
3429
+ workflow: "deploy.yml",
3430
+ branch: PRODUCTION_BRANCH,
3431
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3432
+ }
3433
+ ].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
2904
3434
  const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
2905
3435
  const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
2906
3436
  const activeDevTags = collectActiveDevTagReferences(root);
@@ -2952,7 +3482,13 @@ async function workflowRelease(helpers, input) {
2952
3482
  productionPushed: true,
2953
3483
  tagPushed: true
2954
3484
  },
2955
- workspaceLinks
3485
+ workspaceLinks,
3486
+ ciMode,
3487
+ workflowGates: [
3488
+ ...packageReports.flatMap((report) => report.workflowGates),
3489
+ ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
3490
+ ],
3491
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2956
3492
  };
2957
3493
  completeWorkflowRun(root, workflowRun.runId, payload);
2958
3494
  return buildWorkflowResult("release", root, payload, {
@@ -3002,7 +3538,21 @@ async function workflowResume(helpers, input) {
3002
3538
  details: { runId, status: journal.status }
3003
3539
  });
3004
3540
  }
3005
- const resumedHelpers = {
3541
+ const session = resolveTreeseedWorkflowSession(root);
3542
+ const currentHeads = Object.fromEntries(
3543
+ [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
3544
+ );
3545
+ const classification = classifyWorkflowRunJournal(journal, {
3546
+ currentBranch: session.branchName,
3547
+ currentHeads
3548
+ });
3549
+ if (classification.state !== "resumable") {
3550
+ workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
3551
+ details: { runId, status: journal.status, classification }
3552
+ });
3553
+ }
3554
+ const resumeRoot = typeof journal.session?.root === "string" && existsSync(journal.session.root) ? journal.session.root : root;
3555
+ const resumedHelpers = helpersForCwd({
3006
3556
  ...helpers,
3007
3557
  context: {
3008
3558
  ...helpers.context,
@@ -3011,7 +3561,7 @@ async function workflowResume(helpers, input) {
3011
3561
  resumeRunId: runId
3012
3562
  }
3013
3563
  }
3014
- };
3564
+ }, resumeRoot);
3015
3565
  switch (journal.command) {
3016
3566
  case "switch":
3017
3567
  return workflowSwitch(resumedHelpers, journal.input);
@@ -3041,7 +3591,15 @@ async function workflowRecover(helpers, input = {}) {
3041
3591
  const root = resolveProjectRootOrThrow("recover", helpers.cwd());
3042
3592
  const lock = inspectWorkflowLock(root);
3043
3593
  const journals = listWorkflowRunJournals(root);
3044
- const interruptedRuns = listInterruptedWorkflowRuns(root).map((journal) => ({
3594
+ const session = resolveTreeseedWorkflowSession(root);
3595
+ const currentHeads = Object.fromEntries(
3596
+ [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
3597
+ );
3598
+ const classifiedRuns = classifyWorkflowRunJournals(root, {
3599
+ currentBranch: session.branchName,
3600
+ currentHeads
3601
+ });
3602
+ const interruptedRuns = classifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
3045
3603
  runId: journal.runId,
3046
3604
  command: journal.command,
3047
3605
  status: journal.status,
@@ -3051,6 +3609,29 @@ async function workflowRecover(helpers, input = {}) {
3051
3609
  failure: journal.failure,
3052
3610
  resumeCommand: `treeseed resume ${journal.runId}`
3053
3611
  }));
3612
+ const staleRuns = classifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
3613
+ runId: journal.runId,
3614
+ command: journal.command,
3615
+ status: journal.status,
3616
+ createdAt: journal.createdAt,
3617
+ updatedAt: journal.updatedAt,
3618
+ nextStep: nextPendingJournalStep(journal)?.description ?? null,
3619
+ failure: journal.failure,
3620
+ classification
3621
+ }));
3622
+ const obsoleteRuns = classifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
3623
+ runId: journal.runId,
3624
+ command: journal.command,
3625
+ status: journal.status,
3626
+ createdAt: journal.createdAt,
3627
+ updatedAt: journal.updatedAt,
3628
+ failure: journal.failure,
3629
+ classification
3630
+ }));
3631
+ const prunedRuns = input.pruneStale === true ? staleRuns.map((run2) => {
3632
+ archiveWorkflowRun(root, run2.runId, run2.classification);
3633
+ return run2;
3634
+ }) : [];
3054
3635
  const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
3055
3636
  return buildWorkflowResult(
3056
3637
  "recover",
@@ -3058,13 +3639,16 @@ async function workflowRecover(helpers, input = {}) {
3058
3639
  {
3059
3640
  lock,
3060
3641
  interruptedRuns,
3642
+ staleRuns,
3643
+ obsoleteRuns,
3644
+ prunedRuns,
3061
3645
  selectedRun,
3062
3646
  runCount: journals.length
3063
3647
  },
3064
3648
  {
3065
3649
  includeFinalState: false,
3066
3650
  nextSteps: createNextSteps([
3067
- ...interruptedRuns.length > 0 ? [{ operation: "resume", reason: "Resume the most recent interrupted workflow run.", input: { runId: interruptedRuns[0].runId } }] : [{ operation: "status", reason: "No interrupted runs were found; inspect current workflow state instead." }]
3651
+ ...interruptedRuns.length > 0 ? [{ operation: "resume", reason: "Resume the most recent interrupted workflow run.", input: { runId: interruptedRuns[0].runId } }] : staleRuns.length > 0 && input.pruneStale !== true ? [{ operation: "recover", reason: "Archive stale interrupted runs that no longer match current heads.", input: { pruneStale: true } }] : [{ operation: "status", reason: "No interrupted runs were found; inspect current workflow state instead." }]
3068
3652
  ])
3069
3653
  }
3070
3654
  );