@treeseed/sdk 0.6.9 → 0.6.11

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.
@@ -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,84 @@ 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
+ }
235
+ function unresolvedMergePaths(repoDir) {
236
+ return run("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
237
+ }
238
+ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
239
+ const gitRoot = repoRoot(root);
240
+ const packages = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageNames.has(pkg.name)).map((pkg) => ({
241
+ ...pkg,
242
+ repoPath: relative(gitRoot, pkg.dir)
243
+ }));
244
+ const packagePaths = new Set(packages.map((pkg) => pkg.repoPath));
245
+ const unresolved = unresolvedMergePaths(gitRoot);
246
+ if (unresolved.length === 0 || unresolved.some((filePath) => !packagePaths.has(filePath))) {
247
+ return false;
248
+ }
249
+ for (const pkg of packages) {
250
+ syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
251
+ run("git", ["add", pkg.repoPath], { cwd: gitRoot });
252
+ }
253
+ return true;
254
+ }
166
255
  function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
167
256
  if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
168
257
  return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
@@ -173,6 +262,163 @@ function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
173
262
  }
174
263
  return report;
175
264
  }
265
+ function normalizeCiMode(mode, operation) {
266
+ if (mode === "hosted" || mode === "off") return mode;
267
+ return operation === "save" ? "off" : "hosted";
268
+ }
269
+ function normalizeSaveVerifyMode(mode) {
270
+ switch (mode) {
271
+ case "skip":
272
+ case "fast":
273
+ case void 0:
274
+ return "skip";
275
+ case "local":
276
+ case "local-only":
277
+ return "local-only";
278
+ case "hosted":
279
+ return "skip";
280
+ case "both":
281
+ case "action-first":
282
+ return "action-first";
283
+ default:
284
+ return "skip";
285
+ }
286
+ }
287
+ function shouldUseHostedSaveCi(input) {
288
+ return normalizeCiMode(input.ciMode, "save") === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both";
289
+ }
290
+ function worktreePayload(root, requestedMode) {
291
+ const metadata = managedWorkflowWorktreeMetadata(root);
292
+ return {
293
+ worktreeMode: requestedMode ?? "auto",
294
+ managedWorktree: metadata,
295
+ worktreePath: metadata?.worktreePath ?? null,
296
+ primaryRoot: metadata?.primaryRoot ?? null
297
+ };
298
+ }
299
+ function helpersForCwd(helpers, cwd) {
300
+ return {
301
+ ...helpers,
302
+ context: {
303
+ ...helpers.context,
304
+ cwd
305
+ },
306
+ cwd: () => cwd
307
+ };
308
+ }
309
+ function shouldDispatchSwitchToManagedWorktree(root, input, env) {
310
+ return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
311
+ }
312
+ function skippedWorkflowGate(gate, reason) {
313
+ return {
314
+ name: gate.name,
315
+ repository: gate.repository ?? null,
316
+ workflow: gate.workflow,
317
+ branch: gate.branch,
318
+ headSha: gate.headSha,
319
+ status: "skipped",
320
+ reason,
321
+ conclusion: null,
322
+ runId: null,
323
+ url: null
324
+ };
325
+ }
326
+ function workflowGateFailureMessage(gate, result) {
327
+ const repository = String(result.repository ?? gate.repository ?? gate.name);
328
+ const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
329
+ const url = typeof result.url === "string" && result.url ? `
330
+ ${result.url}` : "";
331
+ const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => stringRecord(job)?.name).filter((name) => typeof name === "string" && name.length > 0) : [];
332
+ const jobLine = failedJobs.length > 0 ? `
333
+ Failed jobs: ${failedJobs.join(", ")}` : "";
334
+ const command = runId ? `
335
+ Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
336
+ return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
337
+ }
338
+ function assertHostedGitHubWorkflowAuthReady(operation, root) {
339
+ if (getGitHubAutomationMode() === "stub") {
340
+ return null;
341
+ }
342
+ const tools = collectTreeseedToolStatus({
343
+ tenantRoot: root,
344
+ env: process.env
345
+ });
346
+ const github = tools.auth.github;
347
+ if (!github.authenticated) {
348
+ const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
349
+ workflowError(
350
+ operation,
351
+ "github_auth_unavailable",
352
+ [
353
+ "Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
354
+ github.detail,
355
+ `Managed gh check: ${command}`,
356
+ "Remediation:",
357
+ ...github.remediation.map((item) => `- ${item}`)
358
+ ].join("\n"),
359
+ {
360
+ details: {
361
+ toolsHome: tools.toolsHome,
362
+ ghConfigDir: tools.ghConfigDir,
363
+ github,
364
+ tools: tools.tools
365
+ }
366
+ }
367
+ );
368
+ }
369
+ return tools;
370
+ }
371
+ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
372
+ if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
373
+ return gates.map((gate) => skippedWorkflowGate(gate, ciMode === "off" ? "disabled" : "stubbed"));
374
+ }
375
+ if (gates.length === 0) {
376
+ return [];
377
+ }
378
+ assertHostedGitHubWorkflowAuthReady(operation, options.root ?? gates[0].repoPath);
379
+ const results = [];
380
+ for (const gate of gates) {
381
+ if (options.root && options.runId) {
382
+ const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
383
+ repository: gate.repository ?? null,
384
+ workflow: gate.workflow,
385
+ headSha: gate.headSha,
386
+ branch: gate.branch
387
+ });
388
+ if (cached) {
389
+ results.push({
390
+ ...cached.result,
391
+ name: gate.name,
392
+ cached: true
393
+ });
394
+ continue;
395
+ }
396
+ }
397
+ const result = await waitForGitHubWorkflowCompletion(gate.repoPath, {
398
+ repository: gate.repository,
399
+ workflow: gate.workflow,
400
+ headSha: gate.headSha,
401
+ branch: gate.branch
402
+ });
403
+ const normalized = {
404
+ name: gate.name,
405
+ ...result,
406
+ workflow: String(result.workflow ?? gate.workflow),
407
+ branch: String(result.branch ?? gate.branch),
408
+ headSha: String(result.headSha ?? gate.headSha)
409
+ };
410
+ if (normalized.status === "completed" && normalized.conclusion !== "success") {
411
+ workflowError(operation, "github_workflow_failed", workflowGateFailureMessage(gate, normalized), {
412
+ details: { gate, workflow: normalized }
413
+ });
414
+ }
415
+ if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
416
+ cacheWorkflowGateResult(options.root, options.runId, normalized);
417
+ }
418
+ results.push(normalized);
419
+ }
420
+ return results;
421
+ }
176
422
  function ensureTreeseedCommandReadiness(root) {
177
423
  if (getGitHubAutomationMode() === "stub") {
178
424
  return {
@@ -295,7 +541,8 @@ function createRepoReport(name, path, branch, dirty) {
295
541
  tagName: null,
296
542
  commitSha: branch ? headCommit(path) : null,
297
543
  skippedReason: null,
298
- publishWait: null
544
+ publishWait: null,
545
+ workflowGates: []
299
546
  };
300
547
  }
301
548
  function createWorkspaceRootRepoReport(root) {
@@ -712,6 +959,10 @@ function findAutoResumableSaveRun(root, branch) {
712
959
  if (!branch) return null;
713
960
  return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
714
961
  }
962
+ function findAutoResumableTaskRun(root, command, branch) {
963
+ if (!branch) return null;
964
+ return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
965
+ }
715
966
  function stringRecord(value) {
716
967
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
717
968
  }
@@ -1033,7 +1284,7 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
1033
1284
  if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1034
1285
  return;
1035
1286
  }
1036
- createGitHubApiClient();
1287
+ assertHostedGitHubWorkflowAuthReady("release", root);
1037
1288
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1038
1289
  if (!selectedPackageNames.has(pkg.name)) continue;
1039
1290
  resolveGitHubRepositorySlug(pkg.dir);
@@ -1101,6 +1352,14 @@ function previewStateFor(tenantRoot, branchName) {
1101
1352
  target: createBranchPreviewDeployTarget(branchName)
1102
1353
  });
1103
1354
  }
1355
+ function branchPreviewInitialized(tenantRoot, branchName) {
1356
+ if (!branchName) return false;
1357
+ try {
1358
+ return previewStateFor(tenantRoot, branchName).readiness?.initialized === true;
1359
+ } catch {
1360
+ return false;
1361
+ }
1362
+ }
1104
1363
  async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
1105
1364
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
1106
1365
  assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
@@ -1710,7 +1969,10 @@ async function workflowExport(helpers, input = {}) {
1710
1969
  return await withContextEnv(helpers.context.env, async () => {
1711
1970
  const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
1712
1971
  const exported = await exportTreeseedCodebase({ directory });
1713
- return buildWorkflowResult("export", exported.tenantRoot, exported);
1972
+ return buildWorkflowResult("export", exported.tenantRoot, {
1973
+ ...exported,
1974
+ ...worktreePayload(exported.tenantRoot, input.worktreeMode)
1975
+ });
1714
1976
  });
1715
1977
  }
1716
1978
  async function workflowSwitch(helpers, input) {
@@ -1725,6 +1987,27 @@ async function workflowSwitch(helpers, input) {
1725
1987
  }
1726
1988
  const preview = input.preview === true;
1727
1989
  const executionMode = normalizeExecutionMode(input);
1990
+ if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
1991
+ const managed = ensureManagedWorkflowWorktree({
1992
+ root,
1993
+ branchName,
1994
+ mode: input.worktreeMode,
1995
+ env: helpers.context.env
1996
+ });
1997
+ const result = await workflowSwitch(helpersForCwd(helpers, managed.worktreePath), {
1998
+ ...input,
1999
+ worktreeMode: "off"
2000
+ });
2001
+ return {
2002
+ ...result,
2003
+ payload: {
2004
+ ...result.payload,
2005
+ worktreeMode: input.worktreeMode ?? "auto",
2006
+ worktreePath: managed.worktreePath,
2007
+ managedWorktree: managed
2008
+ }
2009
+ };
2010
+ }
1728
2011
  const mode = session.mode;
1729
2012
  const repoDir = session.gitRoot;
1730
2013
  const rootRepo = createWorkspaceRootRepoReport(root);
@@ -1747,6 +2030,8 @@ async function workflowSwitch(helpers, input) {
1747
2030
  rootRepo,
1748
2031
  repos: packageReports,
1749
2032
  previewRequested: preview,
2033
+ worktreeMode: input.worktreeMode ?? "auto",
2034
+ worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
1750
2035
  blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
1751
2036
  plannedSteps: [
1752
2037
  { id: "switch-root", description: `Switch market repo to ${branchName}` },
@@ -1772,7 +2057,7 @@ async function workflowSwitch(helpers, input) {
1772
2057
  const workflowRun = acquireWorkflowRun(
1773
2058
  "switch",
1774
2059
  session,
1775
- { branch: branchName, preview },
2060
+ { branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
1776
2061
  [
1777
2062
  { id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
1778
2063
  ...packageReports.map((report) => ({
@@ -1849,6 +2134,7 @@ async function workflowSwitch(helpers, input) {
1849
2134
  },
1850
2135
  previewResult,
1851
2136
  workspaceLinks,
2137
+ ...worktreePayload(root, input.worktreeMode),
1852
2138
  preconditions: {
1853
2139
  cleanWorktreeRequired: true,
1854
2140
  baseBranch: STAGING_BRANCH
@@ -1975,6 +2261,7 @@ async function workflowSave(helpers, input) {
1975
2261
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
1976
2262
  const message = String(effectiveInput.message ?? "").trim();
1977
2263
  const optionsHotfix = effectiveInput.hotfix === true;
2264
+ const previewInitialized = branchPreviewInitialized(root, branch);
1978
2265
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
1979
2266
  if (!branch) {
1980
2267
  workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
@@ -2002,7 +2289,7 @@ async function workflowSave(helpers, input) {
2002
2289
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2003
2290
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2004
2291
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2005
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2292
+ verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2006
2293
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
2007
2294
  });
2008
2295
  const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
@@ -2024,6 +2311,9 @@ async function workflowSave(helpers, input) {
2024
2311
  failure: planAutoResumeRun.failure
2025
2312
  } : null,
2026
2313
  workspaceLinks,
2314
+ ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2315
+ verifyMode: effectiveInput.verifyMode ?? "fast",
2316
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2027
2317
  repositoryPlan,
2028
2318
  waves: repositoryPlan.waves,
2029
2319
  plannedVersions: repositoryPlan.plannedVersions,
@@ -2032,7 +2322,7 @@ async function workflowSave(helpers, input) {
2032
2322
  ...repositoryPlan.plannedSteps,
2033
2323
  { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2034
2324
  { id: "workspace-link", description: "Restore local workspace links after save" },
2035
- ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2325
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
2036
2326
  ]
2037
2327
  },
2038
2328
  {
@@ -2065,7 +2355,9 @@ async function workflowSave(helpers, input) {
2065
2355
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2066
2356
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2067
2357
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2068
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2358
+ verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
2359
+ ciMode: effectiveInput.ciMode ?? "auto",
2360
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2069
2361
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2070
2362
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2071
2363
  },
@@ -2078,7 +2370,15 @@ async function workflowSave(helpers, input) {
2078
2370
  branch,
2079
2371
  resumable: true
2080
2372
  },
2081
- ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && beforeState.preview.enabled) ? [{
2373
+ ...shouldUseHostedSaveCi(effectiveInput) ? [{
2374
+ id: "hosted-ci",
2375
+ description: `Wait for hosted save workflows on ${branch}`,
2376
+ repoName: rootRepo.name,
2377
+ repoPath: rootRepo.path,
2378
+ branch,
2379
+ resumable: true
2380
+ }] : [],
2381
+ ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && previewInitialized) ? [{
2082
2382
  id: "preview",
2083
2383
  description: `Refresh preview ${branch}`,
2084
2384
  repoName: rootRepo.name,
@@ -2112,7 +2412,7 @@ async function workflowSave(helpers, input) {
2112
2412
  devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
2113
2413
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2114
2414
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2115
- verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "action-first"),
2415
+ verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2116
2416
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2117
2417
  workflowRunId: workflowRun.runId,
2118
2418
  onProgress: (line, stream) => helpers.write(line, stream)
@@ -2139,14 +2439,30 @@ async function workflowSave(helpers, input) {
2139
2439
  lockfileValidation: repo.lockfileValidation
2140
2440
  }))
2141
2441
  };
2442
+ const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates("save", [
2443
+ ...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
2444
+ name: savedRootRepo.name,
2445
+ repoPath: savedRootRepo.path,
2446
+ workflow: "verify.yml",
2447
+ branch,
2448
+ headSha: savedRootRepo.commitSha
2449
+ }] : [],
2450
+ ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
2451
+ name: repo.name,
2452
+ repoPath: repo.path,
2453
+ workflow: "verify.yml",
2454
+ branch: String(repo.branch),
2455
+ headSha: String(repo.commitSha)
2456
+ }))
2457
+ ], "hosted", { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates }))) : { workflowGates: [] };
2142
2458
  let previewAction = { status: "skipped" };
2143
2459
  if (beforeState.branchRole === "feature" && branch) {
2144
2460
  if (effectiveInput.preview === true) {
2145
2461
  previewAction = {
2146
- status: beforeState.preview.enabled ? "refreshed" : "created",
2147
- details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
2462
+ status: previewInitialized ? "refreshed" : "created",
2463
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
2148
2464
  };
2149
- } else if (effectiveInput.refreshPreview !== false && beforeState.preview.enabled) {
2465
+ } else if (effectiveInput.refreshPreview !== false && previewInitialized) {
2150
2466
  previewAction = {
2151
2467
  status: "refreshed",
2152
2468
  details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
@@ -2175,7 +2491,11 @@ async function workflowSave(helpers, input) {
2175
2491
  mergeConflict: null,
2176
2492
  workspaceLinks,
2177
2493
  commandReadiness,
2178
- lockfileValidation
2494
+ lockfileValidation,
2495
+ ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2496
+ verifyMode: effectiveInput.verifyMode ?? "fast",
2497
+ workflowGates: saveWorkflowGates?.workflowGates ?? [],
2498
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2179
2499
  };
2180
2500
  completeWorkflowRun(root, workflowRun.runId, payload);
2181
2501
  return buildWorkflowResult(
@@ -2232,9 +2552,13 @@ async function workflowClose(helpers, input) {
2232
2552
  return await withContextEnv(helpers.context.env, async () => {
2233
2553
  const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
2234
2554
  const root = workspaceRoot(tenantRoot);
2235
- const message = ensureMessage("close", input.message, "a close reason");
2236
2555
  const executionMode = normalizeExecutionMode(input);
2237
2556
  const session = resolveTreeseedWorkflowSession(root);
2557
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2558
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
2559
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
2560
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2561
+ const message = ensureMessage("close", effectiveInput.message, "a close reason");
2238
2562
  if (executionMode === "plan") {
2239
2563
  const branchName = session.branchName;
2240
2564
  const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
@@ -2245,18 +2569,26 @@ async function workflowClose(helpers, input) {
2245
2569
  mode: session.mode,
2246
2570
  branchName,
2247
2571
  message,
2572
+ autoResumeCandidate: planAutoResumeRun ? {
2573
+ runId: planAutoResumeRun.runId,
2574
+ branch: planAutoResumeRun.session.branchName,
2575
+ failure: planAutoResumeRun.failure
2576
+ } : null,
2577
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2248
2578
  autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
2249
2579
  repos: createWorkspacePackageReports(root),
2250
2580
  rootRepo: createWorkspaceRootRepoReport(root),
2251
2581
  blockers,
2252
2582
  plannedSteps: [
2583
+ { id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
2253
2584
  { id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
2254
2585
  { id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
2255
2586
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
2256
2587
  id: `cleanup-${pkg.name}`,
2257
2588
  description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
2258
2589
  })),
2259
- { id: "workspace-link", description: "Restore local workspace links on the final branch" }
2590
+ { id: "workspace-link", description: "Restore local workspace links on the final branch" },
2591
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
2260
2592
  ]
2261
2593
  },
2262
2594
  {
@@ -2267,12 +2599,10 @@ async function workflowClose(helpers, input) {
2267
2599
  }
2268
2600
  );
2269
2601
  }
2270
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2271
2602
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
2272
2603
  message,
2273
- autoSave: input.autoSave
2604
+ autoSave: effectiveInput.autoSave
2274
2605
  });
2275
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2276
2606
  const activeSession = resolveTreeseedWorkflowSession(root);
2277
2607
  const featureBranch = assertFeatureBranch(root);
2278
2608
  const mode = activeSession.mode;
@@ -2288,8 +2618,14 @@ async function workflowClose(helpers, input) {
2288
2618
  const workflowRun = acquireWorkflowRun(
2289
2619
  "close",
2290
2620
  activeSession,
2291
- { message, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
2621
+ {
2622
+ message,
2623
+ deletePreview: effectiveInput.deletePreview !== false,
2624
+ deleteBranch: effectiveInput.deleteBranch !== false,
2625
+ worktreeMode: effectiveInput.worktreeMode ?? "auto"
2626
+ },
2292
2627
  [
2628
+ { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2293
2629
  { id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2294
2630
  { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2295
2631
  ...packageReports.map((report) => ({
@@ -2299,23 +2635,35 @@ async function workflowClose(helpers, input) {
2299
2635
  repoPath: report.path,
2300
2636
  branch: featureBranch,
2301
2637
  resumable: true
2302
- }))
2638
+ })),
2639
+ { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2640
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
2303
2641
  ],
2304
- helpers.context
2642
+ autoResumeRun ? {
2643
+ ...helpers.context,
2644
+ workflow: {
2645
+ ...helpers.context.workflow ?? {},
2646
+ resumeRunId: autoResumeRun.runId
2647
+ }
2648
+ } : helpers.context
2305
2649
  );
2650
+ if (autoResumeRun) {
2651
+ helpers.write(`[workflow][resume] Resuming interrupted close ${autoResumeRun.runId} on ${featureBranch}.`);
2652
+ }
2306
2653
  try {
2307
- 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));
2654
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2655
+ 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));
2308
2656
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
2309
2657
  const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
2310
- const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2658
+ const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2311
2659
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2312
- if (input.deleteBranch !== false) {
2660
+ if (effectiveInput.deleteBranch !== false) {
2313
2661
  deleteLocalBranch(repoDir, featureBranch);
2314
2662
  }
2315
2663
  return {
2316
2664
  deprecatedTag,
2317
2665
  deletedRemote,
2318
- deletedLocal: input.deleteBranch !== false,
2666
+ deletedLocal: effectiveInput.deleteBranch !== false,
2319
2667
  branch: currentBranch(repoDir) || STAGING_BRANCH,
2320
2668
  dirty: hasMeaningfulChanges(repoDir)
2321
2669
  };
@@ -2332,12 +2680,15 @@ async function workflowClose(helpers, input) {
2332
2680
  continue;
2333
2681
  }
2334
2682
  const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
2335
- deleteBranch: input.deleteBranch !== false,
2683
+ deleteBranch: effectiveInput.deleteBranch !== false,
2336
2684
  targetBranch: STAGING_BRANCH
2337
2685
  }));
2338
2686
  Object.assign(report, cleanup);
2339
2687
  }
2340
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2688
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
2689
+ const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
2690
+ const managedWorktree = managedWorkflowWorktreeMetadata(root);
2691
+ const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
2341
2692
  const payload = {
2342
2693
  mode,
2343
2694
  branchName: featureBranch,
@@ -2350,8 +2701,13 @@ async function workflowClose(helpers, input) {
2350
2701
  previewCleanup,
2351
2702
  remoteDeleted: rootRepo.deletedRemote,
2352
2703
  localDeleted: rootRepo.deletedLocal,
2353
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2354
- workspaceLinks
2704
+ finalBranch,
2705
+ workspaceLinks,
2706
+ worktreeCleanup,
2707
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2708
+ managedWorktree,
2709
+ worktreePath: managedWorktree?.worktreePath ?? null,
2710
+ primaryRoot: managedWorktree?.primaryRoot ?? null
2355
2711
  };
2356
2712
  completeWorkflowRun(root, workflowRun.runId, payload);
2357
2713
  return buildWorkflowResult(
@@ -2366,7 +2722,7 @@ async function workflowClose(helpers, input) {
2366
2722
  }
2367
2723
  );
2368
2724
  } catch (error) {
2369
- ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2725
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2370
2726
  failWorkflowRun(root, workflowRun.runId, error, {
2371
2727
  resumable: true,
2372
2728
  runId: workflowRun.runId,
@@ -2387,11 +2743,19 @@ async function workflowStage(helpers, input) {
2387
2743
  return await withContextEnv(helpers.context.env, async () => {
2388
2744
  const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
2389
2745
  const root = workspaceRoot(tenantRoot);
2390
- const message = ensureMessage("stage", input.message, "a resolution message");
2391
2746
  const executionMode = normalizeExecutionMode(input);
2392
2747
  const initialSession = resolveTreeseedWorkflowSession(root);
2748
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
2749
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
2750
+ const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
2751
+ const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2752
+ const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
2753
+ const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
2393
2754
  if (executionMode === "plan") {
2394
2755
  const blockers = [];
2756
+ if (initialSession.branchRole !== "feature") {
2757
+ blockers.push("Stage only applies to task branches.");
2758
+ }
2395
2759
  try {
2396
2760
  validateStagingWorkflowContracts(root);
2397
2761
  } catch (error) {
@@ -2406,6 +2770,13 @@ async function workflowStage(helpers, input) {
2406
2770
  mergeTarget: STAGING_BRANCH,
2407
2771
  mergeStrategy: "squash",
2408
2772
  message,
2773
+ ciMode,
2774
+ autoResumeCandidate: planAutoResumeRun ? {
2775
+ runId: planAutoResumeRun.runId,
2776
+ branch: planAutoResumeRun.session.branchName,
2777
+ failure: planAutoResumeRun.failure
2778
+ } : null,
2779
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2409
2780
  autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
2410
2781
  blockers,
2411
2782
  rootRepo: createWorkspaceRootRepoReport(root),
@@ -2418,7 +2789,7 @@ async function workflowStage(helpers, input) {
2418
2789
  { id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
2419
2790
  { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
2420
2791
  { id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
2421
- { id: "wait-staging", description: "Wait for staging automation" },
2792
+ { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
2422
2793
  { id: "preview-cleanup", description: "Destroy preview resources" },
2423
2794
  { id: "cleanup-root", description: "Archive and delete the task branch from market" },
2424
2795
  ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
@@ -2436,12 +2807,10 @@ async function workflowStage(helpers, input) {
2436
2807
  }
2437
2808
  );
2438
2809
  }
2439
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2440
2810
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
2441
2811
  message,
2442
- autoSave: input.autoSave
2812
+ autoSave: effectiveInput.autoSave
2443
2813
  });
2444
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2445
2814
  const session = resolveTreeseedWorkflowSession(root);
2446
2815
  const featureBranch = assertFeatureBranch(root);
2447
2816
  const mode = session.mode;
@@ -2451,6 +2820,8 @@ async function workflowStage(helpers, input) {
2451
2820
  } else {
2452
2821
  assertCleanWorktree(root);
2453
2822
  }
2823
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2824
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
2454
2825
  validateStagingWorkflowContracts(root);
2455
2826
  runWorkspaceSavePreflight({ cwd: root });
2456
2827
  const repoDir = session.gitRoot;
@@ -2459,8 +2830,16 @@ async function workflowStage(helpers, input) {
2459
2830
  const workflowRun = acquireWorkflowRun(
2460
2831
  "stage",
2461
2832
  session,
2462
- { message, waitForStaging: input.waitForStaging !== false, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
2833
+ {
2834
+ message,
2835
+ waitForStaging: effectiveInput.waitForStaging !== false,
2836
+ deletePreview: effectiveInput.deletePreview !== false,
2837
+ deleteBranch: effectiveInput.deleteBranch !== false,
2838
+ ciMode,
2839
+ worktreeMode: effectiveInput.worktreeMode ?? "auto"
2840
+ },
2463
2841
  [
2842
+ { id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2464
2843
  ...packageReports.map((report) => ({
2465
2844
  id: `merge-${report.name}`,
2466
2845
  description: `Merge ${featureBranch} into ${report.name} staging`,
@@ -2470,7 +2849,7 @@ async function workflowStage(helpers, input) {
2470
2849
  resumable: true
2471
2850
  })),
2472
2851
  { id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2473
- { id: "wait-staging", description: "Wait for staging automation", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2852
+ { id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2474
2853
  { id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2475
2854
  { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
2476
2855
  ...packageReports.map((report) => ({
@@ -2480,12 +2859,23 @@ async function workflowStage(helpers, input) {
2480
2859
  repoPath: report.path,
2481
2860
  branch: featureBranch,
2482
2861
  resumable: true
2483
- }))
2862
+ })),
2863
+ { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
2864
+ ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
2484
2865
  ],
2485
- helpers.context
2866
+ autoResumeRun ? {
2867
+ ...helpers.context,
2868
+ workflow: {
2869
+ ...helpers.context.workflow ?? {},
2870
+ resumeRunId: autoResumeRun.runId
2871
+ }
2872
+ } : helpers.context
2486
2873
  );
2874
+ if (autoResumeRun) {
2875
+ helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
2876
+ }
2487
2877
  try {
2488
- unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
2878
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2489
2879
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2490
2880
  const report = findReportByName(packageReports, pkg.name);
2491
2881
  if (!report) {
@@ -2517,7 +2907,11 @@ async function workflowStage(helpers, input) {
2517
2907
  try {
2518
2908
  rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
2519
2909
  assertCleanWorktree(root);
2520
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2910
+ if (isManagedWorkflowWorktree(root)) {
2911
+ checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
2912
+ } else {
2913
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2914
+ }
2521
2915
  run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
2522
2916
  if (mode === "recursive-workspace") {
2523
2917
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
@@ -2532,10 +2926,14 @@ async function workflowStage(helpers, input) {
2532
2926
  run("git", ["add", "-A"], { cwd: repoDir });
2533
2927
  run("git", ["commit", "-m", message], { cwd: repoDir });
2534
2928
  }
2535
- pushBranch(repoDir, STAGING_BRANCH);
2929
+ if (isManagedWorkflowWorktree(root)) {
2930
+ pushHeadToBranch(repoDir, STAGING_BRANCH);
2931
+ } else {
2932
+ pushBranch(repoDir, STAGING_BRANCH);
2933
+ }
2536
2934
  return {
2537
2935
  commitSha: headCommit(repoDir),
2538
- branch: currentBranch(repoDir) || STAGING_BRANCH,
2936
+ branch: STAGING_BRANCH,
2539
2937
  committed: hasMeaningfulChanges(repoDir) ? false : true,
2540
2938
  lockfileValidation: lockfileSafety.lockfileValidation,
2541
2939
  lockfileInstall: lockfileSafety.install
@@ -2553,18 +2951,47 @@ async function workflowStage(helpers, input) {
2553
2951
  exitCode: 12
2554
2952
  });
2555
2953
  }
2556
- 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));
2557
- 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));
2954
+ 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", [
2955
+ {
2956
+ name: rootRepo.name,
2957
+ repoPath: rootRepo.path,
2958
+ workflow: "verify.yml",
2959
+ branch: STAGING_BRANCH,
2960
+ headSha: rootRepo.commitSha
2961
+ },
2962
+ {
2963
+ name: rootRepo.name,
2964
+ repoPath: rootRepo.path,
2965
+ workflow: "deploy.yml",
2966
+ branch: STAGING_BRANCH,
2967
+ headSha: rootRepo.commitSha
2968
+ },
2969
+ ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
2970
+ name: report.name,
2971
+ repoPath: report.path,
2972
+ workflow: "verify.yml",
2973
+ branch: STAGING_BRANCH,
2974
+ headSha: String(report.commitSha)
2975
+ }))
2976
+ ], ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({
2977
+ status: "completed",
2978
+ workflowGates
2979
+ })));
2980
+ const stagingWait = {
2981
+ status: String(stageWorkflowGateResult?.status ?? "completed"),
2982
+ workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
2983
+ };
2984
+ 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));
2558
2985
  const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
2559
2986
  const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
2560
- const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2561
- if (input.deleteBranch !== false) {
2987
+ const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2988
+ if (effectiveInput.deleteBranch !== false) {
2562
2989
  deleteLocalBranch(repoDir, featureBranch);
2563
2990
  }
2564
2991
  return {
2565
2992
  deprecatedTag,
2566
2993
  deletedRemote,
2567
- deletedLocal: input.deleteBranch !== false,
2994
+ deletedLocal: effectiveInput.deleteBranch !== false,
2568
2995
  branch: currentBranch(repoDir) || STAGING_BRANCH
2569
2996
  };
2570
2997
  });
@@ -2579,12 +3006,15 @@ async function workflowStage(helpers, input) {
2579
3006
  continue;
2580
3007
  }
2581
3008
  const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
2582
- deleteBranch: input.deleteBranch !== false,
3009
+ deleteBranch: effectiveInput.deleteBranch !== false,
2583
3010
  targetBranch: STAGING_BRANCH
2584
3011
  }));
2585
3012
  Object.assign(report, cleanup);
2586
3013
  }
2587
- const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
3014
+ const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3015
+ const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
3016
+ const managedWorktree = managedWorkflowWorktreeMetadata(root);
3017
+ const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
2588
3018
  const payload = {
2589
3019
  mode,
2590
3020
  branchName: featureBranch,
@@ -2602,8 +3032,15 @@ async function workflowStage(helpers, input) {
2602
3032
  lockfileInstall: rootMerge?.lockfileInstall ?? null,
2603
3033
  remoteDeleted: rootRepo.deletedRemote,
2604
3034
  localDeleted: rootRepo.deletedLocal,
2605
- finalBranch: currentBranch(repoDir) || STAGING_BRANCH,
2606
- workspaceLinks
3035
+ finalBranch,
3036
+ workspaceLinks,
3037
+ ciMode,
3038
+ workflowGates: stagingWait.workflowGates,
3039
+ worktreeCleanup,
3040
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
3041
+ managedWorktree,
3042
+ worktreePath: managedWorktree?.worktreePath ?? null,
3043
+ primaryRoot: managedWorktree?.primaryRoot ?? null
2607
3044
  };
2608
3045
  completeWorkflowRun(root, workflowRun.runId, payload);
2609
3046
  return buildWorkflowResult(
@@ -2619,7 +3056,7 @@ async function workflowStage(helpers, input) {
2619
3056
  }
2620
3057
  );
2621
3058
  } catch (error) {
2622
- ensureWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
3059
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2623
3060
  failWorkflowRun(root, workflowRun.runId, error, {
2624
3061
  resumable: true,
2625
3062
  runId: workflowRun.runId,
@@ -2650,6 +3087,7 @@ async function workflowRelease(helpers, input) {
2650
3087
  const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
2651
3088
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
2652
3089
  const level = effectiveInput.bump ?? "patch";
3090
+ const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
2653
3091
  const isResume = Boolean(explicitResumeRunId || autoResumeRun);
2654
3092
  const packageSelection = session.packageSelection;
2655
3093
  const selectedPackageNames = new Set(packageSelection.selected);
@@ -2666,6 +3104,8 @@ async function workflowRelease(helpers, input) {
2666
3104
  if (executionMode === "plan") {
2667
3105
  return buildWorkflowResult("release", root, {
2668
3106
  ...plannedRelease,
3107
+ ciMode,
3108
+ ...worktreePayload(root, effectiveInput.worktreeMode),
2669
3109
  autoResumeCandidate: planAutoResumeRun ? {
2670
3110
  runId: planAutoResumeRun.runId,
2671
3111
  branch: planAutoResumeRun.session.branchName,
@@ -2691,6 +3131,8 @@ async function workflowRelease(helpers, input) {
2691
3131
  devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
2692
3132
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2693
3133
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3134
+ ciMode,
3135
+ worktreeMode: effectiveInput.worktreeMode ?? "auto",
2694
3136
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
2695
3137
  },
2696
3138
  [
@@ -2706,6 +3148,7 @@ async function workflowRelease(helpers, input) {
2706
3148
  resumable: true
2707
3149
  })),
2708
3150
  { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3151
+ { id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
2709
3152
  ...mode === "recursive-workspace" ? [{ id: "cleanup-dev-tags", description: "Clean replaced dev package tags", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : []
2710
3153
  ],
2711
3154
  autoResumeRun ? {
@@ -2732,19 +3175,22 @@ async function workflowRelease(helpers, input) {
2732
3175
  assertCleanWorktree(root);
2733
3176
  }
2734
3177
  prepareReleaseBranches(root);
3178
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2735
3179
  runWorkspaceSavePreflight({ cwd: root });
2736
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
3180
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
2737
3181
  if (mode === "root-only") {
2738
3182
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2739
3183
  setRootPackageJsonVersion(root, rootVersion);
2740
3184
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2741
3185
  commitAllIfChanged(gitRoot, `release: ${level} bump`);
2742
3186
  pushBranch(gitRoot, STAGING_BRANCH);
3187
+ const stagingCommit = headCommit(gitRoot);
2743
3188
  const released = mergeStagingIntoMain(root);
2744
3189
  const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
2745
3190
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2746
3191
  return {
2747
3192
  rootVersion,
3193
+ stagingCommit,
2748
3194
  releasedCommit: released.commitSha,
2749
3195
  tag
2750
3196
  };
@@ -2755,6 +3201,43 @@ async function workflowRelease(helpers, input) {
2755
3201
  rootRepo.branch = PRODUCTION_BRANCH;
2756
3202
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
2757
3203
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3204
+ const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3205
+ {
3206
+ name: rootRepo.name,
3207
+ repoPath: rootRepo.path,
3208
+ workflow: "verify.yml",
3209
+ branch: STAGING_BRANCH,
3210
+ headSha: String(rootRelease2?.stagingCommit ?? "")
3211
+ },
3212
+ {
3213
+ name: rootRepo.name,
3214
+ repoPath: rootRepo.path,
3215
+ workflow: "deploy.yml",
3216
+ branch: STAGING_BRANCH,
3217
+ headSha: String(rootRelease2?.stagingCommit ?? "")
3218
+ },
3219
+ {
3220
+ name: rootRepo.name,
3221
+ repoPath: rootRepo.path,
3222
+ workflow: "verify.yml",
3223
+ branch: rootVersion,
3224
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3225
+ },
3226
+ {
3227
+ name: rootRepo.name,
3228
+ repoPath: rootRepo.path,
3229
+ workflow: "verify.yml",
3230
+ branch: PRODUCTION_BRANCH,
3231
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3232
+ },
3233
+ {
3234
+ name: rootRepo.name,
3235
+ repoPath: rootRepo.path,
3236
+ workflow: "deploy.yml",
3237
+ branch: PRODUCTION_BRANCH,
3238
+ headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3239
+ }
3240
+ ].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
2758
3241
  const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
2759
3242
  const payload2 = {
2760
3243
  mode,
@@ -2775,7 +3258,10 @@ async function workflowRelease(helpers, input) {
2775
3258
  rootRepo,
2776
3259
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
2777
3260
  pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
2778
- workspaceLinks: workspaceLinks2
3261
+ workspaceLinks: workspaceLinks2,
3262
+ ciMode,
3263
+ workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
3264
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2779
3265
  };
2780
3266
  completeWorkflowRun(root, workflowRun.runId, payload2);
2781
3267
  return buildWorkflowResult("release", root, payload2, {
@@ -2843,18 +3329,38 @@ async function workflowRelease(helpers, input) {
2843
3329
  });
2844
3330
  const tagName = String(effectiveVersions.get(pkg.name));
2845
3331
  const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
2846
- const publish = await waitForGitHubWorkflowCompletion(pkg.dir, {
2847
- workflow: "publish.yml",
2848
- headSha: mergeResult.commitSha,
2849
- branch: tagName
2850
- });
3332
+ const workflowGates = await waitForWorkflowGates("release", [
3333
+ {
3334
+ name: pkg.name,
3335
+ repoPath: pkg.dir,
3336
+ workflow: "publish.yml",
3337
+ headSha: mergeResult.commitSha,
3338
+ branch: tagName
3339
+ },
3340
+ {
3341
+ name: pkg.name,
3342
+ repoPath: pkg.dir,
3343
+ workflow: "verify.yml",
3344
+ headSha: mergeResult.commitSha,
3345
+ branch: tagName
3346
+ },
3347
+ {
3348
+ name: pkg.name,
3349
+ repoPath: pkg.dir,
3350
+ workflow: "verify.yml",
3351
+ headSha: mergeResult.commitSha,
3352
+ branch: PRODUCTION_BRANCH
3353
+ }
3354
+ ], ciMode, { root, runId: workflowRun.runId });
3355
+ const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
2851
3356
  assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
2852
3357
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
2853
3358
  return {
2854
3359
  commitSha: mergeResult.commitSha,
2855
3360
  tagName,
2856
3361
  tag,
2857
- publish
3362
+ publish,
3363
+ workflowGates
2858
3364
  };
2859
3365
  });
2860
3366
  report.committed = true;
@@ -2863,6 +3369,7 @@ async function workflowRelease(helpers, input) {
2863
3369
  report.tagName = String(releasedPackage?.tagName ?? "");
2864
3370
  report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
2865
3371
  report.publishWait = releasedPackage?.publish ?? null;
3372
+ report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
2866
3373
  report.branch = STAGING_BRANCH;
2867
3374
  publishWait.push({
2868
3375
  name: report.name,
@@ -2875,12 +3382,22 @@ async function workflowRelease(helpers, input) {
2875
3382
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2876
3383
  commitAllIfChanged(gitRoot, `release: ${level} bump`);
2877
3384
  pushBranch(gitRoot, STAGING_BRANCH);
2878
- const released = mergeBranchIntoTarget(root, {
2879
- sourceBranch: STAGING_BRANCH,
2880
- targetBranch: PRODUCTION_BRANCH,
2881
- message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
2882
- pushTarget: false
2883
- });
3385
+ const stagingCommit = headCommit(gitRoot);
3386
+ let released;
3387
+ try {
3388
+ released = mergeBranchIntoTarget(root, {
3389
+ sourceBranch: STAGING_BRANCH,
3390
+ targetBranch: PRODUCTION_BRANCH,
3391
+ message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3392
+ pushTarget: false
3393
+ });
3394
+ } catch (error) {
3395
+ if (!resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames)) {
3396
+ throw error;
3397
+ }
3398
+ commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
3399
+ released = { commitSha: headCommit(gitRoot) };
3400
+ }
2884
3401
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2885
3402
  if (effectiveSelectedPackageNames.has(pkg.name)) {
2886
3403
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
@@ -2894,6 +3411,7 @@ async function workflowRelease(helpers, input) {
2894
3411
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2895
3412
  return {
2896
3413
  rootVersion,
3414
+ stagingCommit,
2897
3415
  releasedCommit,
2898
3416
  mergeCommit: released.commitSha,
2899
3417
  tag
@@ -2905,6 +3423,43 @@ async function workflowRelease(helpers, input) {
2905
3423
  rootRepo.branch = PRODUCTION_BRANCH;
2906
3424
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
2907
3425
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
3426
+ const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3427
+ {
3428
+ name: rootRepo.name,
3429
+ repoPath: rootRepo.path,
3430
+ workflow: "verify.yml",
3431
+ branch: STAGING_BRANCH,
3432
+ headSha: String(rootRelease?.stagingCommit ?? "")
3433
+ },
3434
+ {
3435
+ name: rootRepo.name,
3436
+ repoPath: rootRepo.path,
3437
+ workflow: "deploy.yml",
3438
+ branch: STAGING_BRANCH,
3439
+ headSha: String(rootRelease?.stagingCommit ?? "")
3440
+ },
3441
+ {
3442
+ name: rootRepo.name,
3443
+ repoPath: rootRepo.path,
3444
+ workflow: "verify.yml",
3445
+ branch: rootVersion,
3446
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3447
+ },
3448
+ {
3449
+ name: rootRepo.name,
3450
+ repoPath: rootRepo.path,
3451
+ workflow: "verify.yml",
3452
+ branch: PRODUCTION_BRANCH,
3453
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3454
+ },
3455
+ {
3456
+ name: rootRepo.name,
3457
+ repoPath: rootRepo.path,
3458
+ workflow: "deploy.yml",
3459
+ branch: PRODUCTION_BRANCH,
3460
+ headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3461
+ }
3462
+ ].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
2908
3463
  const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
2909
3464
  const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
2910
3465
  const activeDevTags = collectActiveDevTagReferences(root);
@@ -2956,7 +3511,13 @@ async function workflowRelease(helpers, input) {
2956
3511
  productionPushed: true,
2957
3512
  tagPushed: true
2958
3513
  },
2959
- workspaceLinks
3514
+ workspaceLinks,
3515
+ ciMode,
3516
+ workflowGates: [
3517
+ ...packageReports.flatMap((report) => report.workflowGates),
3518
+ ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
3519
+ ],
3520
+ ...worktreePayload(root, effectiveInput.worktreeMode)
2960
3521
  };
2961
3522
  completeWorkflowRun(root, workflowRun.runId, payload);
2962
3523
  return buildWorkflowResult("release", root, payload, {
@@ -3006,7 +3567,21 @@ async function workflowResume(helpers, input) {
3006
3567
  details: { runId, status: journal.status }
3007
3568
  });
3008
3569
  }
3009
- const resumedHelpers = {
3570
+ const session = resolveTreeseedWorkflowSession(root);
3571
+ const currentHeads = Object.fromEntries(
3572
+ [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
3573
+ );
3574
+ const classification = classifyWorkflowRunJournal(journal, {
3575
+ currentBranch: session.branchName,
3576
+ currentHeads
3577
+ });
3578
+ if (classification.state !== "resumable") {
3579
+ workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
3580
+ details: { runId, status: journal.status, classification }
3581
+ });
3582
+ }
3583
+ const resumeRoot = typeof journal.session?.root === "string" && existsSync(journal.session.root) ? journal.session.root : root;
3584
+ const resumedHelpers = helpersForCwd({
3010
3585
  ...helpers,
3011
3586
  context: {
3012
3587
  ...helpers.context,
@@ -3015,7 +3590,7 @@ async function workflowResume(helpers, input) {
3015
3590
  resumeRunId: runId
3016
3591
  }
3017
3592
  }
3018
- };
3593
+ }, resumeRoot);
3019
3594
  switch (journal.command) {
3020
3595
  case "switch":
3021
3596
  return workflowSwitch(resumedHelpers, journal.input);
@@ -3045,7 +3620,15 @@ async function workflowRecover(helpers, input = {}) {
3045
3620
  const root = resolveProjectRootOrThrow("recover", helpers.cwd());
3046
3621
  const lock = inspectWorkflowLock(root);
3047
3622
  const journals = listWorkflowRunJournals(root);
3048
- const interruptedRuns = listInterruptedWorkflowRuns(root).map((journal) => ({
3623
+ const session = resolveTreeseedWorkflowSession(root);
3624
+ const currentHeads = Object.fromEntries(
3625
+ [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
3626
+ );
3627
+ const classifiedRuns = classifyWorkflowRunJournals(root, {
3628
+ currentBranch: session.branchName,
3629
+ currentHeads
3630
+ });
3631
+ const interruptedRuns = classifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
3049
3632
  runId: journal.runId,
3050
3633
  command: journal.command,
3051
3634
  status: journal.status,
@@ -3055,6 +3638,29 @@ async function workflowRecover(helpers, input = {}) {
3055
3638
  failure: journal.failure,
3056
3639
  resumeCommand: `treeseed resume ${journal.runId}`
3057
3640
  }));
3641
+ const staleRuns = classifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
3642
+ runId: journal.runId,
3643
+ command: journal.command,
3644
+ status: journal.status,
3645
+ createdAt: journal.createdAt,
3646
+ updatedAt: journal.updatedAt,
3647
+ nextStep: nextPendingJournalStep(journal)?.description ?? null,
3648
+ failure: journal.failure,
3649
+ classification
3650
+ }));
3651
+ const obsoleteRuns = classifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
3652
+ runId: journal.runId,
3653
+ command: journal.command,
3654
+ status: journal.status,
3655
+ createdAt: journal.createdAt,
3656
+ updatedAt: journal.updatedAt,
3657
+ failure: journal.failure,
3658
+ classification
3659
+ }));
3660
+ const prunedRuns = input.pruneStale === true ? staleRuns.map((run2) => {
3661
+ archiveWorkflowRun(root, run2.runId, run2.classification);
3662
+ return run2;
3663
+ }) : [];
3058
3664
  const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
3059
3665
  return buildWorkflowResult(
3060
3666
  "recover",
@@ -3062,13 +3668,16 @@ async function workflowRecover(helpers, input = {}) {
3062
3668
  {
3063
3669
  lock,
3064
3670
  interruptedRuns,
3671
+ staleRuns,
3672
+ obsoleteRuns,
3673
+ prunedRuns,
3065
3674
  selectedRun,
3066
3675
  runCount: journals.length
3067
3676
  },
3068
3677
  {
3069
3678
  includeFinalState: false,
3070
3679
  nextSteps: createNextSteps([
3071
- ...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." }]
3680
+ ...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." }]
3072
3681
  ])
3073
3682
  }
3074
3683
  );