@treeseed/sdk 0.4.6 → 0.4.7

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.
@@ -73,6 +73,10 @@ import {
73
73
  } from "../operations/services/workspace-save.js";
74
74
  import { run, workspaceRoot } from "../operations/services/workspace-tools.js";
75
75
  import { resolveTreeseedWorkflowState } from "../workflow-state.js";
76
+ import {
77
+ classifyTreeseedBranchRole,
78
+ resolveTreeseedWorkflowPaths
79
+ } from "./policy.js";
76
80
  class TreeseedWorkflowError extends Error {
77
81
  code;
78
82
  operation;
@@ -125,17 +129,6 @@ function withContextEnv(env, action) {
125
129
  }
126
130
  }
127
131
  }
128
- function runChild(command, args, context, cwd, label) {
129
- const result = spawnSync(command, args, {
130
- cwd,
131
- env: { ...process.env, ...context.env ?? {} },
132
- stdio: "inherit"
133
- });
134
- if (result.status !== 0) {
135
- workflowError("dev", "unsupported_state", `${label} failed.`, { exitCode: result.status ?? 1 });
136
- }
137
- return result;
138
- }
139
132
  function runNodeScript(scriptName, context, cwd, label) {
140
133
  const result = spawnSync(process.execPath, [packageScriptPath(scriptName)], {
141
134
  cwd,
@@ -157,6 +150,52 @@ function normalizeConfigScopes(input) {
157
150
  }
158
151
  return ["local", "staging", "prod"].filter((scope) => requested.includes(scope));
159
152
  }
153
+ function resolveWorkflowStateSnapshot(cwd) {
154
+ return resolveTreeseedWorkflowState(cwd);
155
+ }
156
+ function resolveProjectRootOrThrow(operation, cwd) {
157
+ const resolved = resolveTreeseedWorkflowPaths(cwd);
158
+ if (!resolved.tenantRoot) {
159
+ workflowError(operation, "validation_failed", `Treeseed ${operation} requires a Treeseed project. Run the command from inside a tenant or initialize one first.`);
160
+ }
161
+ return resolved.cwd;
162
+ }
163
+ function resolveRepoState(repoDir) {
164
+ const branchName = currentBranch(repoDir) || null;
165
+ return {
166
+ repoDir,
167
+ branchName,
168
+ branchRole: classifyTreeseedBranchRole(branchName, repoDir),
169
+ dirtyWorktree: gitStatusPorcelain(repoDir).length > 0
170
+ };
171
+ }
172
+ function buildWorkflowResult(operation, cwd, payload, nextSteps) {
173
+ return {
174
+ ok: true,
175
+ operation,
176
+ payload: {
177
+ ...payload,
178
+ finalState: resolveWorkflowStateSnapshot(cwd)
179
+ },
180
+ nextSteps
181
+ };
182
+ }
183
+ function ensureLocalReadinessOrThrow(operation, tenantRoot) {
184
+ const state = resolveWorkflowStateSnapshot(tenantRoot);
185
+ if (!state.readiness.local.ready) {
186
+ workflowError(
187
+ operation,
188
+ "validation_failed",
189
+ [
190
+ `Treeseed ${operation} requires the local environment to be configured.`,
191
+ ...state.readiness.local.blockers,
192
+ "Run `treeseed config --environment local` first."
193
+ ].join("\n"),
194
+ { details: { readiness: state.readiness.local } }
195
+ );
196
+ }
197
+ return state;
198
+ }
160
199
  function dedupeRepairActions(actions) {
161
200
  const seen = /* @__PURE__ */ new Set();
162
201
  return actions.filter((action) => {
@@ -342,6 +381,56 @@ function resolveDestroyConfirmation(context, expected, input) {
342
381
  }
343
382
  return false;
344
383
  }
384
+ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
385
+ try {
386
+ if (remoteBranchExists(repoDir, branch)) {
387
+ run("git", ["pull", "--rebase", "origin", branch], { cwd: repoDir });
388
+ run("git", ["push", "origin", branch], { cwd: repoDir });
389
+ return {
390
+ remoteBranchExisted: true,
391
+ pulledRebase: true,
392
+ pushed: true,
393
+ createdRemoteBranch: false,
394
+ conflicts: false
395
+ };
396
+ }
397
+ run("git", ["push", "-u", "origin", branch], { cwd: repoDir });
398
+ return {
399
+ remoteBranchExisted: false,
400
+ pulledRebase: false,
401
+ pushed: true,
402
+ createdRemoteBranch: true,
403
+ conflicts: false
404
+ };
405
+ } catch {
406
+ const report = collectMergeConflictReport(repoDir);
407
+ throw new TreeseedWorkflowError(operation, "merge_conflict", formatMergeConflictReport(report, repoDir, branch), {
408
+ details: { branch, report },
409
+ exitCode: 12
410
+ });
411
+ }
412
+ }
413
+ async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
414
+ const tenantRoot = resolveProjectRootOrThrow(operation, helpers.cwd());
415
+ const repoDir = gitWorkflowRoot(tenantRoot);
416
+ const before = resolveRepoState(repoDir);
417
+ if (!before.dirtyWorktree) {
418
+ return { performed: false, save: null };
419
+ }
420
+ if (input.autoSave === false) {
421
+ workflowError(operation, "validation_failed", `Treeseed ${operation} requires a clean worktree or autoSave enabled.`);
422
+ }
423
+ const saveResult = await workflowSave(helpers, {
424
+ message: operation === "close" ? `close: ${input.message}` : input.message,
425
+ verify: input.verify === true,
426
+ refreshPreview: false,
427
+ preview: input.preview
428
+ });
429
+ return {
430
+ performed: true,
431
+ save: saveResult.payload
432
+ };
433
+ }
345
434
  async function workflowStatus(helpers) {
346
435
  return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
347
436
  }
@@ -351,7 +440,7 @@ async function workflowTasks(helpers) {
351
440
  async function workflowConfig(helpers, input = {}) {
352
441
  try {
353
442
  return await withContextEnv(helpers.context.env, async () => {
354
- const tenantRoot = helpers.cwd();
443
+ const tenantRoot = resolveProjectRootOrThrow("config", helpers.cwd());
355
444
  const scopes = normalizeConfigScopes(input);
356
445
  const sync = input.syncProviders ?? input.sync ?? "all";
357
446
  const printEnv = input.printEnv === true;
@@ -442,10 +531,10 @@ async function workflowConfig(helpers, input = {}) {
442
531
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
443
532
  const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
444
533
  const state = resolveTreeseedWorkflowState(tenantRoot);
445
- return {
446
- ok: true,
447
- operation: "config",
448
- payload: {
534
+ return buildWorkflowResult(
535
+ "config",
536
+ tenantRoot,
537
+ {
449
538
  mode: "configure",
450
539
  scopes,
451
540
  sync,
@@ -455,14 +544,15 @@ async function workflowConfig(helpers, input = {}) {
455
544
  preflight,
456
545
  toolHealth,
457
546
  result: wizardResult,
458
- state
547
+ state,
548
+ readiness: state.readiness
459
549
  },
460
- nextSteps: createNextSteps([
550
+ createNextSteps([
461
551
  ...scopes.includes("local") ? [{ operation: "dev", reason: "Start the local Treeseed runtime on the initialized local environment." }] : [],
462
552
  ...scopes.includes("staging") ? [{ operation: "status", reason: "Confirm staging readiness after initializing shared services." }] : [],
463
553
  { operation: "switch", reason: "Create or resume a task branch once the runtime foundation is ready.", input: { branch: "feature/my-change", preview: true } }
464
554
  ])
465
- };
555
+ );
466
556
  });
467
557
  } catch (error) {
468
558
  toError("config", error);
@@ -471,7 +561,7 @@ async function workflowConfig(helpers, input = {}) {
471
561
  async function workflowSwitch(helpers, input) {
472
562
  try {
473
563
  return withContextEnv(helpers.context.env, () => {
474
- const tenantRoot = helpers.cwd();
564
+ const tenantRoot = resolveProjectRootOrThrow("switch", helpers.cwd());
475
565
  const branchName = String(input.branch ?? input.branchName ?? "").trim();
476
566
  if (!branchName) {
477
567
  workflowError("switch", "validation_failed", "Treeseed switch requires a branch name.");
@@ -503,10 +593,10 @@ async function workflowSwitch(helpers, input) {
503
593
  previewResult = deployBranchPreview(tenantRoot, branchName, helpers.context, { initialize: true });
504
594
  }
505
595
  const state = resolveTreeseedWorkflowState(tenantRoot);
506
- return {
507
- ok: true,
508
- operation: "switch",
509
- payload: {
596
+ return buildWorkflowResult(
597
+ "switch",
598
+ tenantRoot,
599
+ {
510
600
  branchName,
511
601
  created,
512
602
  resumed,
@@ -517,13 +607,16 @@ async function workflowSwitch(helpers, input) {
517
607
  lastDeploymentTimestamp: state.preview.lastDeploymentTimestamp
518
608
  },
519
609
  previewResult,
520
- state
610
+ preconditions: {
611
+ cleanWorktreeRequired: true,
612
+ baseBranch: STAGING_BRANCH
613
+ }
521
614
  },
522
- nextSteps: createNextSteps([
523
- state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change" } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
615
+ createNextSteps([
616
+ 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." },
524
617
  { operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
525
618
  ])
526
- };
619
+ );
527
620
  });
528
621
  } catch (error) {
529
622
  toError("switch", error);
@@ -535,7 +628,8 @@ async function workflowDev(helpers, input = {}) {
535
628
  if (helpers.context.transport === "api") {
536
629
  workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
537
630
  }
538
- const tenantRoot = helpers.cwd();
631
+ const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
632
+ const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
539
633
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
540
634
  assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
541
635
  const args = [packageScriptPath("tenant-dev")];
@@ -553,38 +647,42 @@ async function workflowDev(helpers, input = {}) {
553
647
  stdio: input.stdio ?? "inherit",
554
648
  detached: process.platform !== "win32"
555
649
  });
556
- return {
557
- ok: true,
558
- operation: "dev",
559
- payload: {
560
- watch: input.watch === true,
561
- background: true,
562
- command: process.execPath,
563
- args,
564
- cwd: tenantRoot,
565
- pid: child.pid ?? null,
566
- exitCode: null
567
- }
568
- };
650
+ return buildWorkflowResult("dev", tenantRoot, {
651
+ watch: input.watch === true,
652
+ background: true,
653
+ command: process.execPath,
654
+ args,
655
+ cwd: tenantRoot,
656
+ pid: child.pid ?? null,
657
+ exitCode: null,
658
+ runtime: {
659
+ mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
660
+ apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
661
+ webUrl: "http://127.0.0.1:8787"
662
+ },
663
+ readiness: readiness.readiness.local
664
+ });
569
665
  }
570
666
  const result = spawnSync(process.execPath, args, {
571
667
  cwd: tenantRoot,
572
668
  env,
573
669
  stdio: input.stdio ?? "inherit"
574
670
  });
575
- return {
576
- ok: (result.status ?? 1) === 0,
577
- operation: "dev",
578
- payload: {
579
- watch: input.watch === true,
580
- background: false,
581
- command: process.execPath,
582
- args,
583
- cwd: tenantRoot,
584
- pid: null,
585
- exitCode: result.status ?? 1
586
- }
587
- };
671
+ return buildWorkflowResult("dev", tenantRoot, {
672
+ watch: input.watch === true,
673
+ background: false,
674
+ command: process.execPath,
675
+ args,
676
+ cwd: tenantRoot,
677
+ pid: null,
678
+ exitCode: result.status ?? 1,
679
+ runtime: {
680
+ mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
681
+ apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
682
+ webUrl: "http://127.0.0.1:8787"
683
+ },
684
+ readiness: readiness.readiness.local
685
+ });
588
686
  });
589
687
  } catch (error) {
590
688
  toError("dev", error);
@@ -593,7 +691,7 @@ async function workflowDev(helpers, input = {}) {
593
691
  async function workflowSave(helpers, input) {
594
692
  try {
595
693
  return withContextEnv(helpers.context.env, () => {
596
- const tenantRoot = helpers.cwd();
694
+ const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
597
695
  const message = ensureMessage("save", input.message, "a commit message");
598
696
  const optionsHotfix = input.hotfix === true;
599
697
  const root = workspaceRoot(tenantRoot);
@@ -616,45 +714,49 @@ async function workflowSave(helpers, input) {
616
714
  if (input.verify !== false) {
617
715
  runWorkspaceSavePreflight({ cwd: root });
618
716
  }
619
- if (!hasMeaningfulChanges(gitRoot)) {
620
- workflowError("save", "validation_failed", "Treeseed save found no meaningful repository changes to commit.");
717
+ const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
718
+ let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
719
+ let commitCreated = false;
720
+ if (hadMeaningfulChanges) {
721
+ run("git", ["add", "-A"], { cwd: gitRoot });
722
+ run("git", ["commit", "-m", message], { cwd: gitRoot });
723
+ head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
724
+ commitCreated = true;
621
725
  }
622
- run("git", ["add", "-A"], { cwd: gitRoot });
623
- run("git", ["commit", "-m", message], { cwd: gitRoot });
624
- const head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
625
- try {
626
- if (remoteBranchExists(gitRoot, branch)) {
627
- run("git", ["pull", "--rebase", "origin", branch], { cwd: gitRoot });
628
- run("git", ["push", "origin", branch], { cwd: gitRoot });
629
- } else {
630
- run("git", ["push", "-u", "origin", branch], { cwd: gitRoot });
726
+ const branchSync = syncCurrentBranchToOrigin("save", gitRoot, branch);
727
+ let previewAction = { status: "skipped" };
728
+ if (beforeState.branchRole === "feature" && branch) {
729
+ if (input.preview === true) {
730
+ previewAction = {
731
+ status: beforeState.preview.enabled ? "refreshed" : "created",
732
+ details: deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled })
733
+ };
734
+ } else if (input.refreshPreview !== false && beforeState.preview.enabled) {
735
+ previewAction = {
736
+ status: "refreshed",
737
+ details: deployBranchPreview(root, branch, helpers.context, { initialize: false })
738
+ };
631
739
  }
632
- } catch {
633
- const report = collectMergeConflictReport(gitRoot);
634
- throw new TreeseedWorkflowError("save", "merge_conflict", formatMergeConflictReport(report, gitRoot, branch), {
635
- details: { branch, report },
636
- exitCode: 12
637
- });
638
740
  }
639
- let previewRefresh = null;
640
- if (input.refreshPreview !== false && beforeState.branchRole === "feature" && beforeState.preview.enabled && branch) {
641
- previewRefresh = deployBranchPreview(root, branch, helpers.context, { initialize: false });
642
- }
643
- return {
644
- ok: true,
645
- operation: "save",
646
- payload: {
741
+ return buildWorkflowResult(
742
+ "save",
743
+ root,
744
+ {
647
745
  branch,
648
746
  scope,
649
747
  hotfix: optionsHotfix,
650
748
  message,
651
749
  commitSha: head,
652
- previewRefresh
750
+ commitCreated,
751
+ noChanges: !hadMeaningfulChanges,
752
+ branchSync,
753
+ previewAction,
754
+ mergeConflict: null
653
755
  },
654
- nextSteps: createNextSteps([
756
+ createNextSteps([
655
757
  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" } }
656
758
  ])
657
- };
759
+ );
658
760
  });
659
761
  } catch (error) {
660
762
  toError("save", error);
@@ -662,9 +764,13 @@ async function workflowSave(helpers, input) {
662
764
  }
663
765
  async function workflowClose(helpers, input) {
664
766
  try {
665
- return withContextEnv(helpers.context.env, () => {
666
- const tenantRoot = helpers.cwd();
767
+ return withContextEnv(helpers.context.env, async () => {
768
+ const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
667
769
  const message = ensureMessage("close", input.message, "a close reason");
770
+ const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
771
+ message,
772
+ autoSave: input.autoSave
773
+ });
668
774
  const featureBranch = assertFeatureBranch(tenantRoot);
669
775
  const repoDir = gitWorkflowRoot(tenantRoot);
670
776
  assertCleanWorktree(tenantRoot);
@@ -675,21 +781,24 @@ async function workflowClose(helpers, input) {
675
781
  if (input.deleteBranch !== false) {
676
782
  deleteLocalBranch(repoDir, featureBranch);
677
783
  }
678
- return {
679
- ok: true,
680
- operation: "close",
681
- payload: {
784
+ return buildWorkflowResult(
785
+ "close",
786
+ tenantRoot,
787
+ {
682
788
  branchName: featureBranch,
683
789
  message,
790
+ autoSaved: autoSave.performed,
791
+ autoSaveResult: autoSave.save,
684
792
  deprecatedTag,
685
793
  previewCleanup,
686
794
  remoteDeleted,
687
- localDeleted: input.deleteBranch !== false
795
+ localDeleted: input.deleteBranch !== false,
796
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH
688
797
  },
689
- nextSteps: createNextSteps([
798
+ createNextSteps([
690
799
  { operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
691
800
  ])
692
- };
801
+ );
693
802
  });
694
803
  } catch (error) {
695
804
  toError("close", error);
@@ -697,12 +806,25 @@ async function workflowClose(helpers, input) {
697
806
  }
698
807
  async function workflowStage(helpers, input) {
699
808
  try {
700
- return withContextEnv(helpers.context.env, () => {
701
- const tenantRoot = helpers.cwd();
809
+ return withContextEnv(helpers.context.env, async () => {
810
+ const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
702
811
  const message = ensureMessage("stage", input.message, "a resolution message");
812
+ const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
813
+ message,
814
+ autoSave: input.autoSave
815
+ });
703
816
  const featureBranch = assertFeatureBranch(tenantRoot);
704
817
  runWorkspaceSavePreflight({ cwd: tenantRoot });
705
- const repoDir = mergeCurrentBranchIntoStaging(tenantRoot, featureBranch);
818
+ let repoDir;
819
+ try {
820
+ repoDir = mergeCurrentBranchIntoStaging(tenantRoot, featureBranch);
821
+ } catch (error) {
822
+ const report = collectMergeConflictReport(gitWorkflowRoot(tenantRoot));
823
+ throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, gitWorkflowRoot(tenantRoot), STAGING_BRANCH), {
824
+ details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
825
+ exitCode: 12
826
+ });
827
+ }
706
828
  const stagingWait = input.waitForStaging === false ? { status: "skipped", reason: "disabled" } : waitForStagingAutomation(repoDir);
707
829
  const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
708
830
  const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
@@ -710,24 +832,27 @@ async function workflowStage(helpers, input) {
710
832
  if (input.deleteBranch !== false) {
711
833
  deleteLocalBranch(repoDir, featureBranch);
712
834
  }
713
- return {
714
- ok: true,
715
- operation: "stage",
716
- payload: {
835
+ return buildWorkflowResult(
836
+ "stage",
837
+ tenantRoot,
838
+ {
717
839
  branchName: featureBranch,
718
840
  mergeTarget: STAGING_BRANCH,
719
841
  message,
842
+ autoSaved: autoSave.performed,
843
+ autoSaveResult: autoSave.save,
720
844
  deprecatedTag,
721
845
  stagingWait,
722
846
  previewCleanup,
723
847
  remoteDeleted,
724
- localDeleted: input.deleteBranch !== false
848
+ localDeleted: input.deleteBranch !== false,
849
+ finalBranch: currentBranch(repoDir) || STAGING_BRANCH
725
850
  },
726
- nextSteps: createNextSteps([
851
+ createNextSteps([
727
852
  { operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
728
853
  { operation: "status", reason: "Inspect staging readiness after the task branch merge." }
729
854
  ])
730
- };
855
+ );
731
856
  });
732
857
  } catch (error) {
733
858
  toError("stage", error);
@@ -737,7 +862,7 @@ async function workflowRelease(helpers, input) {
737
862
  try {
738
863
  return withContextEnv(helpers.context.env, () => {
739
864
  const level = input.bump ?? "patch";
740
- const root = workspaceRoot(helpers.cwd());
865
+ const root = resolveProjectRootOrThrow("release", helpers.cwd());
741
866
  const gitRoot = repoRoot(root);
742
867
  prepareReleaseBranches(root);
743
868
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
@@ -750,23 +875,32 @@ async function workflowRelease(helpers, input) {
750
875
  run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
751
876
  pushBranch(gitRoot, STAGING_BRANCH);
752
877
  mergeStagingIntoMain(root);
878
+ const releasedCommit = run("git", ["rev-parse", PRODUCTION_BRANCH], { cwd: gitRoot, capture: true }).trim();
753
879
  run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
754
880
  run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
755
- return {
756
- ok: true,
757
- operation: "release",
758
- payload: {
881
+ syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
882
+ return buildWorkflowResult(
883
+ "release",
884
+ root,
885
+ {
759
886
  level,
760
887
  rootVersion,
761
888
  releaseTag: rootVersion,
889
+ releasedCommit,
762
890
  stagingBranch: STAGING_BRANCH,
763
891
  productionBranch: PRODUCTION_BRANCH,
764
- touchedPackages: [...plan.touched]
892
+ touchedPackages: [...plan.touched],
893
+ finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
894
+ pushStatus: {
895
+ stagingPushed: true,
896
+ productionPushed: true,
897
+ tagPushed: true
898
+ }
765
899
  },
766
- nextSteps: createNextSteps([
900
+ createNextSteps([
767
901
  { operation: "status", reason: "Inspect release readiness and production state after the promotion." }
768
902
  ])
769
- };
903
+ );
770
904
  });
771
905
  } catch (error) {
772
906
  toError("release", error);
@@ -0,0 +1,12 @@
1
+ export type TreeseedWorkflowBranchRole = 'feature' | 'staging' | 'main' | 'detached' | 'none';
2
+ export type TreeseedResolvedWorkflowPaths = {
3
+ requestedCwd: string;
4
+ tenantRoot: string | null;
5
+ cwd: string;
6
+ repoRoot: string | null;
7
+ branchName: string | null;
8
+ branchRole: TreeseedWorkflowBranchRole;
9
+ };
10
+ export declare function classifyTreeseedBranchRole(branchName: string | null, repoDir: string | null): TreeseedWorkflowBranchRole;
11
+ export declare function resolveTreeseedWorkflowPaths(startCwd: string): TreeseedResolvedWorkflowPaths;
12
+ export declare function workflowEnvironmentForBranchRole(branchRole: TreeseedWorkflowBranchRole): "local" | "staging" | "prod" | "none";
@@ -0,0 +1,58 @@
1
+ import { findNearestTreeseedRoot, run } from "../operations/services/workspace-tools.js";
2
+ import { currentBranch, repoRoot } from "../operations/services/workspace-save.js";
3
+ import { PRODUCTION_BRANCH, STAGING_BRANCH } from "../operations/services/git-workflow.js";
4
+ function safeRepoRoot(cwd) {
5
+ try {
6
+ return repoRoot(cwd);
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ function repoHasHead(repoDir) {
12
+ try {
13
+ run("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoDir, capture: true });
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function classifyTreeseedBranchRole(branchName, repoDir) {
20
+ if (!repoDir) {
21
+ return "none";
22
+ }
23
+ if (!branchName) {
24
+ return repoHasHead(repoDir) ? "detached" : "none";
25
+ }
26
+ if (branchName === STAGING_BRANCH) {
27
+ return "staging";
28
+ }
29
+ if (branchName === PRODUCTION_BRANCH) {
30
+ return "main";
31
+ }
32
+ return "feature";
33
+ }
34
+ function resolveTreeseedWorkflowPaths(startCwd) {
35
+ const tenantRoot = findNearestTreeseedRoot(startCwd);
36
+ const cwd = tenantRoot ?? startCwd;
37
+ const gitRoot = safeRepoRoot(cwd);
38
+ const branchName = gitRoot ? currentBranch(gitRoot) || null : null;
39
+ return {
40
+ requestedCwd: startCwd,
41
+ tenantRoot,
42
+ cwd,
43
+ repoRoot: gitRoot,
44
+ branchName,
45
+ branchRole: classifyTreeseedBranchRole(branchName, gitRoot)
46
+ };
47
+ }
48
+ function workflowEnvironmentForBranchRole(branchRole) {
49
+ if (branchRole === "staging") return "staging";
50
+ if (branchRole === "main") return "prod";
51
+ if (branchRole === "feature") return "local";
52
+ return "none";
53
+ }
54
+ export {
55
+ classifyTreeseedBranchRole,
56
+ resolveTreeseedWorkflowPaths,
57
+ workflowEnvironmentForBranchRole
58
+ };
@@ -1,5 +1,6 @@
1
1
  import type { TreeseedWorkflowNextStep } from './workflow.ts';
2
- export type TreeseedBranchRole = 'feature' | 'staging' | 'main' | 'detached' | 'none';
2
+ import { type TreeseedWorkflowBranchRole } from './workflow/policy.ts';
3
+ export type TreeseedBranchRole = TreeseedWorkflowBranchRole;
3
4
  export type TreeseedWorkflowRecommendation = TreeseedWorkflowNextStep;
4
5
  export type TreeseedWorkflowState = {
5
6
  cwd: string;
@@ -44,6 +45,23 @@ export type TreeseedWorkflowState = {
44
45
  devVars: boolean;
45
46
  };
46
47
  releaseReady: boolean;
48
+ readiness: {
49
+ local: {
50
+ ready: boolean;
51
+ blockers: string[];
52
+ warnings: string[];
53
+ };
54
+ staging: {
55
+ ready: boolean;
56
+ blockers: string[];
57
+ warnings: string[];
58
+ };
59
+ prod: {
60
+ ready: boolean;
61
+ blockers: string[];
62
+ warnings: string[];
63
+ };
64
+ };
47
65
  rollbackCandidates: Array<{
48
66
  scope: 'staging' | 'prod';
49
67
  commit: string | null;