@treeseed/sdk 0.4.12 → 0.5.0

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 (98) hide show
  1. package/dist/control-plane-client.d.ts +60 -1
  2. package/dist/control-plane-client.js +59 -0
  3. package/dist/control-plane.d.ts +1 -1
  4. package/dist/control-plane.js +11 -4
  5. package/dist/d1-store.d.ts +58 -0
  6. package/dist/d1-store.js +64 -0
  7. package/dist/dispatch.js +6 -0
  8. package/dist/graph/schema.js +4 -0
  9. package/dist/index.d.ts +5 -1
  10. package/dist/index.js +32 -0
  11. package/dist/knowledge-coop.d.ts +223 -0
  12. package/dist/knowledge-coop.js +82 -0
  13. package/dist/model-registry.js +79 -0
  14. package/dist/operations/providers/default.js +128 -7
  15. package/dist/operations/services/config-runtime.d.ts +102 -24
  16. package/dist/operations/services/config-runtime.js +896 -160
  17. package/dist/operations/services/deploy.d.ts +223 -15
  18. package/dist/operations/services/deploy.js +626 -55
  19. package/dist/operations/services/git-workflow.d.ts +47 -3
  20. package/dist/operations/services/git-workflow.js +125 -19
  21. package/dist/operations/services/github-automation.d.ts +85 -0
  22. package/dist/operations/services/github-automation.js +220 -1
  23. package/dist/operations/services/key-agent.d.ts +118 -0
  24. package/dist/operations/services/key-agent.js +476 -0
  25. package/dist/operations/services/knowledge-coop-launch.d.ts +90 -0
  26. package/dist/operations/services/knowledge-coop-launch.js +753 -0
  27. package/dist/operations/services/knowledge-coop-packaging.d.ts +59 -0
  28. package/dist/operations/services/knowledge-coop-packaging.js +234 -0
  29. package/dist/operations/services/local-dev.d.ts +0 -1
  30. package/dist/operations/services/local-dev.js +1 -14
  31. package/dist/operations/services/project-platform.d.ts +42 -182
  32. package/dist/operations/services/project-platform.js +162 -59
  33. package/dist/operations/services/railway-deploy.d.ts +1 -0
  34. package/dist/operations/services/railway-deploy.js +31 -13
  35. package/dist/operations/services/runtime-tools.d.ts +52 -5
  36. package/dist/operations/services/runtime-tools.js +186 -26
  37. package/dist/operations/services/watch-dev.js +2 -4
  38. package/dist/operations/services/workspace-preflight.d.ts +4 -4
  39. package/dist/operations/services/workspace-preflight.js +22 -20
  40. package/dist/operations/services/workspace-save.d.ts +10 -1
  41. package/dist/operations/services/workspace-save.js +54 -3
  42. package/dist/operations/services/workspace-tools.d.ts +1 -0
  43. package/dist/operations/services/workspace-tools.js +20 -5
  44. package/dist/operations-registry.js +15 -8
  45. package/dist/operations-types.d.ts +2 -2
  46. package/dist/platform/contracts.d.ts +39 -3
  47. package/dist/platform/deploy-config.d.ts +12 -1
  48. package/dist/platform/deploy-config.js +214 -15
  49. package/dist/platform/deploy-runtime.d.ts +1 -0
  50. package/dist/platform/deploy-runtime.js +10 -2
  51. package/dist/platform/env.yaml +93 -61
  52. package/dist/platform/environment.d.ts +13 -2
  53. package/dist/platform/environment.js +90 -20
  54. package/dist/platform/plugins/constants.d.ts +1 -0
  55. package/dist/platform/plugins/constants.js +7 -6
  56. package/dist/platform/tenant/runtime-config.js +8 -1
  57. package/dist/platform/tenant-config.js +4 -0
  58. package/dist/platform/utils/site-config-schema.js +18 -0
  59. package/dist/plugin-default.js +2 -2
  60. package/dist/scripts/key-agent.js +165 -0
  61. package/dist/scripts/tenant-build.js +4 -1
  62. package/dist/scripts/tenant-check.js +4 -1
  63. package/dist/scripts/tenant-deploy.js +43 -4
  64. package/dist/scripts/tenant-dev.js +0 -1
  65. package/dist/scripts/workspace-start-warning.js +2 -2
  66. package/dist/sdk-types.d.ts +2 -2
  67. package/dist/sdk-types.js +2 -0
  68. package/dist/sdk.d.ts +13 -0
  69. package/dist/sdk.js +40 -0
  70. package/dist/stores/knowledge-coop-store.d.ts +56 -0
  71. package/dist/stores/knowledge-coop-store.js +482 -0
  72. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +6 -2
  73. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +4 -0
  74. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +25 -0
  75. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +22 -0
  76. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +11 -0
  77. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +17 -0
  78. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +17 -10
  79. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +69 -7
  80. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +1 -0
  81. package/dist/workflow/operations.d.ts +592 -243
  82. package/dist/workflow/operations.js +1908 -219
  83. package/dist/workflow/runs.d.ts +90 -0
  84. package/dist/workflow/runs.js +242 -0
  85. package/dist/workflow/session.d.ts +31 -0
  86. package/dist/workflow/session.js +97 -0
  87. package/dist/workflow-state.d.ts +88 -2
  88. package/dist/workflow-state.js +288 -26
  89. package/dist/workflow-support.d.ts +1 -1
  90. package/dist/workflow-support.js +32 -2
  91. package/dist/workflow.d.ts +93 -3
  92. package/dist/workflow.js +12 -0
  93. package/package.json +1 -1
  94. package/templates/github/deploy.workflow.yml +11 -1
  95. package/dist/scripts/sync-dev-vars.js +0 -6
  96. package/dist/scripts/workspace-close.js +0 -24
  97. package/dist/scripts/workspace-release.js +0 -42
  98. package/dist/scripts/workspace-start.js +0 -71
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import {
@@ -9,12 +9,20 @@ import {
9
9
  checkTreeseedProviderConnections,
10
10
  collectTreeseedConfigContext,
11
11
  collectTreeseedPrintEnvReport,
12
+ ensureTreeseedSecretSessionForConfig,
12
13
  ensureTreeseedActVerificationTooling,
13
14
  ensureTreeseedGitignoreEntries,
14
15
  finalizeTreeseedConfig,
15
16
  getTreeseedMachineConfigPaths,
16
- rotateTreeseedMachineKey
17
+ inspectTreeseedKeyAgentStatus,
18
+ loadTreeseedMachineConfig,
19
+ resolveTreeseedLaunchEnvironment,
20
+ resolveTreeseedRemoteSession,
21
+ rotateTreeseedMachineKey,
22
+ setTreeseedRemoteSession,
23
+ writeTreeseedMachineConfig
17
24
  } from "../operations/services/config-runtime.js";
25
+ import { ControlPlaneClient } from "../control-plane-client.js";
18
26
  import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
19
27
  import {
20
28
  assertDeploymentInitialized,
@@ -33,32 +41,36 @@ import {
33
41
  } from "../operations/services/deploy.js";
34
42
  import {
35
43
  assertCleanWorktree,
44
+ assertCleanWorktrees,
36
45
  assertFeatureBranch,
37
46
  branchExists,
38
47
  checkoutBranch,
48
+ checkoutTaskBranchFromStaging,
39
49
  createDeprecatedTaskTag,
40
- createFeatureBranchFromStaging,
41
- currentManagedBranch,
42
50
  deleteLocalBranch,
43
51
  deleteRemoteBranch,
44
52
  ensureLocalBranchTracking,
45
53
  gitWorkflowRoot,
54
+ headCommit,
46
55
  listTaskBranches,
47
- mergeCurrentBranchIntoStaging,
56
+ mergeBranchIntoTarget,
48
57
  mergeStagingIntoMain,
49
58
  prepareReleaseBranches,
50
59
  PRODUCTION_BRANCH,
51
60
  pushBranch,
52
61
  remoteBranchExists,
53
62
  STAGING_BRANCH,
63
+ squashMergeBranchIntoStaging,
54
64
  syncBranchWithOrigin,
55
65
  waitForStagingAutomation
56
66
  } from "../operations/services/git-workflow.js";
67
+ import { waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
57
68
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
58
69
  import { runTenantDeployPreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
59
70
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
60
71
  import {
61
72
  applyWorkspaceVersionChanges,
73
+ assertWorkspaceVersionConsistency,
62
74
  collectMergeConflictReport,
63
75
  currentBranch,
64
76
  formatMergeConflictReport,
@@ -69,8 +81,30 @@ import {
69
81
  planWorkspaceReleaseBump,
70
82
  repoRoot
71
83
  } from "../operations/services/workspace-save.js";
72
- import { run, workspaceRoot } from "../operations/services/workspace-tools.js";
84
+ import {
85
+ changedWorkspacePackages,
86
+ publishableWorkspacePackages,
87
+ run,
88
+ sortWorkspacePackages,
89
+ workspaceRoot
90
+ } from "../operations/services/workspace-tools.js";
73
91
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
92
+ import {
93
+ acquireWorkflowLock,
94
+ createWorkflowRunJournal,
95
+ generateWorkflowRunId,
96
+ inspectWorkflowLock,
97
+ listInterruptedWorkflowRuns,
98
+ listWorkflowRunJournals,
99
+ readWorkflowRunJournal,
100
+ refreshWorkflowLock,
101
+ releaseWorkflowLock,
102
+ updateWorkflowRunJournal
103
+ } from "./runs.js";
104
+ import {
105
+ checkedOutWorkspacePackageRepos,
106
+ resolveTreeseedWorkflowSession
107
+ } from "./session.js";
74
108
  import {
75
109
  classifyTreeseedBranchRole,
76
110
  resolveTreeseedWorkflowPaths
@@ -167,17 +201,81 @@ function resolveRepoState(repoDir) {
167
201
  dirtyWorktree: gitStatusPorcelain(repoDir).length > 0
168
202
  };
169
203
  }
170
- function buildWorkflowResult(operation, cwd, payload, nextSteps) {
204
+ function createRepoReport(name, path, branch, dirty) {
205
+ return {
206
+ name,
207
+ path,
208
+ branch,
209
+ dirty,
210
+ created: false,
211
+ resumed: false,
212
+ merged: false,
213
+ verified: false,
214
+ committed: false,
215
+ pushed: false,
216
+ deletedLocal: false,
217
+ deletedRemote: false,
218
+ tagName: null,
219
+ commitSha: branch ? headCommit(path) : null,
220
+ skippedReason: null,
221
+ publishWait: null
222
+ };
223
+ }
224
+ function createWorkspaceRootRepoReport(root) {
225
+ const gitRoot = repoRoot(root);
226
+ return createRepoReport("@treeseed/market", gitRoot, currentBranch(gitRoot) || null, hasMeaningfulChanges(gitRoot));
227
+ }
228
+ function createWorkspacePackageReports(root) {
229
+ return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
230
+ }
231
+ function findReportByName(reports, name) {
232
+ return reports.find((report) => report.name === name) ?? null;
233
+ }
234
+ function findReportByPath(reports, path) {
235
+ return reports.find((report) => report.path === path) ?? null;
236
+ }
237
+ function assertWorkspaceClean(root) {
238
+ const repoDirs = [repoRoot(root), ...checkedOutWorkspacePackageRepos(root).map((pkg) => pkg.dir)];
239
+ assertCleanWorktrees(repoDirs);
240
+ return repoDirs;
241
+ }
242
+ function buildWorkflowResult(operation, cwd, payload, options = {}) {
243
+ const resolvedPayload = options.includeFinalState ?? true ? {
244
+ ...payload,
245
+ finalState: resolveWorkflowStateSnapshot(cwd)
246
+ } : payload;
171
247
  return {
248
+ schemaVersion: 1,
249
+ kind: "treeseed.workflow.result",
250
+ command: operation,
251
+ executionMode: options.executionMode ?? "execute",
252
+ runId: options.runId ?? null,
172
253
  ok: true,
173
254
  operation,
174
- payload: {
175
- ...payload,
176
- finalState: resolveWorkflowStateSnapshot(cwd)
177
- },
178
- nextSteps
255
+ summary: options.summary,
256
+ facts: options.facts,
257
+ payload: resolvedPayload,
258
+ result: resolvedPayload,
259
+ nextSteps: options.nextSteps,
260
+ recovery: options.recovery ?? null,
261
+ errors: options.errors ?? []
179
262
  };
180
263
  }
264
+ function normalizeExecutionMode(input) {
265
+ return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
266
+ }
267
+ function submodulePointerForRef(repoDir, ref, relativeDir) {
268
+ try {
269
+ const output = run("git", ["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
270
+ if (!output) {
271
+ return null;
272
+ }
273
+ const match = output.match(/^[0-9]{6}\s+commit\s+([0-9a-f]{40})\t/u);
274
+ return match?.[1] ?? null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
181
279
  function ensureLocalReadinessOrThrow(operation, tenantRoot) {
182
280
  const state = resolveWorkflowStateSnapshot(tenantRoot);
183
281
  if (!state.readiness.local.ready) {
@@ -207,12 +305,10 @@ function createNextSteps(steps) {
207
305
  }
208
306
  function createStatusResult(cwd) {
209
307
  const state = resolveTreeseedWorkflowState(cwd);
210
- return {
211
- ok: true,
212
- operation: "status",
213
- payload: state,
214
- nextSteps: createNextSteps(state.recommendations)
215
- };
308
+ return buildWorkflowResult("status", cwd, state, {
309
+ nextSteps: createNextSteps(state.recommendations),
310
+ includeFinalState: false
311
+ });
216
312
  }
217
313
  function createTasksResult(cwd) {
218
314
  const tenantRoot = cwd;
@@ -223,6 +319,21 @@ function createTasksResult(cwd) {
223
319
  const previewState = loadDeployState(tenantRoot, deployConfig, {
224
320
  target: createBranchPreviewDeployTarget(branch.name)
225
321
  });
322
+ const packages = checkedOutWorkspacePackageRepos(tenantRoot).map((pkg) => {
323
+ const packageBranches = listTaskBranches(pkg.dir);
324
+ const match = packageBranches.find((candidate) => candidate.name === branch.name) ?? null;
325
+ const pointer = submodulePointerForRef(repoDir, branch.name, pkg.relativeDir);
326
+ return {
327
+ name: pkg.name,
328
+ path: pkg.relativeDir,
329
+ local: match?.local === true,
330
+ remote: match?.remote === true,
331
+ current: match?.current === true,
332
+ head: match?.head ?? null,
333
+ pointer,
334
+ aligned: pointer != null && match?.head != null ? pointer === match.head : match != null
335
+ };
336
+ });
226
337
  return {
227
338
  ...branch,
228
339
  ageDays: ageDays(branch.lastCommitDate),
@@ -231,10 +342,179 @@ function createTasksResult(cwd) {
231
342
  enabled: previewState.previewEnabled === true || previewState.readiness?.initialized === true,
232
343
  url: previewState.lastDeployedUrl ?? null,
233
344
  lastDeploymentTimestamp: previewState.lastDeploymentTimestamp ?? null
234
- }
345
+ },
346
+ packages
235
347
  };
236
348
  });
237
- return { ok: true, operation: "tasks", payload: { tasks } };
349
+ const workstreams = tasks.map((task) => ({
350
+ id: task.name,
351
+ title: task.name.replace(/^task\//u, "").replace(/[-_]+/gu, " "),
352
+ linkedDirectRefs: [],
353
+ branch: task.name,
354
+ local: task.local,
355
+ remote: task.remote,
356
+ current: task.current,
357
+ previewUrl: task.preview.url,
358
+ lastSaveAt: task.lastCommitDate ?? null,
359
+ verificationResult: task.dirtyCurrent ? "needs_attention" : task.head ? "ready" : "unknown",
360
+ stagingCandidate: task.name === STAGING_BRANCH,
361
+ archived: false
362
+ }));
363
+ return buildWorkflowResult("tasks", cwd, { tasks, workstreams }, { includeFinalState: false });
364
+ }
365
+ function normalizeOptionalString(value) {
366
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
367
+ }
368
+ async function connectTreeseedMarketProject(helpers, tenantRoot, input, context) {
369
+ const machineConfig = loadTreeseedMachineConfig(tenantRoot);
370
+ const marketSettings = machineConfig.settings?.market && typeof machineConfig.settings.market === "object" ? machineConfig.settings.market : {};
371
+ const remoteSettings = machineConfig.settings?.remote && typeof machineConfig.settings.remote === "object" ? machineConfig.settings.remote : { activeHostId: "official", executionMode: "prefer-local", hosts: [] };
372
+ const baseUrl = normalizeOptionalString(input.marketBaseUrl) ?? normalizeOptionalString(marketSettings.baseUrl) ?? normalizeOptionalString(remoteSettings.hosts?.find?.((entry) => entry?.official === true)?.baseUrl) ?? normalizeOptionalString(remoteSettings.hosts?.find?.((entry) => entry?.id === remoteSettings.activeHostId)?.baseUrl);
373
+ if (!baseUrl) {
374
+ workflowError(
375
+ "config",
376
+ "validation_failed",
377
+ "Treeseed config --connect-market requires a market base URL. Pass --market-base-url or configure an authenticated remote host first."
378
+ );
379
+ }
380
+ const hostId = normalizeOptionalString(marketSettings.hostId) ?? "knowledge-coop";
381
+ const activeRemoteSession = resolveTreeseedRemoteSession(tenantRoot, hostId) ?? resolveTreeseedRemoteSession(tenantRoot, remoteSettings.activeHostId) ?? resolveTreeseedRemoteSession(tenantRoot, "official");
382
+ const accessToken = normalizeOptionalString(input.marketAccessToken) ?? normalizeOptionalString(activeRemoteSession?.accessToken);
383
+ if (!accessToken) {
384
+ workflowError(
385
+ "config",
386
+ "validation_failed",
387
+ "Treeseed config --connect-market requires a market access token. Authenticate to the Knowledge Coop control-plane first or pass --market-access-token."
388
+ );
389
+ }
390
+ const projectId = normalizeOptionalString(input.marketProjectId) ?? normalizeOptionalString(marketSettings.projectId);
391
+ if (!projectId) {
392
+ workflowError(
393
+ "config",
394
+ "validation_failed",
395
+ "Treeseed config --connect-market requires --market-project-id or an existing settings.market.projectId value."
396
+ );
397
+ }
398
+ const teamId = normalizeOptionalString(input.marketTeamId) ?? normalizeOptionalString(marketSettings.teamId);
399
+ const projectSlug = normalizeOptionalString(input.marketProjectSlug) ?? normalizeOptionalString(marketSettings.projectSlug) ?? normalizeOptionalString(machineConfig.project?.slug) ?? projectId;
400
+ const teamSlug = normalizeOptionalString(input.marketTeamSlug) ?? normalizeOptionalString(marketSettings.teamSlug);
401
+ const projectApiBaseUrl = normalizeOptionalString(input.marketProjectApiBaseUrl) ?? normalizeOptionalString(marketSettings.projectApiBaseUrl);
402
+ const client = new ControlPlaneClient({
403
+ baseUrl,
404
+ accessToken
405
+ });
406
+ const connectionResult = await client.upsertProjectConnection(projectId, {
407
+ mode: "hybrid",
408
+ projectApiBaseUrl,
409
+ executionOwner: "project_runner",
410
+ metadata: {
411
+ pairingSource: "treeseed_config_connect_market",
412
+ tenantRoot,
413
+ tenantSlug: normalizeOptionalString(machineConfig.project?.slug),
414
+ repoSlug: normalizeOptionalString(machineConfig.project?.slug),
415
+ teamId,
416
+ teamSlug,
417
+ projectSlug,
418
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
419
+ },
420
+ rotateRunnerToken: input.rotateRunnerToken === true
421
+ });
422
+ const hosts = Array.isArray(remoteSettings.hosts) ? [...remoteSettings.hosts] : [];
423
+ const updatedHost = {
424
+ id: hostId,
425
+ label: "Knowledge Coop",
426
+ baseUrl,
427
+ official: false
428
+ };
429
+ const existingHostIndex = hosts.findIndex(
430
+ (entry) => String(entry?.id ?? "") === hostId || String(entry?.baseUrl ?? "").replace(/\/+$/u, "") === baseUrl.replace(/\/+$/u, "")
431
+ );
432
+ if (existingHostIndex >= 0) {
433
+ hosts.splice(existingHostIndex, 1, {
434
+ ...hosts[existingHostIndex],
435
+ ...updatedHost
436
+ });
437
+ } else {
438
+ hosts.unshift(updatedHost);
439
+ }
440
+ if (normalizeOptionalString(input.marketAccessToken)) {
441
+ setTreeseedRemoteSession(tenantRoot, {
442
+ hostId,
443
+ accessToken,
444
+ refreshToken: activeRemoteSession?.refreshToken ?? "",
445
+ expiresAt: activeRemoteSession?.expiresAt ?? "",
446
+ principal: activeRemoteSession?.principal ?? null
447
+ });
448
+ }
449
+ const runnerHostId = `market-runner:${projectId}`;
450
+ if (connectionResult.runnerToken) {
451
+ setTreeseedRemoteSession(tenantRoot, {
452
+ hostId: runnerHostId,
453
+ accessToken: connectionResult.runnerToken,
454
+ refreshToken: "",
455
+ expiresAt: "",
456
+ principal: {
457
+ id: `runner:${projectId}`,
458
+ displayName: "Knowledge Coop Project Runner",
459
+ scopes: [],
460
+ roles: ["project_runner"],
461
+ permissions: [],
462
+ metadata: { projectId }
463
+ }
464
+ });
465
+ }
466
+ machineConfig.settings.remote = {
467
+ ...remoteSettings,
468
+ activeHostId: hostId,
469
+ hosts
470
+ };
471
+ machineConfig.settings.market = {
472
+ baseUrl,
473
+ hostId,
474
+ teamId,
475
+ teamSlug,
476
+ projectId,
477
+ projectSlug,
478
+ projectApiBaseUrl: connectionResult.connection?.projectApiBaseUrl ?? projectApiBaseUrl ?? null,
479
+ connectionMode: connectionResult.connection?.mode ?? "hybrid",
480
+ executionOwner: connectionResult.connection?.executionOwner ?? "project_runner",
481
+ runnerHostId,
482
+ runnerReady: Boolean(connectionResult.runnerToken || resolveTreeseedRemoteSession(tenantRoot, runnerHostId)?.accessToken),
483
+ runnerRegisteredAt: connectionResult.connection?.runnerRegisteredAt ?? null,
484
+ runnerLastSeenAt: connectionResult.connection?.runnerLastSeenAt ?? null,
485
+ launchPhase: null,
486
+ lastSuccessfulPhase: null,
487
+ githubRepository: null,
488
+ workflowBootstrapReady: false,
489
+ approvalBlockers: [],
490
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
491
+ };
492
+ writeTreeseedMachineConfig(tenantRoot, machineConfig);
493
+ const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
494
+ return buildWorkflowResult(
495
+ "config",
496
+ tenantRoot,
497
+ {
498
+ mode: "connect-market",
499
+ scopes: context.scopes,
500
+ sync: context.sync,
501
+ configPath,
502
+ keyPath,
503
+ repairs: context.repairs,
504
+ preflight: context.preflight,
505
+ toolHealth: context.toolHealth,
506
+ market: machineConfig.settings.market,
507
+ connection: connectionResult.connection,
508
+ runnerTokenIssued: Boolean(connectionResult.runnerToken)
509
+ },
510
+ {
511
+ summary: "Knowledge Coop project pairing completed.",
512
+ nextSteps: createNextSteps([
513
+ { operation: "status", reason: "Confirm the new market connection, runner health, and current workstream posture." },
514
+ { operation: "tasks", reason: "Inspect the branch-backed workstreams that will now sync into the Knowledge Coop UI." }
515
+ ])
516
+ }
517
+ );
238
518
  }
239
519
  function maybePrint(write, line, stream = "stdout") {
240
520
  if (!line) return;
@@ -259,6 +539,210 @@ function toError(operation, error) {
259
539
  }
260
540
  throw new TreeseedWorkflowError(operation, "unsupported_state", String(error));
261
541
  }
542
+ function workflowSessionSnapshot(session) {
543
+ return {
544
+ root: session.root,
545
+ mode: session.mode,
546
+ branchName: session.branchName,
547
+ repos: [session.rootRepo, ...session.packageRepos].map((repo) => ({
548
+ name: repo.name,
549
+ path: repo.path,
550
+ branchName: repo.branchName
551
+ }))
552
+ };
553
+ }
554
+ function nextPendingJournalStep(journal) {
555
+ return journal.steps.find((step) => step.status === "pending") ?? null;
556
+ }
557
+ async function executeJournalStep(root, runId, stepId, action) {
558
+ const current = readWorkflowRunJournal(root, runId);
559
+ const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
560
+ if (!current || !step) {
561
+ throw new Error(`Unknown workflow step "${stepId}" for run ${runId}.`);
562
+ }
563
+ if (step.status === "completed") {
564
+ return step.data ?? null;
565
+ }
566
+ const data = await Promise.resolve(action());
567
+ updateWorkflowRunJournal(root, runId, (journal) => ({
568
+ ...journal,
569
+ steps: journal.steps.map((entry) => entry.id === stepId ? {
570
+ ...entry,
571
+ status: "completed",
572
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
573
+ data: data ?? null
574
+ } : entry)
575
+ }));
576
+ refreshWorkflowLock(root, runId);
577
+ return data;
578
+ }
579
+ function skipJournalStep(root, runId, stepId, data = null) {
580
+ updateWorkflowRunJournal(root, runId, (journal) => ({
581
+ ...journal,
582
+ steps: journal.steps.map((entry) => entry.id === stepId ? {
583
+ ...entry,
584
+ status: "skipped",
585
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
586
+ data
587
+ } : entry)
588
+ }));
589
+ refreshWorkflowLock(root, runId);
590
+ }
591
+ function acquireWorkflowRun(operation, session, input, steps, context) {
592
+ const resumeRunId = context.workflow?.resumeRunId;
593
+ if (resumeRunId) {
594
+ const existing = readWorkflowRunJournal(session.root, resumeRunId);
595
+ if (!existing || existing.command !== operation) {
596
+ workflowError(operation, "resume_unavailable", `Treeseed ${operation} cannot resume run ${resumeRunId}.`, {
597
+ details: { runId: resumeRunId, command: operation }
598
+ });
599
+ }
600
+ const lockResult2 = acquireWorkflowLock(session.root, operation, resumeRunId);
601
+ if (!lockResult2.acquired) {
602
+ workflowError(operation, "workflow_locked", `Treeseed ${operation} is blocked by active run ${lockResult2.lock.runId}.`, {
603
+ details: {
604
+ lock: lockResult2.lock,
605
+ recovery: {
606
+ resumable: true,
607
+ runId: lockResult2.lock.runId,
608
+ command: lockResult2.lock.command,
609
+ recoverCommand: "treeseed recover",
610
+ resumeCommand: `treeseed resume ${lockResult2.lock.runId}`
611
+ }
612
+ }
613
+ });
614
+ }
615
+ return {
616
+ runId: resumeRunId,
617
+ session,
618
+ journal: existing,
619
+ resumed: true
620
+ };
621
+ }
622
+ const runId = generateWorkflowRunId(operation);
623
+ const lockResult = acquireWorkflowLock(session.root, operation, runId);
624
+ if (!lockResult.acquired) {
625
+ workflowError(operation, "workflow_locked", `Treeseed ${operation} is blocked by active run ${lockResult.lock.runId}.`, {
626
+ details: {
627
+ lock: lockResult.lock,
628
+ recovery: {
629
+ resumable: true,
630
+ runId: lockResult.lock.runId,
631
+ command: lockResult.lock.command,
632
+ recoverCommand: "treeseed recover",
633
+ resumeCommand: `treeseed resume ${lockResult.lock.runId}`
634
+ }
635
+ }
636
+ });
637
+ }
638
+ const journal = createWorkflowRunJournal(session.root, {
639
+ runId,
640
+ command: operation,
641
+ input,
642
+ session: workflowSessionSnapshot(session),
643
+ steps
644
+ });
645
+ return {
646
+ runId,
647
+ session,
648
+ journal,
649
+ resumed: false
650
+ };
651
+ }
652
+ function completeWorkflowRun(root, runId, result) {
653
+ updateWorkflowRunJournal(root, runId, (journal) => ({
654
+ ...journal,
655
+ status: "completed",
656
+ result,
657
+ failure: null
658
+ }));
659
+ releaseWorkflowLock(root, runId);
660
+ }
661
+ function failWorkflowRun(root, runId, error, recovery) {
662
+ const message = error instanceof Error ? error.message : String(error);
663
+ const code = error instanceof TreeseedWorkflowError ? error.code : "unsupported_state";
664
+ const details = error instanceof TreeseedWorkflowError ? {
665
+ ...error.details ?? {},
666
+ recovery: recovery ?? error.details?.recovery ?? null
667
+ } : recovery ? { recovery } : null;
668
+ updateWorkflowRunJournal(root, runId, (journal) => ({
669
+ ...journal,
670
+ status: "failed",
671
+ failure: {
672
+ code,
673
+ message,
674
+ details,
675
+ at: (/* @__PURE__ */ new Date()).toISOString()
676
+ }
677
+ }));
678
+ releaseWorkflowLock(root, runId);
679
+ }
680
+ function validatePackageReleaseWorkflows(root, packageNames) {
681
+ if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
682
+ return;
683
+ }
684
+ const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => !existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))).map((pkg) => pkg.name);
685
+ if (missing.length > 0) {
686
+ workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
687
+ details: {
688
+ missing
689
+ }
690
+ });
691
+ }
692
+ }
693
+ function validateStagingWorkflowContracts(root) {
694
+ if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
695
+ return;
696
+ }
697
+ const missing = [];
698
+ for (const fileName of ["verify.yml", "deploy.yml"]) {
699
+ if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
700
+ missing.push(fileName);
701
+ }
702
+ }
703
+ if (missing.length > 0) {
704
+ workflowError("stage", "workflow_contract_missing", `Treeseed stage requires standardized root workflows: ${missing.join(", ")}.`, {
705
+ details: { missing }
706
+ });
707
+ }
708
+ }
709
+ function assertSessionBranchSafety(operation, session, {
710
+ requireCleanPackages = false,
711
+ requireCurrentBranch = false,
712
+ allowPackageReposWithoutOrigin = false
713
+ } = {}) {
714
+ const detached = session.packageRepos.filter((repo) => repo.detached).map((repo) => repo.name);
715
+ if (detached.length > 0) {
716
+ workflowError(operation, "validation_failed", `Detached package heads detected: ${detached.join(", ")}.`, {
717
+ details: { detached }
718
+ });
719
+ }
720
+ if (requireCleanPackages) {
721
+ const dirty = session.packageRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
722
+ if (dirty.length > 0) {
723
+ workflowError(operation, "validation_failed", `Dirty package repos block ${operation}: ${dirty.join(", ")}.`, {
724
+ details: { dirty }
725
+ });
726
+ }
727
+ }
728
+ if (requireCurrentBranch && session.branchName) {
729
+ const missing = session.packageRepos.filter((repo) => repo.branchName !== session.branchName).map((repo) => ({ name: repo.name, branchName: repo.branchName }));
730
+ if (missing.length > 0) {
731
+ workflowError(operation, "validation_failed", `Package branch alignment is required for ${operation}.`, {
732
+ details: { expectedBranch: session.branchName, repos: missing }
733
+ });
734
+ }
735
+ }
736
+ const missingOriginRepos = [
737
+ session.rootRepo,
738
+ ...allowPackageReposWithoutOrigin ? [] : session.packageRepos
739
+ ].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
740
+ if (missingOriginRepos.length > 0 && operation !== "destroy") {
741
+ workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
742
+ details: { missingOrigin: missingOriginRepos }
743
+ });
744
+ }
745
+ }
262
746
  function previewStateFor(tenantRoot, branchName) {
263
747
  const deployConfig = loadCliDeployConfig(tenantRoot);
264
748
  return loadDeployState(tenantRoot, deployConfig, {
@@ -365,9 +849,11 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
365
849
  }
366
850
  async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
367
851
  const tenantRoot = resolveProjectRootOrThrow(operation, helpers.cwd());
368
- const repoDir = gitWorkflowRoot(tenantRoot);
852
+ const root = workspaceRoot(tenantRoot);
853
+ const repoDir = gitWorkflowRoot(root);
369
854
  const before = resolveRepoState(repoDir);
370
- if (!before.dirtyWorktree) {
855
+ const packageDirty = checkedOutWorkspacePackageRepos(root).some((pkg) => hasMeaningfulChanges(pkg.dir));
856
+ if (!before.dirtyWorktree && !packageDirty) {
371
857
  return { performed: false, save: null };
372
858
  }
373
859
  if (input.autoSave === false) {
@@ -384,6 +870,142 @@ async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
384
870
  save: saveResult.payload
385
871
  };
386
872
  }
873
+ function checkoutOrCreateSaveBranch(repoDir, branch) {
874
+ const current = currentBranch(repoDir);
875
+ if (current === branch) {
876
+ return current;
877
+ }
878
+ if (branchExists(repoDir, branch)) {
879
+ checkoutBranch(repoDir, branch);
880
+ return branch;
881
+ }
882
+ if (remoteBranchExists(repoDir, branch)) {
883
+ run("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
884
+ return branch;
885
+ }
886
+ run("git", ["checkout", "-b", branch], { cwd: repoDir });
887
+ return branch;
888
+ }
889
+ function runPackageVerifyLocal(pkgDir) {
890
+ run("npm", ["run", "verify:local"], { cwd: pkgDir });
891
+ }
892
+ function branchNeedsSync(repoDir, branch) {
893
+ if (!remoteBranchExists(repoDir, branch)) {
894
+ return true;
895
+ }
896
+ const localHead = run("git", ["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
897
+ const remoteHead = run("git", ["rev-parse", `origin/${branch}`], { cwd: repoDir, capture: true }).trim();
898
+ return localHead !== remoteHead;
899
+ }
900
+ function savePackageRepo(report, message, branch, shouldVerify) {
901
+ checkoutOrCreateSaveBranch(report.path, branch);
902
+ report.branch = currentBranch(report.path);
903
+ report.dirty = hasMeaningfulChanges(report.path);
904
+ const needsSync = branchNeedsSync(report.path, branch);
905
+ if (!report.dirty && !needsSync) {
906
+ report.skippedReason = "clean";
907
+ report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
908
+ return report;
909
+ }
910
+ if (shouldVerify && report.dirty) {
911
+ runPackageVerifyLocal(report.path);
912
+ report.verified = true;
913
+ }
914
+ if (report.dirty) {
915
+ run("git", ["add", "-A"], { cwd: report.path });
916
+ run("git", ["commit", "-m", message], { cwd: report.path });
917
+ report.committed = true;
918
+ }
919
+ report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
920
+ const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
921
+ report.pushed = branchSync.pushed === true;
922
+ if (!report.dirty && needsSync) {
923
+ report.skippedReason = "sync-only";
924
+ }
925
+ return report;
926
+ }
927
+ function createSaveFailure(message, repos, rootRepo, failingRepo, error) {
928
+ const rendered = error instanceof Error ? error.message : String(error);
929
+ const code = error instanceof TreeseedWorkflowError ? error.code : "unsupported_state";
930
+ const exitCode = error instanceof TreeseedWorkflowError ? error.exitCode : void 0;
931
+ throw new TreeseedWorkflowError("save", code, `${message}
932
+ ${rendered}`, {
933
+ details: {
934
+ partialFailure: {
935
+ message,
936
+ failingRepo: failingRepo?.name ?? null,
937
+ repos,
938
+ rootRepo,
939
+ error: rendered
940
+ }
941
+ },
942
+ exitCode
943
+ });
944
+ }
945
+ function ensureLocalTaskBranch(repoDir, branchName) {
946
+ if (!branchExists(repoDir, branchName) && !remoteBranchExists(repoDir, branchName)) {
947
+ return false;
948
+ }
949
+ if (!branchExists(repoDir, branchName) && remoteBranchExists(repoDir, branchName)) {
950
+ ensureLocalBranchTracking(repoDir, branchName);
951
+ }
952
+ if (currentBranch(repoDir) !== branchName) {
953
+ checkoutBranch(repoDir, branchName);
954
+ }
955
+ return true;
956
+ }
957
+ function cleanupTaskBranchReport(report, branchName, message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
958
+ if (!ensureLocalTaskBranch(report.path, branchName)) {
959
+ report.skippedReason = "branch-missing";
960
+ return report;
961
+ }
962
+ const deprecatedTag = createDeprecatedTaskTag(report.path, branchName, message);
963
+ report.tagName = deprecatedTag.tagName;
964
+ report.commitSha = deprecatedTag.head;
965
+ report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
966
+ syncBranchWithOrigin(report.path, targetBranch);
967
+ if (deleteBranch) {
968
+ deleteLocalBranch(report.path, branchName);
969
+ report.deletedLocal = true;
970
+ }
971
+ report.branch = currentBranch(report.path) || targetBranch;
972
+ report.dirty = hasMeaningfulChanges(report.path);
973
+ return report;
974
+ }
975
+ function syncAllCheckedOutPackageRepos(root, branchName) {
976
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
977
+ syncBranchWithOrigin(pkg.dir, branchName);
978
+ }
979
+ }
980
+ function collectReleasePackageSelection(root) {
981
+ const publishable = sortWorkspacePackages(
982
+ publishableWorkspacePackages(root).filter((pkg) => pkg.name?.startsWith("@treeseed/"))
983
+ );
984
+ const changed = changedWorkspacePackages({
985
+ root,
986
+ baseRef: PRODUCTION_BRANCH,
987
+ includeDependents: false,
988
+ packages: publishable
989
+ });
990
+ const selected = changedWorkspacePackages({
991
+ root,
992
+ baseRef: PRODUCTION_BRANCH,
993
+ includeDependents: true,
994
+ packages: publishable
995
+ });
996
+ const changedNames = changed.map((pkg) => pkg.name);
997
+ const selectedNames = selected.map((pkg) => pkg.name);
998
+ const dependents = selected.filter((pkg) => !changedNames.includes(pkg.name)).map((pkg) => pkg.name);
999
+ return {
1000
+ changed: changedNames,
1001
+ dependents,
1002
+ selected: selectedNames,
1003
+ publishable
1004
+ };
1005
+ }
1006
+ function hasStagedChanges(repoDir) {
1007
+ return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
1008
+ }
387
1009
  async function workflowStatus(helpers) {
388
1010
  return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
389
1011
  }
@@ -400,13 +1022,27 @@ async function workflowConfig(helpers, input = {}) {
400
1022
  const revealSecrets = input.showSecrets === true;
401
1023
  const printEnvOnly = input.printEnvOnly === true;
402
1024
  const rotateMachineKeyFlag = input.rotateMachineKey === true;
1025
+ const connectMarketFlag = input.connectMarket === true;
1026
+ const nonInteractive = input.nonInteractive === true;
403
1027
  const repairs = input.repair === false ? [] : resolveTreeseedWorkflowState(tenantRoot).deployConfigPresent ? applyTreeseedSafeRepairs(tenantRoot) : [];
404
1028
  const toolHealth = ensureTreeseedActVerificationTooling({
405
1029
  tenantRoot,
406
- installIfMissing: true,
1030
+ installIfMissing: input.installMissingTooling === true,
407
1031
  env: helpers.context.env,
408
1032
  write: (line) => maybePrint(helpers.write, line)
409
1033
  });
1034
+ const secretSession = printEnvOnly && !revealSecrets ? {
1035
+ status: inspectTreeseedKeyAgentStatus(tenantRoot),
1036
+ createdWrappedKey: false,
1037
+ migratedWrappedKey: false,
1038
+ unlockSource: "existing-session"
1039
+ } : await ensureTreeseedSecretSessionForConfig({
1040
+ tenantRoot,
1041
+ interactive: false,
1042
+ env: helpers.context.env,
1043
+ createIfMissing: true,
1044
+ allowMigration: true
1045
+ });
410
1046
  ensureTreeseedGitignoreEntries(tenantRoot);
411
1047
  const preflight = collectCliPreflight({ cwd: tenantRoot, requireAuth: false });
412
1048
  const contextSnapshot = collectTreeseedConfigContext({
@@ -425,10 +1061,10 @@ async function workflowConfig(helpers, input = {}) {
425
1061
  }),
426
1062
  provider: checkTreeseedProviderConnections({ tenantRoot, scope, env: helpers.context.env })
427
1063
  }));
428
- return {
429
- ok: true,
430
- operation: "config",
431
- payload: {
1064
+ return buildWorkflowResult(
1065
+ "config",
1066
+ tenantRoot,
1067
+ {
432
1068
  mode: "print-env-only",
433
1069
  scopes,
434
1070
  sync,
@@ -436,31 +1072,48 @@ async function workflowConfig(helpers, input = {}) {
436
1072
  reports: reports2,
437
1073
  repairs,
438
1074
  preflight,
439
- toolHealth
1075
+ toolHealth,
1076
+ context: contextSnapshot,
1077
+ secretSession
440
1078
  },
441
- nextSteps: createNextSteps([
442
- { operation: "config", reason: "Initialize the selected environment after reviewing the generated values.", input: { environment: scopes } }
443
- ])
444
- };
1079
+ {
1080
+ nextSteps: createNextSteps([
1081
+ { operation: "config", reason: "Initialize the selected environment after reviewing the generated values.", input: { environment: scopes } }
1082
+ ])
1083
+ }
1084
+ );
445
1085
  }
446
1086
  if (rotateMachineKeyFlag) {
447
1087
  const result = rotateTreeseedMachineKey(tenantRoot);
448
- return {
449
- ok: true,
450
- operation: "config",
451
- payload: {
1088
+ return buildWorkflowResult(
1089
+ "config",
1090
+ tenantRoot,
1091
+ {
452
1092
  mode: "rotate-machine-key",
453
1093
  scopes,
454
1094
  sync,
455
1095
  keyPath: result.keyPath,
456
1096
  repairs,
457
1097
  preflight,
458
- toolHealth
1098
+ toolHealth,
1099
+ context: contextSnapshot,
1100
+ secretSession
459
1101
  },
460
- nextSteps: createNextSteps([
461
- { operation: "config", reason: "Inspect the regenerated local environment after the machine key rotation.", input: { environment: ["local"], printEnvOnly: true } }
462
- ])
463
- };
1102
+ {
1103
+ nextSteps: createNextSteps([
1104
+ { operation: "config", reason: "Inspect the regenerated local environment after the machine key rotation.", input: { environment: ["local"], printEnvOnly: true } }
1105
+ ])
1106
+ }
1107
+ );
1108
+ }
1109
+ if (connectMarketFlag) {
1110
+ return connectTreeseedMarketProject(helpers, tenantRoot, input, {
1111
+ scopes,
1112
+ sync,
1113
+ repairs,
1114
+ preflight,
1115
+ toolHealth
1116
+ });
464
1117
  }
465
1118
  const explicitUpdates = Array.isArray(input.updates) ? input.updates.map((update) => ({
466
1119
  scope: update.scope,
@@ -468,6 +1121,13 @@ async function workflowConfig(helpers, input = {}) {
468
1121
  value: typeof update.value === "string" ? update.value : "",
469
1122
  reused: update.reused === true
470
1123
  })) : null;
1124
+ if (!explicitUpdates && !nonInteractive) {
1125
+ workflowError(
1126
+ "config",
1127
+ "validation_failed",
1128
+ "Treeseed config requires interactive input or explicit updates. Re-run in a TTY, or use --non-interactive/--json from the CLI when you want resolved values applied automatically."
1129
+ );
1130
+ }
471
1131
  const autoUpdates = scopes.flatMap(
472
1132
  (scope) => contextSnapshot.entriesByScope[scope].map((entry) => ({
473
1133
  scope,
@@ -476,6 +1136,7 @@ async function workflowConfig(helpers, input = {}) {
476
1136
  reused: entry.currentValue.length > 0 || entry.suggestedValue.length > 0
477
1137
  }))
478
1138
  );
1139
+ maybePrint(helpers.write, "Saving resolved configuration values to machine config...");
479
1140
  const applyResult = applyTreeseedConfigValues({
480
1141
  tenantRoot,
481
1142
  updates: explicitUpdates ?? autoUpdates
@@ -484,6 +1145,12 @@ async function workflowConfig(helpers, input = {}) {
484
1145
  tenantRoot,
485
1146
  scopes,
486
1147
  sync,
1148
+ env: helpers.context.env,
1149
+ onProgress: (line) => maybePrint(helpers.write, line)
1150
+ });
1151
+ const refreshedContext = collectTreeseedConfigContext({
1152
+ tenantRoot,
1153
+ scopes,
487
1154
  env: helpers.context.env
488
1155
  });
489
1156
  const reports = printEnv ? scopes.map((scope) => ({
@@ -510,7 +1177,8 @@ async function workflowConfig(helpers, input = {}) {
510
1177
  repairs,
511
1178
  preflight,
512
1179
  toolHealth,
513
- context: contextSnapshot,
1180
+ secretSession,
1181
+ context: refreshedContext,
514
1182
  result: {
515
1183
  ...applyResult,
516
1184
  ...finalizeResult
@@ -539,46 +1207,129 @@ async function workflowExport(helpers, input = {}) {
539
1207
  }
540
1208
  async function workflowSwitch(helpers, input) {
541
1209
  try {
542
- return withContextEnv(helpers.context.env, () => {
1210
+ return await withContextEnv(helpers.context.env, async () => {
543
1211
  const tenantRoot = resolveProjectRootOrThrow("switch", helpers.cwd());
1212
+ const root = workspaceRoot(tenantRoot);
1213
+ const session = resolveTreeseedWorkflowSession(root);
544
1214
  const branchName = String(input.branch ?? input.branchName ?? "").trim();
545
1215
  if (!branchName) {
546
1216
  workflowError("switch", "validation_failed", "Treeseed switch requires a branch name.");
547
1217
  }
548
1218
  const preview = input.preview === true;
549
- const repoDir = gitWorkflowRoot(tenantRoot);
550
- const currentBranchName = currentManagedBranch(tenantRoot);
551
- let created = false;
552
- let resumed = false;
1219
+ const executionMode = normalizeExecutionMode(input);
1220
+ const mode = session.mode;
1221
+ const repoDir = session.gitRoot;
1222
+ const rootRepo = createWorkspaceRootRepoReport(root);
1223
+ const packageReports = createWorkspacePackageReports(root);
553
1224
  let previewResult = null;
554
- if (currentBranchName === branchName) {
555
- resumed = true;
556
- } else if (!branchExists(repoDir, branchName) && !remoteBranchExists(repoDir, branchName)) {
557
- if (input.createIfMissing === false) {
558
- workflowError("switch", "validation_failed", `Branch "${branchName}" does not exist locally or on origin.`);
1225
+ const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
1226
+ if (executionMode === "plan") {
1227
+ for (const report of [rootRepo, ...packageReports]) {
1228
+ const local = branchExists(report.path, branchName);
1229
+ const remote = remoteBranchExists(report.path, branchName);
1230
+ report.created = !local && !remote;
1231
+ report.resumed = local || remote;
559
1232
  }
560
- const result = createFeatureBranchFromStaging(tenantRoot, branchName);
561
- pushBranch(result.repoDir, branchName, { setUpstream: true });
562
- created = true;
563
- } else {
564
- assertCleanWorktree(tenantRoot);
565
- ensureLocalBranchTracking(repoDir, branchName);
566
- checkoutBranch(repoDir, branchName);
567
- syncBranchWithOrigin(repoDir, branchName);
568
- resumed = true;
1233
+ return buildWorkflowResult(
1234
+ "switch",
1235
+ root,
1236
+ {
1237
+ mode,
1238
+ branchName,
1239
+ rootRepo,
1240
+ repos: packageReports,
1241
+ previewRequested: preview,
1242
+ blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
1243
+ plannedSteps: [
1244
+ { id: "switch-root", description: `Switch market repo to ${branchName}` },
1245
+ ...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
1246
+ ...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
1247
+ ]
1248
+ },
1249
+ {
1250
+ executionMode,
1251
+ nextSteps: createNextSteps([
1252
+ { operation: "switch", reason: "Run without --plan to create or resume the task branch.", input: { branch: branchName, preview } }
1253
+ ])
1254
+ }
1255
+ );
569
1256
  }
570
- const stateAfterSwitch = resolveTreeseedWorkflowState(tenantRoot);
571
- if (preview && !stateAfterSwitch.preview.enabled) {
572
- previewResult = deployBranchPreview(tenantRoot, branchName, helpers.context, { initialize: true });
1257
+ if (mode === "recursive-workspace") {
1258
+ assertWorkspaceClean(root);
1259
+ assertSessionBranchSafety("switch", session);
1260
+ } else {
1261
+ assertCleanWorktree(root);
573
1262
  }
574
- const state = resolveTreeseedWorkflowState(tenantRoot);
575
- return buildWorkflowResult(
1263
+ const workflowRun = acquireWorkflowRun(
576
1264
  "switch",
577
- tenantRoot,
578
- {
1265
+ session,
1266
+ { branch: branchName, preview },
1267
+ [
1268
+ { id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
1269
+ ...packageReports.map((report) => ({
1270
+ id: `switch-${report.name}`,
1271
+ description: `Mirror ${branchName} into ${report.name}`,
1272
+ repoName: report.name,
1273
+ repoPath: report.path,
1274
+ branch: branchName,
1275
+ resumable: true
1276
+ })),
1277
+ ...preview ? [{ id: "preview", description: `Provision or refresh preview ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true }] : []
1278
+ ],
1279
+ helpers.context
1280
+ );
1281
+ try {
1282
+ const rootSwitch = await executeJournalStep(
1283
+ root,
1284
+ workflowRun.runId,
1285
+ "switch-root",
1286
+ () => checkoutTaskBranchFromStaging(repoDir, branchName, {
1287
+ createIfMissing: input.createIfMissing !== false,
1288
+ pushIfCreated: true
1289
+ })
1290
+ );
1291
+ rootRepo.branch = currentBranch(repoDir) || branchName;
1292
+ rootRepo.created = rootSwitch.created;
1293
+ rootRepo.resumed = rootSwitch.resumed;
1294
+ rootRepo.commitSha = headCommit(repoDir);
1295
+ rootRepo.pushed = rootSwitch.created;
1296
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1297
+ const report = findReportByName(packageReports, pkg.name);
1298
+ if (!report) {
1299
+ continue;
1300
+ }
1301
+ const packageSwitch = await executeJournalStep(
1302
+ root,
1303
+ workflowRun.runId,
1304
+ `switch-${report.name}`,
1305
+ () => checkoutTaskBranchFromStaging(pkg.dir, branchName, {
1306
+ createIfMissing: input.createIfMissing !== false,
1307
+ pushIfCreated: false
1308
+ })
1309
+ );
1310
+ report.branch = currentBranch(pkg.dir) || branchName;
1311
+ report.created = packageSwitch.created;
1312
+ report.resumed = packageSwitch.resumed;
1313
+ report.commitSha = headCommit(pkg.dir);
1314
+ report.dirty = hasMeaningfulChanges(pkg.dir);
1315
+ }
1316
+ const stateAfterSwitch = resolveTreeseedWorkflowState(root);
1317
+ if (preview) {
1318
+ previewResult = await executeJournalStep(
1319
+ root,
1320
+ workflowRun.runId,
1321
+ "preview",
1322
+ () => deployBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
1323
+ ) ?? null;
1324
+ }
1325
+ const state = resolveTreeseedWorkflowState(root);
1326
+ const payload = {
1327
+ mode,
579
1328
  branchName,
580
- created,
581
- resumed,
1329
+ created: rootRepo.created,
1330
+ resumed: rootRepo.resumed,
1331
+ repos: packageReports,
1332
+ rootRepo,
582
1333
  previewRequested: preview,
583
1334
  preview: {
584
1335
  enabled: state.preview.enabled,
@@ -590,12 +1341,31 @@ async function workflowSwitch(helpers, input) {
590
1341
  cleanWorktreeRequired: true,
591
1342
  baseBranch: STAGING_BRANCH
592
1343
  }
593
- },
594
- createNextSteps([
595
- state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change", preview: true } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
596
- { operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
597
- ])
598
- );
1344
+ };
1345
+ completeWorkflowRun(root, workflowRun.runId, payload);
1346
+ return buildWorkflowResult(
1347
+ "switch",
1348
+ root,
1349
+ payload,
1350
+ {
1351
+ runId: workflowRun.runId,
1352
+ nextSteps: createNextSteps([
1353
+ state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change", preview: true } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
1354
+ { operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
1355
+ ])
1356
+ }
1357
+ );
1358
+ } catch (error) {
1359
+ failWorkflowRun(root, workflowRun.runId, error, {
1360
+ resumable: true,
1361
+ runId: workflowRun.runId,
1362
+ command: "switch",
1363
+ message: `Resume the interrupted switch for ${branchName}.`,
1364
+ recoverCommand: "treeseed recover",
1365
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
1366
+ });
1367
+ throw error;
1368
+ }
599
1369
  });
600
1370
  } catch (error) {
601
1371
  toError("switch", error);
@@ -618,7 +1388,11 @@ async function workflowDev(helpers, input = {}) {
618
1388
  if (input.port !== void 0) {
619
1389
  args.push("--port", String(input.port));
620
1390
  }
621
- const env = { ...process.env, ...helpers.context.env ?? {} };
1391
+ const env = resolveTreeseedLaunchEnvironment({
1392
+ tenantRoot,
1393
+ scope: "local",
1394
+ baseEnv: { ...process.env, ...helpers.context.env ?? {} }
1395
+ });
622
1396
  if (input.background) {
623
1397
  const child = spawn(process.execPath, args, {
624
1398
  cwd: tenantRoot,
@@ -669,15 +1443,19 @@ async function workflowDev(helpers, input = {}) {
669
1443
  }
670
1444
  async function workflowSave(helpers, input) {
671
1445
  try {
672
- return withContextEnv(helpers.context.env, () => {
1446
+ return await withContextEnv(helpers.context.env, async () => {
673
1447
  const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
674
1448
  const message = ensureMessage("save", input.message, "a commit message");
675
1449
  const optionsHotfix = input.hotfix === true;
676
1450
  const root = workspaceRoot(tenantRoot);
677
- const gitRoot = repoRoot(root);
678
- const branch = currentBranch(gitRoot);
1451
+ const session = resolveTreeseedWorkflowSession(root);
1452
+ const gitRoot = session.gitRoot;
1453
+ const branch = session.branchName;
679
1454
  const scope = branch === STAGING_BRANCH ? "staging" : branch === PRODUCTION_BRANCH ? "prod" : "local";
680
1455
  const beforeState = resolveTreeseedWorkflowState(root);
1456
+ const recursiveWorkspace = session.mode === "recursive-workspace";
1457
+ const mode = session.mode;
1458
+ const executionMode = normalizeExecutionMode(input);
681
1459
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
682
1460
  if (!branch) {
683
1461
  workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
@@ -685,42 +1463,218 @@ async function workflowSave(helpers, input) {
685
1463
  if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
686
1464
  workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
687
1465
  }
1466
+ const packageReports = createWorkspacePackageReports(root);
1467
+ const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
1468
+ const blockers = [];
1469
+ if (executionMode === "plan") {
1470
+ if (!session.rootRepo.hasOriginRemote) {
1471
+ blockers.push("Market repo is missing origin remote.");
1472
+ }
1473
+ if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
1474
+ blockers.push("Main saves require --hotfix.");
1475
+ }
1476
+ if (recursiveWorkspace) {
1477
+ try {
1478
+ assertWorkspaceVersionConsistency(root);
1479
+ } catch (error) {
1480
+ blockers.push(error instanceof Error ? error.message : String(error));
1481
+ }
1482
+ }
1483
+ return buildWorkflowResult(
1484
+ "save",
1485
+ root,
1486
+ {
1487
+ mode,
1488
+ branch,
1489
+ scope,
1490
+ hotfix: optionsHotfix,
1491
+ message,
1492
+ repos: packageReports,
1493
+ rootRepo,
1494
+ blockers,
1495
+ plannedSteps: [
1496
+ ...packageReports.map((report) => ({ id: `save-${report.name}`, description: `Verify, commit, and push ${report.name}` })),
1497
+ ...input.verify !== false ? [{ id: "verify-root", description: "Run market workspace verification" }] : [],
1498
+ { id: "commit-root", description: "Commit market repo changes if present" },
1499
+ { id: "sync-root", description: `Push ${branch} to origin` },
1500
+ ...beforeState.branchRole === "feature" && (input.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
1501
+ ]
1502
+ },
1503
+ {
1504
+ executionMode,
1505
+ nextSteps: createNextSteps([
1506
+ { operation: "save", reason: "Run without --plan to persist the workspace checkpoint.", input: { message, hotfix: optionsHotfix, preview: input.preview === true } }
1507
+ ])
1508
+ }
1509
+ );
1510
+ }
1511
+ assertSessionBranchSafety("save", session, {
1512
+ allowPackageReposWithoutOrigin: true
1513
+ });
688
1514
  try {
689
1515
  originRemoteUrl(gitRoot);
690
1516
  } catch {
691
1517
  workflowError("save", "validation_failed", "Treeseed save requires an origin remote.");
692
1518
  }
693
- if (input.verify !== false) {
694
- runWorkspaceSavePreflight({ cwd: root });
695
- }
696
- const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
697
- let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
698
- let commitCreated = false;
699
- if (hadMeaningfulChanges) {
700
- run("git", ["add", "-A"], { cwd: gitRoot });
701
- run("git", ["commit", "-m", message], { cwd: gitRoot });
702
- head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
703
- commitCreated = true;
704
- }
705
- const branchSync = syncCurrentBranchToOrigin("save", gitRoot, branch);
706
- let previewAction = { status: "skipped" };
707
- if (beforeState.branchRole === "feature" && branch) {
708
- if (input.preview === true) {
709
- previewAction = {
710
- status: beforeState.preview.enabled ? "refreshed" : "created",
711
- details: deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled })
712
- };
713
- } else if (input.refreshPreview !== false && beforeState.preview.enabled) {
714
- previewAction = {
715
- status: "refreshed",
716
- details: deployBranchPreview(root, branch, helpers.context, { initialize: false })
717
- };
718
- }
719
- }
720
- return buildWorkflowResult(
1519
+ const workflowRun = acquireWorkflowRun(
721
1520
  "save",
722
- root,
1521
+ session,
723
1522
  {
1523
+ message,
1524
+ hotfix: optionsHotfix,
1525
+ preview: input.preview === true,
1526
+ refreshPreview: input.refreshPreview !== false,
1527
+ verify: input.verify !== false
1528
+ },
1529
+ [
1530
+ ...packageReports.map((report) => ({
1531
+ id: `save-${report.name}`,
1532
+ description: `Save ${report.name}`,
1533
+ repoName: report.name,
1534
+ repoPath: report.path,
1535
+ branch,
1536
+ resumable: true
1537
+ })),
1538
+ ...input.verify !== false ? [{
1539
+ id: "verify-root",
1540
+ description: "Verify market workspace",
1541
+ repoName: rootRepo.name,
1542
+ repoPath: rootRepo.path,
1543
+ branch,
1544
+ resumable: true
1545
+ }] : [],
1546
+ {
1547
+ id: "commit-root",
1548
+ description: "Commit market workspace changes",
1549
+ repoName: rootRepo.name,
1550
+ repoPath: rootRepo.path,
1551
+ branch,
1552
+ resumable: true
1553
+ },
1554
+ {
1555
+ id: "sync-root",
1556
+ description: `Push ${branch} to origin`,
1557
+ repoName: rootRepo.name,
1558
+ repoPath: rootRepo.path,
1559
+ branch,
1560
+ resumable: true
1561
+ },
1562
+ ...beforeState.branchRole === "feature" && (input.preview === true || input.refreshPreview !== false && beforeState.preview.enabled) ? [{
1563
+ id: "preview",
1564
+ description: `Refresh preview ${branch}`,
1565
+ repoName: rootRepo.name,
1566
+ repoPath: rootRepo.path,
1567
+ branch,
1568
+ resumable: true
1569
+ }] : []
1570
+ ],
1571
+ helpers.context
1572
+ );
1573
+ try {
1574
+ if (recursiveWorkspace) {
1575
+ assertWorkspaceVersionConsistency(root);
1576
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1577
+ const report = findReportByName(packageReports, pkg.name);
1578
+ if (!report) {
1579
+ continue;
1580
+ }
1581
+ try {
1582
+ const step = readWorkflowRunJournal(root, workflowRun.runId)?.steps.find((entry) => entry.id === `save-${report.name}`) ?? null;
1583
+ const resumePendingSync = workflowRun.resumed && step?.status === "pending" && branchNeedsSync(report.path, branch);
1584
+ if (!report.dirty && !resumePendingSync) {
1585
+ report.skippedReason = "clean";
1586
+ skipJournalStep(root, workflowRun.runId, `save-${report.name}`, {
1587
+ skippedReason: "clean"
1588
+ });
1589
+ continue;
1590
+ }
1591
+ const savedReport = await executeJournalStep(root, workflowRun.runId, `save-${report.name}`, () => savePackageRepo(report, message, branch, input.verify !== false));
1592
+ Object.assign(report, savedReport);
1593
+ } catch (error) {
1594
+ createSaveFailure(
1595
+ `Treeseed save stopped while saving workspace package ${pkg.name}.`,
1596
+ packageReports,
1597
+ rootRepo,
1598
+ report,
1599
+ error
1600
+ );
1601
+ }
1602
+ }
1603
+ }
1604
+ if (input.verify !== false) {
1605
+ try {
1606
+ await executeJournalStep(root, workflowRun.runId, "verify-root", () => {
1607
+ runWorkspaceSavePreflight({ cwd: root });
1608
+ rootRepo.verified = true;
1609
+ return {
1610
+ verified: true
1611
+ };
1612
+ });
1613
+ } catch (error) {
1614
+ createSaveFailure(
1615
+ "Treeseed save stopped while verifying the market workspace.",
1616
+ packageReports,
1617
+ rootRepo,
1618
+ null,
1619
+ error
1620
+ );
1621
+ }
1622
+ }
1623
+ const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
1624
+ let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
1625
+ let commitCreated = false;
1626
+ if (hadMeaningfulChanges) {
1627
+ const commitResult = await executeJournalStep(root, workflowRun.runId, "commit-root", () => {
1628
+ run("git", ["add", "-A"], { cwd: gitRoot });
1629
+ run("git", ["commit", "-m", message], { cwd: gitRoot });
1630
+ return {
1631
+ commitSha: run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim()
1632
+ };
1633
+ });
1634
+ head = String(commitResult?.commitSha ?? head);
1635
+ commitCreated = true;
1636
+ rootRepo.committed = true;
1637
+ } else {
1638
+ skipJournalStep(root, workflowRun.runId, "commit-root", {
1639
+ skippedReason: "clean"
1640
+ });
1641
+ }
1642
+ rootRepo.commitSha = head;
1643
+ let branchSync;
1644
+ try {
1645
+ branchSync = await executeJournalStep(root, workflowRun.runId, "sync-root", () => syncCurrentBranchToOrigin("save", gitRoot, branch));
1646
+ } catch (error) {
1647
+ createSaveFailure(
1648
+ "Treeseed save stopped while syncing the market repository.",
1649
+ packageReports,
1650
+ rootRepo,
1651
+ rootRepo,
1652
+ error
1653
+ );
1654
+ }
1655
+ rootRepo.pushed = branchSync.pushed === true;
1656
+ if (input.verify !== false) {
1657
+ rootRepo.verified = true;
1658
+ }
1659
+ if (!hadMeaningfulChanges) {
1660
+ rootRepo.skippedReason = "clean";
1661
+ }
1662
+ let previewAction = { status: "skipped" };
1663
+ if (beforeState.branchRole === "feature" && branch) {
1664
+ if (input.preview === true) {
1665
+ previewAction = {
1666
+ status: beforeState.preview.enabled ? "refreshed" : "created",
1667
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
1668
+ };
1669
+ } else if (input.refreshPreview !== false && beforeState.preview.enabled) {
1670
+ previewAction = {
1671
+ status: "refreshed",
1672
+ details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
1673
+ };
1674
+ }
1675
+ }
1676
+ const payload = {
1677
+ mode,
724
1678
  branch,
725
1679
  scope,
726
1680
  hotfix: optionsHotfix,
@@ -729,13 +1683,54 @@ async function workflowSave(helpers, input) {
729
1683
  commitCreated,
730
1684
  noChanges: !hadMeaningfulChanges,
731
1685
  branchSync,
1686
+ repos: packageReports,
1687
+ rootRepo,
1688
+ partialFailure: null,
732
1689
  previewAction,
733
1690
  mergeConflict: null
734
- },
735
- createNextSteps([
736
- branch === STAGING_BRANCH ? { operation: "release", reason: "Promote the validated staging branch into production.", input: { bump: "patch" } } : branch === PRODUCTION_BRANCH ? { operation: "status", reason: "Inspect production state after the explicit hotfix save." } : { operation: "stage", reason: "Merge the verified task branch into staging.", input: { message: "describe the resolution" } }
737
- ])
738
- );
1691
+ };
1692
+ completeWorkflowRun(root, workflowRun.runId, payload);
1693
+ return buildWorkflowResult(
1694
+ "save",
1695
+ root,
1696
+ payload,
1697
+ {
1698
+ runId: workflowRun.runId,
1699
+ nextSteps: createNextSteps([
1700
+ branch === STAGING_BRANCH ? { operation: "release", reason: "Promote the validated staging branch into production.", input: { bump: "patch" } } : branch === PRODUCTION_BRANCH ? { operation: "status", reason: "Inspect production state after the explicit hotfix save." } : { operation: "stage", reason: "Merge the verified task branch into staging.", input: { message: "describe the resolution" } }
1701
+ ])
1702
+ }
1703
+ );
1704
+ } catch (error) {
1705
+ const failingRepo = packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
1706
+ const wrappedError = error instanceof TreeseedWorkflowError && error.details?.partialFailure != null ? error : new TreeseedWorkflowError(
1707
+ "save",
1708
+ error instanceof TreeseedWorkflowError ? error.code : "unsupported_state",
1709
+ error instanceof Error ? error.message : String(error),
1710
+ {
1711
+ details: {
1712
+ ...error instanceof TreeseedWorkflowError ? error.details ?? {} : {},
1713
+ partialFailure: {
1714
+ message: "Treeseed save stopped before the workspace could finish syncing.",
1715
+ failingRepo: failingRepo.name,
1716
+ repos: packageReports,
1717
+ rootRepo,
1718
+ error: error instanceof Error ? error.message : String(error)
1719
+ }
1720
+ },
1721
+ exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : void 0
1722
+ }
1723
+ );
1724
+ failWorkflowRun(root, workflowRun.runId, wrappedError, {
1725
+ resumable: true,
1726
+ runId: workflowRun.runId,
1727
+ command: "save",
1728
+ message: `Resume the interrupted save on ${branch}.`,
1729
+ recoverCommand: "treeseed recover",
1730
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
1731
+ });
1732
+ throw wrappedError;
1733
+ }
739
1734
  });
740
1735
  } catch (error) {
741
1736
  toError("save", error);
@@ -743,41 +1738,148 @@ async function workflowSave(helpers, input) {
743
1738
  }
744
1739
  async function workflowClose(helpers, input) {
745
1740
  try {
746
- return withContextEnv(helpers.context.env, async () => {
1741
+ return await withContextEnv(helpers.context.env, async () => {
747
1742
  const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
1743
+ const root = workspaceRoot(tenantRoot);
748
1744
  const message = ensureMessage("close", input.message, "a close reason");
1745
+ const executionMode = normalizeExecutionMode(input);
1746
+ const session = resolveTreeseedWorkflowSession(root);
1747
+ if (executionMode === "plan") {
1748
+ const branchName = session.branchName;
1749
+ const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
1750
+ return buildWorkflowResult(
1751
+ "close",
1752
+ root,
1753
+ {
1754
+ mode: session.mode,
1755
+ branchName,
1756
+ message,
1757
+ autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
1758
+ repos: createWorkspacePackageReports(root),
1759
+ rootRepo: createWorkspaceRootRepoReport(root),
1760
+ blockers,
1761
+ plannedSteps: [
1762
+ { id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
1763
+ { id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
1764
+ ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
1765
+ id: `cleanup-${pkg.name}`,
1766
+ description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
1767
+ }))
1768
+ ]
1769
+ },
1770
+ {
1771
+ executionMode,
1772
+ nextSteps: createNextSteps([
1773
+ { operation: "close", reason: "Run without --plan to archive and delete the task branch.", input: { message } }
1774
+ ])
1775
+ }
1776
+ );
1777
+ }
749
1778
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
750
1779
  message,
751
1780
  autoSave: input.autoSave
752
1781
  });
753
- const featureBranch = assertFeatureBranch(tenantRoot);
754
- const repoDir = gitWorkflowRoot(tenantRoot);
755
- assertCleanWorktree(tenantRoot);
756
- const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
757
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
758
- const remoteDeleted = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
759
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
760
- if (input.deleteBranch !== false) {
761
- deleteLocalBranch(repoDir, featureBranch);
1782
+ const activeSession = resolveTreeseedWorkflowSession(root);
1783
+ const featureBranch = assertFeatureBranch(root);
1784
+ const mode = activeSession.mode;
1785
+ const repoDir = activeSession.gitRoot;
1786
+ assertSessionBranchSafety("close", activeSession);
1787
+ if (mode === "recursive-workspace") {
1788
+ assertWorkspaceClean(root);
1789
+ } else {
1790
+ assertCleanWorktree(root);
762
1791
  }
763
- return buildWorkflowResult(
1792
+ const rootRepo = createWorkspaceRootRepoReport(root);
1793
+ const packageReports = createWorkspacePackageReports(root);
1794
+ const workflowRun = acquireWorkflowRun(
764
1795
  "close",
765
- tenantRoot,
766
- {
1796
+ activeSession,
1797
+ { message, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
1798
+ [
1799
+ { id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
1800
+ { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
1801
+ ...packageReports.map((report) => ({
1802
+ id: `cleanup-${report.name}`,
1803
+ description: `Archive ${featureBranch} in ${report.name}`,
1804
+ repoName: report.name,
1805
+ repoPath: report.path,
1806
+ branch: featureBranch,
1807
+ resumable: true
1808
+ }))
1809
+ ],
1810
+ helpers.context
1811
+ );
1812
+ try {
1813
+ 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));
1814
+ const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
1815
+ const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
1816
+ const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
1817
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1818
+ if (input.deleteBranch !== false) {
1819
+ deleteLocalBranch(repoDir, featureBranch);
1820
+ }
1821
+ return {
1822
+ deprecatedTag,
1823
+ deletedRemote,
1824
+ deletedLocal: input.deleteBranch !== false,
1825
+ branch: currentBranch(repoDir) || STAGING_BRANCH,
1826
+ dirty: hasMeaningfulChanges(repoDir)
1827
+ };
1828
+ });
1829
+ rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? null);
1830
+ rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
1831
+ rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
1832
+ rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
1833
+ rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
1834
+ rootRepo.dirty = rootCleanup?.dirty === true;
1835
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1836
+ const report = findReportByName(packageReports, pkg.name);
1837
+ if (!report) {
1838
+ continue;
1839
+ }
1840
+ const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
1841
+ deleteBranch: input.deleteBranch !== false,
1842
+ targetBranch: STAGING_BRANCH
1843
+ }));
1844
+ Object.assign(report, cleanup);
1845
+ }
1846
+ const payload = {
1847
+ mode,
767
1848
  branchName: featureBranch,
768
1849
  message,
769
1850
  autoSaved: autoSave.performed,
770
1851
  autoSaveResult: autoSave.save,
771
- deprecatedTag,
1852
+ deprecatedTag: rootCleanup?.deprecatedTag ?? null,
1853
+ repos: packageReports,
1854
+ rootRepo,
772
1855
  previewCleanup,
773
- remoteDeleted,
774
- localDeleted: input.deleteBranch !== false,
1856
+ remoteDeleted: rootRepo.deletedRemote,
1857
+ localDeleted: rootRepo.deletedLocal,
775
1858
  finalBranch: currentBranch(repoDir) || STAGING_BRANCH
776
- },
777
- createNextSteps([
778
- { operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
779
- ])
780
- );
1859
+ };
1860
+ completeWorkflowRun(root, workflowRun.runId, payload);
1861
+ return buildWorkflowResult(
1862
+ "close",
1863
+ root,
1864
+ payload,
1865
+ {
1866
+ runId: workflowRun.runId,
1867
+ nextSteps: createNextSteps([
1868
+ { operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
1869
+ ])
1870
+ }
1871
+ );
1872
+ } catch (error) {
1873
+ failWorkflowRun(root, workflowRun.runId, error, {
1874
+ resumable: true,
1875
+ runId: workflowRun.runId,
1876
+ command: "close",
1877
+ message: `Resume the interrupted close for ${featureBranch}.`,
1878
+ recoverCommand: "treeseed recover",
1879
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
1880
+ });
1881
+ throw error;
1882
+ }
781
1883
  });
782
1884
  } catch (error) {
783
1885
  toError("close", error);
@@ -785,53 +1887,232 @@ async function workflowClose(helpers, input) {
785
1887
  }
786
1888
  async function workflowStage(helpers, input) {
787
1889
  try {
788
- return withContextEnv(helpers.context.env, async () => {
1890
+ return await withContextEnv(helpers.context.env, async () => {
789
1891
  const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
1892
+ const root = workspaceRoot(tenantRoot);
790
1893
  const message = ensureMessage("stage", input.message, "a resolution message");
1894
+ const executionMode = normalizeExecutionMode(input);
1895
+ const initialSession = resolveTreeseedWorkflowSession(root);
1896
+ if (executionMode === "plan") {
1897
+ const blockers = [];
1898
+ try {
1899
+ validateStagingWorkflowContracts(root);
1900
+ } catch (error) {
1901
+ blockers.push(error instanceof Error ? error.message : String(error));
1902
+ }
1903
+ return buildWorkflowResult(
1904
+ "stage",
1905
+ root,
1906
+ {
1907
+ mode: initialSession.mode,
1908
+ branchName: initialSession.branchName,
1909
+ mergeTarget: STAGING_BRANCH,
1910
+ mergeStrategy: "squash",
1911
+ message,
1912
+ autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
1913
+ blockers,
1914
+ rootRepo: createWorkspaceRootRepoReport(root),
1915
+ repos: createWorkspacePackageReports(root),
1916
+ plannedSteps: [
1917
+ ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
1918
+ id: `merge-${pkg.name}`,
1919
+ description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
1920
+ })),
1921
+ { id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
1922
+ { id: "wait-staging", description: "Wait for staging automation" },
1923
+ { id: "preview-cleanup", description: "Destroy preview resources" },
1924
+ { id: "cleanup-root", description: "Archive and delete the task branch from market" },
1925
+ ...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
1926
+ id: `cleanup-${pkg.name}`,
1927
+ description: `Archive and delete the task branch from ${pkg.name}`
1928
+ }))
1929
+ ]
1930
+ },
1931
+ {
1932
+ executionMode,
1933
+ nextSteps: createNextSteps([
1934
+ { operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
1935
+ ])
1936
+ }
1937
+ );
1938
+ }
791
1939
  const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
792
1940
  message,
793
1941
  autoSave: input.autoSave
794
1942
  });
795
- const featureBranch = assertFeatureBranch(tenantRoot);
796
- runWorkspaceSavePreflight({ cwd: tenantRoot });
797
- let repoDir;
798
- try {
799
- repoDir = mergeCurrentBranchIntoStaging(tenantRoot, featureBranch);
800
- } catch (error) {
801
- const report = collectMergeConflictReport(gitWorkflowRoot(tenantRoot));
802
- throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, gitWorkflowRoot(tenantRoot), STAGING_BRANCH), {
803
- details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
804
- exitCode: 12
805
- });
806
- }
807
- const stagingWait = input.waitForStaging === false ? { status: "skipped", reason: "disabled" } : waitForStagingAutomation(repoDir);
808
- const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
809
- const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
810
- const remoteDeleted = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
811
- if (input.deleteBranch !== false) {
812
- deleteLocalBranch(repoDir, featureBranch);
1943
+ const session = resolveTreeseedWorkflowSession(root);
1944
+ const featureBranch = assertFeatureBranch(root);
1945
+ const mode = session.mode;
1946
+ assertSessionBranchSafety("stage", session);
1947
+ if (mode === "recursive-workspace") {
1948
+ assertWorkspaceClean(root);
1949
+ } else {
1950
+ assertCleanWorktree(root);
813
1951
  }
814
- return buildWorkflowResult(
1952
+ validateStagingWorkflowContracts(root);
1953
+ runWorkspaceSavePreflight({ cwd: root });
1954
+ const repoDir = session.gitRoot;
1955
+ const rootRepo = createWorkspaceRootRepoReport(root);
1956
+ const packageReports = createWorkspacePackageReports(root);
1957
+ const workflowRun = acquireWorkflowRun(
815
1958
  "stage",
816
- tenantRoot,
817
- {
1959
+ session,
1960
+ { message, waitForStaging: input.waitForStaging !== false, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
1961
+ [
1962
+ ...packageReports.map((report) => ({
1963
+ id: `merge-${report.name}`,
1964
+ description: `Merge ${featureBranch} into ${report.name} staging`,
1965
+ repoName: report.name,
1966
+ repoPath: report.path,
1967
+ branch: featureBranch,
1968
+ resumable: true
1969
+ })),
1970
+ { id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
1971
+ { id: "wait-staging", description: "Wait for staging automation", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
1972
+ { id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
1973
+ { id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
1974
+ ...packageReports.map((report) => ({
1975
+ id: `cleanup-${report.name}`,
1976
+ description: `Archive ${featureBranch} in ${report.name}`,
1977
+ repoName: report.name,
1978
+ repoPath: report.path,
1979
+ branch: featureBranch,
1980
+ resumable: true
1981
+ }))
1982
+ ],
1983
+ helpers.context
1984
+ );
1985
+ try {
1986
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
1987
+ const report = findReportByName(packageReports, pkg.name);
1988
+ if (!report) {
1989
+ continue;
1990
+ }
1991
+ if (!ensureLocalTaskBranch(pkg.dir, featureBranch)) {
1992
+ report.skippedReason = "branch-missing";
1993
+ skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
1994
+ continue;
1995
+ }
1996
+ try {
1997
+ const mergeResult = await executeJournalStep(root, workflowRun.runId, `merge-${report.name}`, () => squashMergeBranchIntoStaging(pkg.dir, featureBranch, message, { pushTarget: true }));
1998
+ report.merged = mergeResult.committed;
1999
+ report.committed = mergeResult.committed;
2000
+ report.pushed = mergeResult.pushed;
2001
+ report.commitSha = mergeResult.commitSha;
2002
+ report.branch = STAGING_BRANCH;
2003
+ checkoutBranch(pkg.dir, featureBranch);
2004
+ report.branch = featureBranch;
2005
+ } catch (error) {
2006
+ const reportData = collectMergeConflictReport(pkg.dir);
2007
+ throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(reportData, pkg.dir, STAGING_BRANCH), {
2008
+ details: { branch: featureBranch, packageName: pkg.name, report: reportData, originalError: error instanceof Error ? error.message : String(error) },
2009
+ exitCode: 12
2010
+ });
2011
+ }
2012
+ }
2013
+ try {
2014
+ const rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", () => {
2015
+ assertCleanWorktree(root);
2016
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
2017
+ run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
2018
+ if (mode === "recursive-workspace") {
2019
+ syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2020
+ }
2021
+ if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
2022
+ run("git", ["add", "-A"], { cwd: repoDir });
2023
+ run("git", ["commit", "-m", message], { cwd: repoDir });
2024
+ }
2025
+ pushBranch(repoDir, STAGING_BRANCH);
2026
+ return {
2027
+ commitSha: headCommit(repoDir),
2028
+ branch: currentBranch(repoDir) || STAGING_BRANCH,
2029
+ committed: hasMeaningfulChanges(repoDir) ? false : true
2030
+ };
2031
+ });
2032
+ rootRepo.merged = true;
2033
+ rootRepo.committed = true;
2034
+ rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
2035
+ rootRepo.pushed = true;
2036
+ rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
2037
+ } catch (error) {
2038
+ const report = collectMergeConflictReport(repoDir);
2039
+ throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
2040
+ details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
2041
+ exitCode: 12
2042
+ });
2043
+ }
2044
+ 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));
2045
+ 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));
2046
+ const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
2047
+ const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
2048
+ const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
2049
+ if (input.deleteBranch !== false) {
2050
+ deleteLocalBranch(repoDir, featureBranch);
2051
+ }
2052
+ return {
2053
+ deprecatedTag,
2054
+ deletedRemote,
2055
+ deletedLocal: input.deleteBranch !== false,
2056
+ branch: currentBranch(repoDir) || STAGING_BRANCH
2057
+ };
2058
+ });
2059
+ rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? rootRepo.tagName ?? "");
2060
+ rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
2061
+ rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
2062
+ rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
2063
+ rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
2064
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2065
+ const report = findReportByName(packageReports, pkg.name);
2066
+ if (!report) {
2067
+ continue;
2068
+ }
2069
+ const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
2070
+ deleteBranch: input.deleteBranch !== false,
2071
+ targetBranch: STAGING_BRANCH
2072
+ }));
2073
+ Object.assign(report, cleanup);
2074
+ }
2075
+ const payload = {
2076
+ mode,
818
2077
  branchName: featureBranch,
819
2078
  mergeTarget: STAGING_BRANCH,
2079
+ mergeStrategy: "squash",
820
2080
  message,
821
2081
  autoSaved: autoSave.performed,
822
2082
  autoSaveResult: autoSave.save,
823
- deprecatedTag,
2083
+ deprecatedTag: rootCleanup?.deprecatedTag ?? null,
2084
+ repos: packageReports,
2085
+ rootRepo,
824
2086
  stagingWait,
825
2087
  previewCleanup,
826
- remoteDeleted,
827
- localDeleted: input.deleteBranch !== false,
2088
+ remoteDeleted: rootRepo.deletedRemote,
2089
+ localDeleted: rootRepo.deletedLocal,
828
2090
  finalBranch: currentBranch(repoDir) || STAGING_BRANCH
829
- },
830
- createNextSteps([
831
- { operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
832
- { operation: "status", reason: "Inspect staging readiness after the task branch merge." }
833
- ])
834
- );
2091
+ };
2092
+ completeWorkflowRun(root, workflowRun.runId, payload);
2093
+ return buildWorkflowResult(
2094
+ "stage",
2095
+ root,
2096
+ payload,
2097
+ {
2098
+ runId: workflowRun.runId,
2099
+ nextSteps: createNextSteps([
2100
+ { operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
2101
+ { operation: "status", reason: "Inspect staging readiness after the task branch merge." }
2102
+ ])
2103
+ }
2104
+ );
2105
+ } catch (error) {
2106
+ failWorkflowRun(root, workflowRun.runId, error, {
2107
+ resumable: true,
2108
+ runId: workflowRun.runId,
2109
+ command: "stage",
2110
+ message: `Resume the interrupted stage for ${featureBranch}.`,
2111
+ recoverCommand: "treeseed recover",
2112
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
2113
+ });
2114
+ throw error;
2115
+ }
835
2116
  });
836
2117
  } catch (error) {
837
2118
  toError("stage", error);
@@ -839,101 +2120,507 @@ async function workflowStage(helpers, input) {
839
2120
  }
840
2121
  async function workflowRelease(helpers, input) {
841
2122
  try {
842
- return withContextEnv(helpers.context.env, () => {
2123
+ return await withContextEnv(helpers.context.env, async () => {
843
2124
  const level = input.bump ?? "patch";
844
2125
  const root = resolveProjectRootOrThrow("release", helpers.cwd());
845
- const gitRoot = repoRoot(root);
2126
+ const session = resolveTreeseedWorkflowSession(root);
2127
+ const gitRoot = session.gitRoot;
2128
+ const mode = session.mode;
2129
+ const executionMode = normalizeExecutionMode(input);
2130
+ const rootRepo = createWorkspaceRootRepoReport(root);
2131
+ const packageReports = createWorkspacePackageReports(root);
2132
+ const packageSelection = session.packageSelection;
2133
+ const selectedPackageNames = new Set(packageSelection.selected);
2134
+ const blockers = [];
2135
+ if (session.branchName !== STAGING_BRANCH) {
2136
+ blockers.push("Release must start from staging.");
2137
+ }
2138
+ if (mode === "recursive-workspace") {
2139
+ try {
2140
+ assertWorkspaceVersionConsistency(root);
2141
+ validatePackageReleaseWorkflows(root, packageSelection.selected);
2142
+ } catch (error) {
2143
+ blockers.push(error instanceof Error ? error.message : String(error));
2144
+ }
2145
+ }
2146
+ const versionPlan = planWorkspaceReleaseBump(level, root, mode === "recursive-workspace" ? { selectedPackageNames } : {});
2147
+ const plannedVersions = Object.fromEntries(versionPlan.versions.entries());
2148
+ if (executionMode === "plan") {
2149
+ return buildWorkflowResult(
2150
+ "release",
2151
+ root,
2152
+ {
2153
+ mode,
2154
+ mergeStrategy: "merge-commit",
2155
+ level,
2156
+ stagingBranch: STAGING_BRANCH,
2157
+ productionBranch: PRODUCTION_BRANCH,
2158
+ packageSelection,
2159
+ plannedVersions,
2160
+ repos: packageReports,
2161
+ rootRepo,
2162
+ plannedSteps: [
2163
+ ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
2164
+ id: `release-${report.name}`,
2165
+ description: `Release ${report.name} from staging to main and tag ${plannedVersions[report.name] ?? "(planned)"}`
2166
+ })),
2167
+ { id: "release-root", description: `Release market ${plannedVersions["@treeseed/market"] ?? "(planned)"}` }
2168
+ ],
2169
+ blockers
2170
+ },
2171
+ {
2172
+ executionMode,
2173
+ nextSteps: createNextSteps([
2174
+ { operation: "release", reason: "Run without --plan to promote staging into production.", input: { bump: level } }
2175
+ ])
2176
+ }
2177
+ );
2178
+ }
2179
+ if (blockers.length > 0) {
2180
+ workflowError("release", "validation_failed", blockers.join("\n"), {
2181
+ details: { blockers }
2182
+ });
2183
+ }
2184
+ assertSessionBranchSafety("release", session);
846
2185
  prepareReleaseBranches(root);
847
2186
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
848
2187
  runWorkspaceSavePreflight({ cwd: root });
849
- const plan = planWorkspaceReleaseBump(level, root);
850
- applyWorkspaceVersionChanges(plan);
851
- const rootVersion = bumpRootPackageJson(root, level);
852
- run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
853
- run("git", ["add", "-A"], { cwd: gitRoot });
854
- run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
855
- pushBranch(gitRoot, STAGING_BRANCH);
856
- mergeStagingIntoMain(root);
857
- const releasedCommit = run("git", ["rev-parse", PRODUCTION_BRANCH], { cwd: gitRoot, capture: true }).trim();
858
- run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
859
- run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
860
- syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
861
- return buildWorkflowResult(
2188
+ const workflowRun = acquireWorkflowRun(
862
2189
  "release",
863
- root,
864
- {
2190
+ session,
2191
+ { bump: level },
2192
+ [
2193
+ ...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
2194
+ id: `release-${report.name}`,
2195
+ description: `Release ${report.name}`,
2196
+ repoName: report.name,
2197
+ repoPath: report.path,
2198
+ branch: STAGING_BRANCH,
2199
+ resumable: true
2200
+ })),
2201
+ { id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
2202
+ ],
2203
+ helpers.context
2204
+ );
2205
+ try {
2206
+ if (mode === "root-only") {
2207
+ const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2208
+ applyWorkspaceVersionChanges(versionPlan);
2209
+ const rootVersion = bumpRootPackageJson(root, level);
2210
+ run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2211
+ run("git", ["add", "-A"], { cwd: gitRoot });
2212
+ run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2213
+ pushBranch(gitRoot, STAGING_BRANCH);
2214
+ const released = mergeStagingIntoMain(root);
2215
+ run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
2216
+ run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2217
+ syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2218
+ return {
2219
+ rootVersion,
2220
+ releasedCommit: released.commitSha
2221
+ };
2222
+ });
2223
+ rootRepo.committed = true;
2224
+ rootRepo.pushed = true;
2225
+ rootRepo.merged = true;
2226
+ rootRepo.branch = PRODUCTION_BRANCH;
2227
+ rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
2228
+ rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
2229
+ const payload2 = {
2230
+ mode,
2231
+ mergeStrategy: "merge-commit",
2232
+ level,
2233
+ rootVersion: String(rootRelease2?.rootVersion ?? ""),
2234
+ releaseTag: String(rootRelease2?.rootVersion ?? ""),
2235
+ releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
2236
+ stagingBranch: STAGING_BRANCH,
2237
+ productionBranch: PRODUCTION_BRANCH,
2238
+ touchedPackages: [...versionPlan.touched],
2239
+ packageSelection: { changed: [], dependents: [], selected: [] },
2240
+ publishWait: [],
2241
+ repos: [],
2242
+ rootRepo,
2243
+ finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
2244
+ pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true }
2245
+ };
2246
+ completeWorkflowRun(root, workflowRun.runId, payload2);
2247
+ return buildWorkflowResult("release", root, payload2, {
2248
+ runId: workflowRun.runId,
2249
+ nextSteps: createNextSteps([
2250
+ { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
2251
+ ])
2252
+ });
2253
+ }
2254
+ assertWorkspaceVersionConsistency(root);
2255
+ validatePackageReleaseWorkflows(root, packageSelection.selected);
2256
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2257
+ if (selectedPackageNames.has(pkg.name)) {
2258
+ prepareReleaseBranches(pkg.dir);
2259
+ }
2260
+ }
2261
+ applyWorkspaceVersionChanges(versionPlan);
2262
+ const publishWait = [];
2263
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2264
+ const report = findReportByName(packageReports, pkg.name);
2265
+ if (!report || !selectedPackageNames.has(pkg.name)) {
2266
+ if (report) {
2267
+ report.skippedReason = "unchanged";
2268
+ }
2269
+ continue;
2270
+ }
2271
+ const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, () => {
2272
+ checkoutBranch(pkg.dir, STAGING_BRANCH);
2273
+ if (hasMeaningfulChanges(pkg.dir)) {
2274
+ run("git", ["add", "-A"], { cwd: pkg.dir });
2275
+ run("git", ["commit", "-m", `release: ${versionPlan.versions.get(pkg.name)}`], { cwd: pkg.dir });
2276
+ }
2277
+ pushBranch(pkg.dir, STAGING_BRANCH);
2278
+ const mergeResult = mergeBranchIntoTarget(pkg.dir, {
2279
+ sourceBranch: STAGING_BRANCH,
2280
+ targetBranch: PRODUCTION_BRANCH,
2281
+ message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
2282
+ pushTarget: true
2283
+ });
2284
+ const tagName = String(versionPlan.versions.get(pkg.name));
2285
+ run("git", ["tag", "-a", tagName, "-m", `release: ${tagName}`], { cwd: pkg.dir });
2286
+ run("git", ["push", "origin", tagName], { cwd: pkg.dir });
2287
+ const publish = waitForGitHubWorkflowCompletion(pkg.dir, {
2288
+ workflow: "publish.yml",
2289
+ headSha: mergeResult.commitSha,
2290
+ branch: PRODUCTION_BRANCH
2291
+ });
2292
+ syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
2293
+ return {
2294
+ commitSha: mergeResult.commitSha,
2295
+ tagName,
2296
+ publish
2297
+ };
2298
+ });
2299
+ report.committed = true;
2300
+ report.pushed = true;
2301
+ report.merged = true;
2302
+ report.tagName = String(releasedPackage?.tagName ?? "");
2303
+ report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
2304
+ report.publishWait = releasedPackage?.publish ?? null;
2305
+ report.branch = STAGING_BRANCH;
2306
+ publishWait.push({
2307
+ name: report.name,
2308
+ ...releasedPackage?.publish ?? {}
2309
+ });
2310
+ }
2311
+ const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
2312
+ const rootVersion = bumpRootPackageJson(root, level);
2313
+ run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
2314
+ run("git", ["add", "-A"], { cwd: gitRoot });
2315
+ run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
2316
+ pushBranch(gitRoot, STAGING_BRANCH);
2317
+ const released = mergeBranchIntoTarget(root, {
2318
+ sourceBranch: STAGING_BRANCH,
2319
+ targetBranch: PRODUCTION_BRANCH,
2320
+ message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
2321
+ pushTarget: false
2322
+ });
2323
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
2324
+ if (selectedPackageNames.has(pkg.name)) {
2325
+ syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
2326
+ }
2327
+ }
2328
+ run("git", ["add", "-A"], { cwd: gitRoot });
2329
+ if (hasMeaningfulChanges(gitRoot)) {
2330
+ run("git", ["commit", "-m", "release: sync package main heads"], { cwd: gitRoot });
2331
+ }
2332
+ run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
2333
+ run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
2334
+ run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
2335
+ syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
2336
+ syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
2337
+ return {
2338
+ rootVersion,
2339
+ releasedCommit: headCommit(gitRoot)
2340
+ };
2341
+ });
2342
+ rootRepo.committed = true;
2343
+ rootRepo.pushed = true;
2344
+ rootRepo.merged = true;
2345
+ rootRepo.branch = PRODUCTION_BRANCH;
2346
+ rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
2347
+ rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
2348
+ const payload = {
2349
+ mode,
2350
+ mergeStrategy: "merge-commit",
865
2351
  level,
866
- rootVersion,
867
- releaseTag: rootVersion,
868
- releasedCommit,
2352
+ rootVersion: String(rootRelease?.rootVersion ?? ""),
2353
+ releaseTag: String(rootRelease?.rootVersion ?? ""),
2354
+ releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
869
2355
  stagingBranch: STAGING_BRANCH,
870
2356
  productionBranch: PRODUCTION_BRANCH,
871
- touchedPackages: [...plan.touched],
2357
+ touchedPackages: packageSelection.selected,
2358
+ packageSelection,
2359
+ publishWait,
2360
+ repos: packageReports,
2361
+ rootRepo,
872
2362
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
873
2363
  pushStatus: {
874
2364
  stagingPushed: true,
875
2365
  productionPushed: true,
876
2366
  tagPushed: true
877
2367
  }
2368
+ };
2369
+ completeWorkflowRun(root, workflowRun.runId, payload);
2370
+ return buildWorkflowResult(
2371
+ "release",
2372
+ root,
2373
+ payload,
2374
+ {
2375
+ runId: workflowRun.runId,
2376
+ nextSteps: createNextSteps([
2377
+ { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
2378
+ ])
2379
+ }
2380
+ );
2381
+ } catch (error) {
2382
+ failWorkflowRun(root, workflowRun.runId, error, {
2383
+ resumable: true,
2384
+ runId: workflowRun.runId,
2385
+ command: "release",
2386
+ message: `Resume the interrupted release on ${STAGING_BRANCH}.`,
2387
+ recoverCommand: "treeseed recover",
2388
+ resumeCommand: `treeseed resume ${workflowRun.runId}`
2389
+ });
2390
+ throw error;
2391
+ }
2392
+ });
2393
+ } catch (error) {
2394
+ toError("release", error);
2395
+ }
2396
+ }
2397
+ async function workflowResume(helpers, input) {
2398
+ try {
2399
+ return await withContextEnv(helpers.context.env, async () => {
2400
+ const root = resolveProjectRootOrThrow("resume", helpers.cwd());
2401
+ const runId = String(input.runId ?? "").trim();
2402
+ if (!runId) {
2403
+ workflowError("resume", "validation_failed", "Treeseed resume requires a run id.");
2404
+ }
2405
+ const journal = readWorkflowRunJournal(root, runId);
2406
+ if (!journal) {
2407
+ workflowError("resume", "resume_unavailable", `Treeseed resume could not find run ${runId}.`, {
2408
+ details: { runId }
2409
+ });
2410
+ }
2411
+ if (journal.status === "completed") {
2412
+ workflowError("resume", "resume_unavailable", `Run ${runId} is already completed.`, {
2413
+ details: { runId, status: journal.status }
2414
+ });
2415
+ }
2416
+ if (!journal.resumable) {
2417
+ workflowError("resume", "resume_unavailable", `Run ${runId} is not resumable.`, {
2418
+ details: { runId, status: journal.status }
2419
+ });
2420
+ }
2421
+ const resumedHelpers = {
2422
+ ...helpers,
2423
+ context: {
2424
+ ...helpers.context,
2425
+ workflow: {
2426
+ ...helpers.context.workflow ?? {},
2427
+ resumeRunId: runId
2428
+ }
2429
+ }
2430
+ };
2431
+ switch (journal.command) {
2432
+ case "switch":
2433
+ return workflowSwitch(resumedHelpers, journal.input);
2434
+ case "save":
2435
+ return workflowSave(resumedHelpers, journal.input);
2436
+ case "close":
2437
+ return workflowClose(resumedHelpers, journal.input);
2438
+ case "stage":
2439
+ return workflowStage(resumedHelpers, journal.input);
2440
+ case "release":
2441
+ return workflowRelease(resumedHelpers, journal.input);
2442
+ case "destroy":
2443
+ return workflowDestroy(resumedHelpers, journal.input);
2444
+ default:
2445
+ workflowError("resume", "resume_unavailable", `Run ${runId} uses unsupported command ${journal.command}.`, {
2446
+ details: { runId, command: journal.command }
2447
+ });
2448
+ }
2449
+ });
2450
+ } catch (error) {
2451
+ toError("resume", error);
2452
+ }
2453
+ }
2454
+ async function workflowRecover(helpers, input = {}) {
2455
+ try {
2456
+ return await withContextEnv(helpers.context.env, async () => {
2457
+ const root = resolveProjectRootOrThrow("recover", helpers.cwd());
2458
+ const lock = inspectWorkflowLock(root);
2459
+ const journals = listWorkflowRunJournals(root);
2460
+ const interruptedRuns = listInterruptedWorkflowRuns(root).map((journal) => ({
2461
+ runId: journal.runId,
2462
+ command: journal.command,
2463
+ status: journal.status,
2464
+ createdAt: journal.createdAt,
2465
+ updatedAt: journal.updatedAt,
2466
+ nextStep: nextPendingJournalStep(journal)?.description ?? null,
2467
+ failure: journal.failure,
2468
+ resumeCommand: `treeseed resume ${journal.runId}`
2469
+ }));
2470
+ const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
2471
+ return buildWorkflowResult(
2472
+ "recover",
2473
+ root,
2474
+ {
2475
+ lock,
2476
+ interruptedRuns,
2477
+ selectedRun,
2478
+ runCount: journals.length
878
2479
  },
879
- createNextSteps([
880
- { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
881
- ])
2480
+ {
2481
+ includeFinalState: false,
2482
+ nextSteps: createNextSteps([
2483
+ ...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." }]
2484
+ ])
2485
+ }
882
2486
  );
883
2487
  });
884
2488
  } catch (error) {
885
- toError("release", error);
2489
+ toError("recover", error);
886
2490
  }
887
2491
  }
888
2492
  async function workflowDestroy(helpers, input) {
889
2493
  try {
890
2494
  return withContextEnv(helpers.context.env, async () => {
891
- const tenantRoot = helpers.cwd();
2495
+ const tenantRoot = resolveProjectRootOrThrow("destroy", helpers.cwd());
2496
+ const root = workspaceRoot(tenantRoot);
2497
+ const session = resolveTreeseedWorkflowSession(root);
892
2498
  const scope = String(input.environment ?? input.target ?? "");
893
2499
  if (!scope) {
894
2500
  workflowError("destroy", "validation_failed", "Treeseed destroy requires an environment target.");
895
2501
  }
2502
+ const executionMode = normalizeExecutionMode(input);
896
2503
  const target = createPersistentDeployTarget(scope);
897
- const dryRun = input.dryRun === true;
2504
+ const dryRun = executionMode === "plan";
898
2505
  const force = input.force === true;
899
2506
  const destroyRemote = input.destroyRemote !== false;
900
2507
  const destroyLocal = input.destroyLocal !== false;
901
2508
  const removeBuildArtifacts = input.removeBuildArtifacts === true;
902
2509
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
903
2510
  assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose: "destroy" });
904
- const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: !dryRun && destroyRemote });
2511
+ const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: executionMode === "execute" && destroyRemote });
905
2512
  const state = loadDeployState(tenantRoot, deployConfig, { target });
906
2513
  const expectedConfirmation = deployConfig.slug;
907
- const confirmed = await Promise.resolve(resolveDestroyConfirmation(helpers.context, expectedConfirmation, input));
908
- if (!confirmed) {
909
- workflowError("destroy", "confirmation_required", `Destroy confirmation required. Re-run with confirm="${expectedConfirmation}".`);
910
- }
911
- const result = destroyRemote ? destroyCloudflareResources(tenantRoot, { dryRun, force, target }) : null;
912
- if (!dryRun && destroyLocal) {
913
- cleanupDestroyedState(tenantRoot, { target, removeBuildArtifacts });
2514
+ const payload = {
2515
+ scope,
2516
+ dryRun,
2517
+ force,
2518
+ destroyRemote,
2519
+ destroyLocal,
2520
+ removeBuildArtifacts,
2521
+ expectedConfirmation,
2522
+ stateSummary: {
2523
+ workerName: state.workerName,
2524
+ lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null
2525
+ },
2526
+ plannedSteps: [
2527
+ ...destroyRemote ? [{ id: "destroy-remote", description: `Destroy remote ${scope} resources` }] : [],
2528
+ ...destroyLocal ? [{ id: "cleanup-local", description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}` }] : []
2529
+ ],
2530
+ remoteResult: null
2531
+ };
2532
+ if (executionMode === "plan") {
2533
+ return buildWorkflowResult(
2534
+ "destroy",
2535
+ tenantRoot,
2536
+ payload,
2537
+ {
2538
+ executionMode,
2539
+ nextSteps: createNextSteps([
2540
+ { operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, removeBuildArtifacts } },
2541
+ { operation: "status", reason: "Confirm the current environment state before making destructive changes." }
2542
+ ])
2543
+ }
2544
+ );
914
2545
  }
915
- return {
916
- ok: true,
917
- operation: "destroy",
918
- payload: {
919
- scope,
920
- dryRun,
2546
+ const workflowRun = acquireWorkflowRun(
2547
+ "destroy",
2548
+ session,
2549
+ {
2550
+ environment: scope,
921
2551
  force,
922
2552
  destroyRemote,
923
2553
  destroyLocal,
924
- removeBuildArtifacts,
925
- expectedConfirmation,
926
- stateSummary: {
927
- workerName: state.workerName,
928
- lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null
929
- },
930
- remoteResult: result
2554
+ removeBuildArtifacts
931
2555
  },
932
- nextSteps: createNextSteps([
933
- { operation: "config", reason: "Recreate the destroyed environment before using it again.", input: { environment: [scope] } },
934
- { operation: "status", reason: "Confirm the environment teardown state and any remaining local runtime setup." }
935
- ])
936
- };
2556
+ [
2557
+ ...destroyRemote ? [{
2558
+ id: "destroy-remote",
2559
+ description: `Destroy remote ${scope} resources`,
2560
+ repoName: session.rootRepo.name,
2561
+ repoPath: session.rootRepo.path,
2562
+ branch: session.branchName,
2563
+ resumable: false
2564
+ }] : [],
2565
+ ...destroyLocal ? [{
2566
+ id: "cleanup-local",
2567
+ description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}`,
2568
+ repoName: session.rootRepo.name,
2569
+ repoPath: session.rootRepo.path,
2570
+ branch: session.branchName,
2571
+ resumable: false
2572
+ }] : []
2573
+ ],
2574
+ helpers.context
2575
+ );
2576
+ try {
2577
+ const confirmed = await Promise.resolve(resolveDestroyConfirmation(helpers.context, expectedConfirmation, input));
2578
+ if (!confirmed) {
2579
+ workflowError("destroy", "confirmation_required", `Destroy confirmation required. Re-run with confirm="${expectedConfirmation}".`);
2580
+ }
2581
+ const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyCloudflareResources(tenantRoot, { dryRun: false, force, target })) : null;
2582
+ if (!destroyRemote) {
2583
+ skipJournalStep(root, workflowRun.runId, "destroy-remote", { skippedReason: "destroyRemote=false" });
2584
+ }
2585
+ if (destroyLocal) {
2586
+ await executeJournalStep(root, workflowRun.runId, "cleanup-local", () => {
2587
+ cleanupDestroyedState(tenantRoot, { target, removeBuildArtifacts });
2588
+ return {
2589
+ cleaned: true,
2590
+ removeBuildArtifacts
2591
+ };
2592
+ });
2593
+ } else {
2594
+ skipJournalStep(root, workflowRun.runId, "cleanup-local", { skippedReason: "destroyLocal=false" });
2595
+ }
2596
+ const resultPayload = {
2597
+ ...payload,
2598
+ dryRun: false,
2599
+ remoteResult
2600
+ };
2601
+ completeWorkflowRun(root, workflowRun.runId, resultPayload);
2602
+ return buildWorkflowResult(
2603
+ "destroy",
2604
+ tenantRoot,
2605
+ resultPayload,
2606
+ {
2607
+ runId: workflowRun.runId,
2608
+ nextSteps: createNextSteps([
2609
+ { operation: "config", reason: "Recreate the destroyed environment before using it again.", input: { environment: [scope] } },
2610
+ { operation: "status", reason: "Confirm the environment teardown state and any remaining local runtime setup." }
2611
+ ])
2612
+ }
2613
+ );
2614
+ } catch (error) {
2615
+ failWorkflowRun(root, workflowRun.runId, error, {
2616
+ resumable: false,
2617
+ runId: workflowRun.runId,
2618
+ command: "destroy",
2619
+ message: `Inspect the failed destroy run for ${scope} before retrying manually.`,
2620
+ recoverCommand: "treeseed recover"
2621
+ });
2622
+ throw error;
2623
+ }
937
2624
  });
938
2625
  } catch (error) {
939
2626
  toError("destroy", error);
@@ -946,7 +2633,9 @@ export {
946
2633
  workflowDestroy,
947
2634
  workflowDev,
948
2635
  workflowExport,
2636
+ workflowRecover,
949
2637
  workflowRelease,
2638
+ workflowResume,
950
2639
  workflowSave,
951
2640
  workflowStage,
952
2641
  workflowStatus,