@treeseed/sdk 0.12.44 → 0.12.46

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 (40) hide show
  1. package/dist/guarantees/index.d.ts +83 -0
  2. package/dist/guarantees/index.js +688 -86
  3. package/dist/hosting/graph.js +40 -6
  4. package/dist/operations/services/git-workflow.d.ts +16 -1
  5. package/dist/operations/services/git-workflow.js +65 -5
  6. package/dist/operations/services/github-api.js +0 -19
  7. package/dist/operations/services/hosted-service-checks.js +17 -1
  8. package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
  9. package/dist/operations/services/live-hosted-service-checks.js +8 -5
  10. package/dist/operations/services/local-cleanup.js +3 -7
  11. package/dist/operations/services/package-adapters.d.ts +14 -0
  12. package/dist/operations/services/package-adapters.js +36 -3
  13. package/dist/operations/services/package-artifacts.d.ts +37 -0
  14. package/dist/operations/services/package-artifacts.js +99 -0
  15. package/dist/operations/services/railway-deploy.js +78 -18
  16. package/dist/operations/services/railway-source-policy.d.ts +19 -0
  17. package/dist/operations/services/railway-source-policy.js +66 -0
  18. package/dist/operations/services/repository-save-orchestrator.js +88 -19
  19. package/dist/operations/services/workspace-dependency-mode.js +4 -0
  20. package/dist/platform/desired-state.js +3 -3
  21. package/dist/reconcile/builtin-adapters.js +10 -2
  22. package/dist/reconcile/providers/railway-iac.d.ts +2 -0
  23. package/dist/reconcile/providers/railway-iac.js +31 -3
  24. package/dist/reconcile/providers/release-private.d.ts +10 -0
  25. package/dist/reconcile/providers/release-private.js +45 -1
  26. package/dist/scenes/builtin-plugins.js +36 -5
  27. package/dist/scenes/device-matrix.js +2 -0
  28. package/dist/scenes/environment.js +1 -1
  29. package/dist/scenes/runner.js +28 -16
  30. package/dist/scenes/schema.js +31 -2
  31. package/dist/scenes/types.d.ts +25 -2
  32. package/dist/scenes/visual-audit-fixtures.js +9 -3
  33. package/dist/workflow/operations.d.ts +27 -92
  34. package/dist/workflow/operations.js +252 -144
  35. package/dist/workflow/runs.d.ts +1 -0
  36. package/dist/workflow/runs.js +57 -0
  37. package/dist/workflow-support.d.ts +1 -0
  38. package/dist/workflow-support.js +8 -0
  39. package/dist/workflow.d.ts +2 -0
  40. package/package.json +4 -1
@@ -6,6 +6,7 @@ import { parse as parseYaml } from "yaml";
6
6
  import { createPersistentDeployTarget } from "../operations/services/deploy.js";
7
7
  import { collectTreeseedConfigSeedValues, resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
8
8
  import { classifyTreeseedGitMode, runTreeseedGitText } from "../operations/services/git-runner.js";
9
+ import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../operations/services/railway-source-policy.js";
9
10
  import { createTreeseedCanonicalReconcileReport } from "../reconcile/index.js";
10
11
  import { reconcileTreeseedTarget } from "../reconcile/index.js";
11
12
  import { discoverTreeseedApplications, findTreeseedApplication } from "./apps.js";
@@ -64,12 +65,15 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
64
65
  const railway = config.publicTreeDxFederation?.railway ?? {};
65
66
  const configuredSource = railway.source && typeof railway.source === "object" && !Array.isArray(railway.source) ? railway.source : {};
66
67
  const configuredMode = typeof railway.sourceMode === "string" ? railway.sourceMode : null;
68
+ if (input.environment === "staging" && configuredMode === "image") {
69
+ throw new Error("public-treedx-node-01: API Railway staging services must use GitHub Dockerfile source builds (configured sourceMode image is not allowed).");
70
+ }
67
71
  const treeDxRoot = resolvePublicTreeDxRoot(input);
68
72
  const repository = typeof railway.sourceRepo === "string" ? railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(treeDxRoot) ?? "treeseed-ai/treedx";
69
73
  const sourceMode = configuredMode === "git" || configuredMode === "image" ? configuredMode : input.environment === "staging" ? "git" : "image";
70
74
  if (sourceMode !== "git") {
71
75
  const imageRef = typeof launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF === "string" && launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() ? launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() : null;
72
- return {
76
+ const policy2 = {
73
77
  sourceMode: "image",
74
78
  sourceRepo: null,
75
79
  sourceBranch: null,
@@ -79,8 +83,10 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
79
83
  imageRef,
80
84
  imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
81
85
  };
86
+ assertApiRailwaySourcePolicy(input.environment, { key: "public-treedx-node-01", serviceName: "public-treedx-node-01", ...policy2 });
87
+ return policy2;
82
88
  }
83
- return {
89
+ const policy = {
84
90
  sourceMode: "git",
85
91
  sourceRepo: repository,
86
92
  sourceBranch: typeof railway.sourceBranch === "string" ? railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : "staging",
@@ -89,6 +95,13 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
89
95
  image: null,
90
96
  imageTagRef: null
91
97
  };
98
+ assertApiRailwaySourcePolicy(input.environment, {
99
+ key: "public-treedx-node-01",
100
+ serviceName: "public-treedx-node-01",
101
+ dockerfilePath: railway.dockerfilePath ?? "/Dockerfile",
102
+ ...policy
103
+ });
104
+ return policy;
92
105
  }
93
106
  function serviceKeyPlacement(serviceKey) {
94
107
  if (serviceKey === "api") return "api";
@@ -161,11 +174,16 @@ function resolveRailwayServiceSourceRoot(input, serviceKey, service) {
161
174
  function railwaySourcePolicy(input, serviceKey, service, imageRef) {
162
175
  const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
163
176
  const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
164
- const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot);
177
+ const serviceName = service.railway?.serviceName ?? null;
178
+ const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
179
+ const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
165
180
  const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
181
+ if (input.environment === "staging" && isApiRailwaySourcePolicyService({ key: serviceKey, serviceName }) && (configuredMode === "image" || service.railway?.imageRef)) {
182
+ throw new Error(`${serviceName ?? serviceKey}: API Railway staging services must use GitHub Dockerfile source builds (configured image source is not allowed).`);
183
+ }
166
184
  const mode = input.environment === "prod" ? "image" : input.environment === "staging" && apiPackageSourceEligible ? "git" : configuredMode === "git" || configuredMode === "image" ? configuredMode : imageRef ? "image" : "git";
167
185
  if (mode !== "git") {
168
- return {
186
+ const policy2 = {
169
187
  sourceMode: "image",
170
188
  sourceRepo: null,
171
189
  sourceBranch: null,
@@ -173,8 +191,17 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
173
191
  sourceRootDirectory: null,
174
192
  imageRef
175
193
  };
194
+ assertApiRailwaySourcePolicy(input.environment, {
195
+ key: serviceKey,
196
+ serviceName,
197
+ dockerfilePath: null,
198
+ buildCommand: null,
199
+ startCommand: null,
200
+ ...policy2
201
+ });
202
+ return policy2;
176
203
  }
177
- return {
204
+ const policy = {
178
205
  sourceMode: "git",
179
206
  sourceRepo: repository,
180
207
  sourceBranch: typeof service.railway?.sourceBranch === "string" ? service.railway.sourceBranch : typeof configuredSource.branch === "string" ? configuredSource.branch : input.environment === "staging" ? "staging" : null,
@@ -182,6 +209,13 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
182
209
  sourceRootDirectory: typeof service.railway?.sourceRootDirectory === "string" ? service.railway.sourceRootDirectory : typeof configuredSource.rootDirectory === "string" ? configuredSource.rootDirectory : ".",
183
210
  imageRef: null
184
211
  };
212
+ assertApiRailwaySourcePolicy(input.environment, {
213
+ key: serviceKey,
214
+ serviceName,
215
+ dockerfilePath,
216
+ ...policy
217
+ });
218
+ return policy;
185
219
  }
186
220
  function collectPluginHostingContributions(input) {
187
221
  const plugins = loadTreeseedPlugins(input.deployConfig);
@@ -358,7 +392,7 @@ function buildProfileFromDeployConfig(input) {
358
392
  sourceBranch: sourcePolicy.sourceBranch,
359
393
  sourceCommit: sourcePolicy.sourceCommit,
360
394
  sourceRootDirectory: sourcePolicy.sourceRootDirectory,
361
- dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? null : null,
395
+ dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
362
396
  buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
363
397
  startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
364
398
  healthcheckPath: service.railway?.healthcheckPath ?? null,
@@ -123,6 +123,16 @@ export declare function checkoutTaskBranchFromStaging(cwd: any, branchName: any,
123
123
  resumed: boolean;
124
124
  remoteBranch: boolean;
125
125
  };
126
+ export declare function checkoutNewTaskBranchWithChanges(cwd: string, branchName: string, { pushIfCreated }?: {
127
+ pushIfCreated?: boolean | undefined;
128
+ }): {
129
+ repoDir: string;
130
+ branchName: string;
131
+ baseBranch: string;
132
+ created: boolean;
133
+ resumed: boolean;
134
+ remoteBranch: boolean;
135
+ };
126
136
  export declare function syncBranchWithOrigin(repoDir: any, branchName: any): void;
127
137
  export declare function checkoutDetachedOriginBranch(repoDir: any, branchName: any): void;
128
138
  export declare function pushHeadToBranch(repoDir: any, branchName: any): void;
@@ -201,13 +211,18 @@ export declare function listTaskBranches(repoDir: any): {
201
211
  remote: boolean;
202
212
  current: boolean;
203
213
  }[];
204
- export declare function waitForStagingAutomation(repoDir: any): {
214
+ export declare function shouldRetryFailedStagingAutomation(status: string | null, conclusion: string | null): boolean;
215
+ export declare function waitForStagingAutomation(repoDir: any, env?: NodeJS.ProcessEnv): {
205
216
  status: string;
206
217
  reason: string;
207
218
  branch?: undefined;
219
+ headSha?: undefined;
220
+ runId?: undefined;
208
221
  } | {
209
222
  status: string;
210
223
  branch: string;
224
+ headSha: string;
225
+ runId: number;
211
226
  reason?: undefined;
212
227
  };
213
228
  export declare function prepareReleaseBranches(cwd?: any): string;
@@ -437,6 +437,25 @@ function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true
437
437
  remoteBranch: pushIfCreated
438
438
  };
439
439
  }
440
+ function checkoutNewTaskBranchWithChanges(cwd, branchName, { pushIfCreated = false } = {}) {
441
+ const repoDir = repoRoot(cwd);
442
+ if (currentBranch(repoDir) !== STAGING_BRANCH) {
443
+ throw new Error(`Dirty change adoption requires ${repoDir} to be on ${STAGING_BRANCH}.`);
444
+ }
445
+ if (branchExists(repoDir, branchName) || remoteBranchExists(repoDir, branchName)) {
446
+ throw new Error(`Dirty change adoption requires a new branch; ${branchName} already exists.`);
447
+ }
448
+ runGit(["checkout", "-b", branchName], { cwd: repoDir });
449
+ if (pushIfCreated) pushBranch(repoDir, branchName, { setUpstream: true });
450
+ return {
451
+ repoDir,
452
+ branchName,
453
+ baseBranch: STAGING_BRANCH,
454
+ created: true,
455
+ resumed: false,
456
+ remoteBranch: pushIfCreated
457
+ };
458
+ }
440
459
  function syncBranchWithOrigin(repoDir, branchName) {
441
460
  fetchOrigin(repoDir);
442
461
  if (!branchExists(repoDir, branchName) && remoteBranchExists(repoDir, branchName)) {
@@ -637,20 +656,59 @@ function listTaskBranches(repoDir) {
637
656
  };
638
657
  });
639
658
  }
640
- function waitForStagingAutomation(repoDir) {
659
+ function shouldRetryFailedStagingAutomation(status, conclusion) {
660
+ return status === "completed" && Boolean(conclusion) && conclusion !== "success";
661
+ }
662
+ function waitForStagingAutomation(repoDir, env = process.env) {
641
663
  if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
642
664
  return { status: "skipped", reason: "disabled" };
643
665
  }
644
666
  try {
645
- const gh = resolveTreeseedToolBinary("gh");
667
+ const gh = resolveTreeseedToolBinary("gh", { env });
646
668
  if (!gh) {
647
669
  throw new Error("GitHub CLI `gh` is unavailable.");
648
670
  }
649
- run(gh, ["run", "watch", "--branch", STAGING_BRANCH, "--exit-status"], {
671
+ const headSha = remoteHeadCommit(repoDir, STAGING_BRANCH);
672
+ let runId = null;
673
+ let runStatus = null;
674
+ let runConclusion = null;
675
+ for (let attempt = 0; attempt < 120 && runId == null; attempt += 1) {
676
+ const output = run(gh, [
677
+ "run",
678
+ "list",
679
+ "--workflow",
680
+ "deploy.yml",
681
+ "--branch",
682
+ STAGING_BRANCH,
683
+ "--commit",
684
+ headSha,
685
+ "--limit",
686
+ "1",
687
+ "--json",
688
+ "databaseId,status,conclusion"
689
+ ], {
690
+ cwd: repoDir,
691
+ env: createTreeseedManagedToolEnv(env),
692
+ capture: true
693
+ });
694
+ const rows = JSON.parse(output || "[]");
695
+ runId = typeof rows[0]?.databaseId === "number" ? rows[0].databaseId : null;
696
+ runStatus = typeof rows[0]?.status === "string" ? rows[0].status : null;
697
+ runConclusion = typeof rows[0]?.conclusion === "string" ? rows[0].conclusion : null;
698
+ if (runId == null) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5e3);
699
+ }
700
+ if (runId == null) throw new Error(`No deploy.yml run appeared for ${headSha}.`);
701
+ if (shouldRetryFailedStagingAutomation(runStatus, runConclusion)) {
702
+ run(gh, ["run", "rerun", String(runId), "--failed"], {
703
+ cwd: repoDir,
704
+ env: createTreeseedManagedToolEnv(env)
705
+ });
706
+ }
707
+ run(gh, ["run", "watch", String(runId), "--exit-status"], {
650
708
  cwd: repoDir,
651
- env: createTreeseedManagedToolEnv(process.env)
709
+ env: createTreeseedManagedToolEnv(env)
652
710
  });
653
- return { status: "completed", branch: STAGING_BRANCH };
711
+ return { status: "completed", branch: STAGING_BRANCH, headSha, runId };
654
712
  } catch (error) {
655
713
  throw new Error([
656
714
  "Treeseed stage could not confirm the staging deploy/checks completed.",
@@ -710,6 +768,7 @@ export {
710
768
  branchExists,
711
769
  checkoutBranch,
712
770
  checkoutDetachedOriginBranch,
771
+ checkoutNewTaskBranchWithChanges,
713
772
  checkoutTaskBranchFromStaging,
714
773
  createDeprecatedTaskTag,
715
774
  createFeatureBranchFromStaging,
@@ -736,6 +795,7 @@ export {
736
795
  reattachDetachedHeadIfSafe,
737
796
  remoteBranchExists,
738
797
  remoteHeadCommit,
798
+ shouldRetryFailedStagingAutomation,
739
799
  squashMergeBranchIntoStaging,
740
800
  syncBranchWithOrigin,
741
801
  taskTagSlug,
@@ -792,25 +792,6 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
792
792
  monitorErrorStartedAt = null;
793
793
  lastMonitorError = null;
794
794
  const matchingRuns = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).filter((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
795
- const failedMatch = matchingRuns.find((run) => run.status === "completed" && run.conclusion !== "success");
796
- if (failedMatch?.id) {
797
- const failedJobs = await listWorkflowJobsForProgress(client, owner, name, failedMatch.id);
798
- emitProgress("completed", failedMatch, failedJobs);
799
- return {
800
- status: "completed",
801
- repository: `${owner}/${name}`,
802
- workflow,
803
- runId: failedMatch.id,
804
- headSha: failedMatch.headSha,
805
- branch: failedMatch.headBranch,
806
- createdAt: failedMatch.createdAt,
807
- updatedAt: failedMatch.updatedAt,
808
- conclusion: failedMatch.conclusion,
809
- url: failedMatch.url,
810
- jobs: failedJobs,
811
- failedJobs: failedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
812
- };
813
- }
814
795
  const match = matchingRuns[0];
815
796
  if (!match?.id) {
816
797
  emitProgress("waiting");
@@ -1,6 +1,7 @@
1
1
  import { relative, resolve } from "node:path";
2
2
  import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues } from "./config-runtime.js";
3
3
  import { configuredRailwayServices } from "./railway-deploy.js";
4
+ import { isApiRailwaySourcePolicyService, isImmutableRailwayImageRef } from "./railway-source-policy.js";
4
5
  import { loadTreeseedPlatformConfig } from "../../platform/config.js";
5
6
  import { discoverTreeseedApplications } from "../../hosting/apps.js";
6
7
  const RAILWAY_SECRET_KEYS_BY_SERVICE = {
@@ -221,6 +222,21 @@ function collectTreeseedHostedServiceChecks(options) {
221
222
  const serviceType = serviceTypeFor(service.key);
222
223
  const observed = observedFor(options, service.serviceName);
223
224
  const expectedRootDirectory = railwayServiceRootDirectory(tenantRoot, service);
225
+ const apiSourcePolicyService = isApiRailwaySourcePolicyService(service);
226
+ if (target === "prod" && apiSourcePolicyService && railwayServiceUsesImageSource(service)) {
227
+ checks.push(check({
228
+ id: `railway:${service.instanceKey}:image-ref-policy`,
229
+ provider: "railway",
230
+ serviceKey: service.key,
231
+ serviceType,
232
+ target,
233
+ description: `Railway ${service.serviceName} uses an immutable production image reference.`,
234
+ expected: { imageRef: "<released-image>:<immutable-tag>" },
235
+ observed: { imageRef: service.imageRef ?? null },
236
+ status: isImmutableRailwayImageRef(service.imageRef) ? "passed" : "failed",
237
+ issues: isImmutableRailwayImageRef(service.imageRef) ? [] : [`Production API Railway service ${service.serviceName} is missing an immutable image ref.`]
238
+ }));
239
+ }
224
240
  checks.push(check({
225
241
  id: `railway:${service.instanceKey}:service`,
226
242
  provider: "railway",
@@ -261,7 +277,7 @@ function collectTreeseedHostedServiceChecks(options) {
261
277
  const actualRepo = observed?.deploymentRepo ?? null;
262
278
  const actualBranch = observed?.deploymentBranch ?? null;
263
279
  const actualRootDirectory = observed?.deploymentRootDirectory ?? null;
264
- const sourceUploadDeployment = target === "staging" && actualRepo === null && actualBranch === null && observed?.deploymentHealthy === true;
280
+ const sourceUploadDeployment = target === "staging" && !apiSourcePolicyService && actualRepo === null && actualBranch === null && observed?.deploymentHealthy === true;
265
281
  checks.push(check({
266
282
  id: `railway:${service.instanceKey}:deployment-repo`,
267
283
  provider: "railway",
@@ -12,6 +12,10 @@ export interface TreeseedLiveHostedServiceCheckOptions {
12
12
  attempts: number;
13
13
  intervalMs: number;
14
14
  };
15
+ httpRetry?: {
16
+ attempts: number;
17
+ intervalMs: number;
18
+ };
15
19
  env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
16
20
  fetchImpl?: typeof fetch;
17
21
  }
@@ -19,7 +19,7 @@ import {
19
19
  } from "./hosted-service-checks.js";
20
20
  const DEFAULT_RETRY_ATTEMPTS = 3;
21
21
  const DEFAULT_RETRY_INTERVAL_MS = 1500;
22
- const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 60;
22
+ const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 120;
23
23
  const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_INTERVAL_MS = 5e3;
24
24
  function sleep(ms) {
25
25
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -46,10 +46,11 @@ function pagesBranchName(config, target) {
46
46
  return typeof pages[key] === "string" && pages[key].trim() ? pages[key].trim() : fallback;
47
47
  }
48
48
  async function observeHttp(url, options) {
49
- const attempts = Math.max(1, Math.floor(options.retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS));
50
- const intervalMs = Math.max(0, Math.floor(options.retry?.intervalMs ?? DEFAULT_RETRY_INTERVAL_MS));
49
+ const attempts = Math.max(1, Math.floor(options.httpRetry?.attempts ?? options.retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS));
50
+ const intervalMs = Math.max(0, Math.floor(options.httpRetry?.intervalMs ?? options.retry?.intervalMs ?? DEFAULT_RETRY_INTERVAL_MS));
51
51
  const timeoutMs = Math.max(1e3, Math.floor(options.timeoutMs ?? 1e4));
52
52
  let lastError = "";
53
+ let lastResponse = null;
53
54
  for (let attempt = 0; attempt < attempts; attempt += 1) {
54
55
  const controller = new AbortController();
55
56
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -60,14 +61,16 @@ async function observeHttp(url, options) {
60
61
  signal: controller.signal
61
62
  });
62
63
  clearTimeout(timeout);
63
- return { status: response.status, ok: response.ok };
64
+ lastResponse = { status: response.status, ok: response.ok };
65
+ if (response.ok) return lastResponse;
64
66
  } catch (error) {
65
67
  clearTimeout(timeout);
66
68
  lastError = error instanceof Error ? error.message : String(error);
67
69
  if (attempt + 1 < attempts) await sleep(intervalMs);
68
70
  }
71
+ if (attempt + 1 < attempts) await sleep(intervalMs);
69
72
  }
70
- return { ok: false, error: lastError || "HTTP request failed." };
73
+ return lastResponse ?? { ok: false, error: lastError || "HTTP request failed." };
71
74
  }
72
75
  async function inspectRailwayServiceDeploymentHealthWithRetry(input) {
73
76
  const attempts = Math.max(1, Math.floor(input.options.retry?.attempts ?? DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS));
@@ -44,15 +44,11 @@ function runTreeseedLocalCleanup(input) {
44
44
  ".treeseed/cache",
45
45
  ".treeseed/scenes/runs",
46
46
  ".treeseed/scenes/matrix",
47
- ".treeseed/scenes/render",
48
- ".treeseed/guarantees/runs",
49
- ".treeseed/guarantees/release",
50
- ".treeseed/generated/release-evidence",
51
- ".treeseed/release/evidence"
47
+ ".treeseed/scenes/render"
52
48
  ] : [".treeseed/tmp", ".treeseed/cache", ".treeseed/scenes/render"];
53
49
  for (const target of directoryTargets) actions.push(removeDirectory(root, target));
54
- if (input.docker !== false && mode === "aggressive") actions.push(runCleanupCommand("docker-system-prune", "docker", ["docker", "system", "prune", "--all", "--volumes", "--force"], root));
55
- if (input.npmCache !== false) actions.push(runCleanupCommand("npm-cache-clean", "npm-cache", ["npm", "cache", "clean", "--force"], root));
50
+ if (input.docker === true && mode === "aggressive") actions.push(runCleanupCommand("docker-system-prune", "docker", ["docker", "system", "prune", "--all", "--volumes", "--force"], root));
51
+ if (input.npmCache === true) actions.push(runCleanupCommand("npm-cache-clean", "npm-cache", ["npm", "cache", "clean", "--force"], root));
56
52
  const afterBytes = directoryBytes(join(root, ".treeseed"));
57
53
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
58
54
  return { ok: actions.every((entry) => entry.status !== "failed"), mode, root, startedAt, completedAt, beforeBytes, afterBytes, reclaimedBytes: Math.max(0, beforeBytes - afterBytes), actions };
@@ -31,6 +31,13 @@ export type TreeseedPackageAdapter = {
31
31
  role?: string | null;
32
32
  architectures?: string[];
33
33
  }>;
34
+ capabilities: {
35
+ save: boolean;
36
+ verify: boolean;
37
+ publish: boolean;
38
+ deploy: boolean;
39
+ localOnly: boolean;
40
+ };
34
41
  releaseChecks: Array<{
35
42
  kind: 'npm-pack-plan' | 'github-workflow' | 'docker-manifest';
36
43
  name: string;
@@ -139,6 +146,13 @@ export declare function packageAdapterPlanSummary(root?: any): {
139
146
  role?: string | null;
140
147
  architectures?: string[];
141
148
  }[];
149
+ capabilities: {
150
+ save: boolean;
151
+ verify: boolean;
152
+ publish: boolean;
153
+ deploy: boolean;
154
+ localOnly: boolean;
155
+ };
142
156
  releaseChecks: {
143
157
  kind: "npm-pack-plan" | "github-workflow" | "docker-manifest";
144
158
  name: string;
@@ -110,12 +110,17 @@ function nodeTypeScriptAdapter(pkg) {
110
110
  const dockerImages = stringRecord(manifest?.dockerImages);
111
111
  const dockerImageReleaseWorkflow = stringValue(dockerImages.releaseWorkflow);
112
112
  const dockerImageArchitectures = stringArray(dockerImages.architectures);
113
+ const releaseGateRecord = stringRecord(manifest?.releaseGate);
114
+ const hostedVerifyTimeoutSeconds = positiveIntegerValue(releaseGateRecord.timeoutSeconds) ?? positiveIntegerValue(manifest?.hostedVerifyTimeoutSeconds);
113
115
  const publishTargetRaw = stringValue(manifest?.publishTarget);
114
116
  const publishTarget = dockerArtifacts.length > 0 && publishTargetRaw === "docker" ? dockerArtifacts[0].name : publishTargetRaw ?? "npm";
115
- const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(stringRecord(manifest?.releaseGate).workflow) ?? (existsSync(resolve(pkg.dir, ".github/workflows/deploy.yml")) ? "deploy.yml" : null);
117
+ const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(releaseGateRecord.workflow) ?? (existsSync(resolve(pkg.dir, ".github/workflows/deploy.yml")) ? "deploy.yml" : null);
116
118
  const projectArchitecture = normalizeTreeseedPackageProjectArchitecture(manifest?.projectArchitecture, id);
117
119
  const docsReadiness = docsSiteReadiness(pkg.dir, projectArchitecture);
118
120
  const verifyLocal = typeof scripts["verify:local"] === "string" ? "verify:local" : typeof scripts.verify === "string" ? "verify" : typeof scripts["verify:action"] === "string" ? "verify:action" : null;
121
+ const capabilityRecord = stringRecord(manifest?.capabilities);
122
+ const localOnly = capabilityRecord.localOnly === true;
123
+ const packageType = stringValue(manifest?.type);
119
124
  return {
120
125
  id,
121
126
  name,
@@ -140,6 +145,13 @@ function nodeTypeScriptAdapter(pkg) {
140
145
  role: artifact.role,
141
146
  architectures: artifact.architectures
142
147
  })) : [{ provider: "npm", name: String(pkg.name) }],
148
+ capabilities: {
149
+ save: capabilityRecord.save !== false,
150
+ verify: capabilityRecord.verify !== false,
151
+ publish: capabilityRecord.publish !== false,
152
+ deploy: !localOnly && (capabilityRecord.deploy === true || packageType === "hosted-service" || packageType === "hosted-app"),
153
+ localOnly
154
+ },
143
155
  releaseChecks: [
144
156
  { kind: "github-workflow", name: "publish workflow", detail: dockerImageReleaseWorkflow ? `.github/workflows/${dockerImageReleaseWorkflow}` : ".github/workflows/publish.yml" },
145
157
  ...dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({ kind: "docker-manifest", name: `${artifact.name} Docker image manifest`, detail: `${artifact.name}:<version>` })) : [{ kind: "npm-pack-plan", name: "npm pack", detail: "npm pack --plan" }]
@@ -153,7 +165,7 @@ function nodeTypeScriptAdapter(pkg) {
153
165
  dockerImageArchitectures,
154
166
  imageHosting: stringRecord(dockerImages.hosting)
155
167
  } : {},
156
- type: stringValue(manifest?.type) ?? null,
168
+ type: packageType ?? null,
157
169
  githubEnvironments: stringArray(manifest?.githubEnvironments),
158
170
  requiredSecrets: stringArray(manifest?.requiredSecrets),
159
171
  requiredVariables: stringArray(manifest?.requiredVariables),
@@ -168,6 +180,7 @@ function nodeTypeScriptAdapter(pkg) {
168
180
  ...hostedVerifyWorkflow ? {
169
181
  hostedVerifyWorkflow: hostedVerifyWorkflow.startsWith(".github/workflows/") ? hostedVerifyWorkflow : `.github/workflows/${hostedVerifyWorkflow}`
170
182
  } : {},
183
+ ...hostedVerifyTimeoutSeconds ? { hostedVerifyTimeoutSeconds } : {},
171
184
  scripts
172
185
  }
173
186
  };
@@ -189,6 +202,10 @@ function stringRecord(value) {
189
202
  function stringValue(value) {
190
203
  return typeof value === "string" && value.trim() ? value.trim() : null;
191
204
  }
205
+ function positiveIntegerValue(value) {
206
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value.trim()) : NaN;
207
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
208
+ }
192
209
  function stringArray(value) {
193
210
  return Array.isArray(value) ? value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry)) : [];
194
211
  }
@@ -225,10 +242,14 @@ function beamPackageAdapter(root, dir) {
225
242
  const dockerImages = stringRecord(manifest?.dockerImages);
226
243
  const dockerImageReleaseWorkflow = stringValue(dockerImages.releaseWorkflow);
227
244
  const dockerImageArchitectures = stringArray(dockerImages.architectures);
245
+ const releaseGateRecord = stringRecord(manifest?.releaseGate);
246
+ const hostedVerifyTimeoutSeconds = positiveIntegerValue(releaseGateRecord.timeoutSeconds) ?? positiveIntegerValue(manifest?.hostedVerifyTimeoutSeconds);
228
247
  const repository = stringValue(manifest?.repository) ?? (id === "treedx" ? "treeseed-ai/treedx" : null);
229
- const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(stringRecord(manifest?.releaseGate).workflow) ?? (existsSync(resolve(dir, ".github/workflows/release-gate.yml")) ? "release-gate.yml" : null);
248
+ const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(releaseGateRecord.workflow) ?? (existsSync(resolve(dir, ".github/workflows/release-gate.yml")) ? "release-gate.yml" : null);
230
249
  const projectArchitecture = normalizeTreeseedPackageProjectArchitecture(manifest?.projectArchitecture, id);
231
250
  const docsReadiness = docsSiteReadiness(dir, projectArchitecture);
251
+ const capabilityRecord = stringRecord(manifest?.capabilities);
252
+ const localOnly = capabilityRecord.localOnly === true;
232
253
  const verify = manifest?.verify && typeof manifest.verify === "object" && !Array.isArray(manifest.verify) ? manifest.verify : {};
233
254
  const fast = verify.fast ?? (existsSync(resolve(dir, "scripts/test-treedx-fast.sh")) ? "scripts/test-treedx-fast.sh" : null);
234
255
  const local = verify.local ?? (existsSync(resolve(dir, "scripts/test-all.sh")) ? "scripts/test-all.sh" : null);
@@ -269,6 +290,13 @@ function beamPackageAdapter(root, dir) {
269
290
  role: id,
270
291
  architectures: dockerImageArchitectures
271
292
  }] : [],
293
+ capabilities: {
294
+ save: capabilityRecord.save !== false,
295
+ verify: capabilityRecord.verify !== false,
296
+ publish: capabilityRecord.publish !== false,
297
+ deploy: !localOnly && capabilityRecord.deploy !== false,
298
+ localOnly
299
+ },
272
300
  releaseChecks: dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({ kind: "docker-manifest", name: `${artifact.name} Docker image manifest`, detail: `${artifact.name}:${version ?? "<version>"}` })) : image ? [{ kind: "docker-manifest", name: "Docker image manifest", detail: `${image}:${version ?? "<version>"}` }] : [],
273
301
  metadata: {
274
302
  hasCargo: existsSync(resolve(dir, "Cargo.toml")),
@@ -282,6 +310,7 @@ function beamPackageAdapter(root, dir) {
282
310
  ...hostedVerifyWorkflow ? {
283
311
  hostedVerifyWorkflow: hostedVerifyWorkflow.startsWith(".github/workflows/") ? hostedVerifyWorkflow : `.github/workflows/${hostedVerifyWorkflow}`
284
312
  } : {},
313
+ ...hostedVerifyTimeoutSeconds ? { hostedVerifyTimeoutSeconds } : {},
285
314
  type: stringValue(manifest?.type) ?? null,
286
315
  githubEnvironments: stringArray(manifest?.githubEnvironments),
287
316
  requiredSecrets: stringArray(manifest?.requiredSecrets),
@@ -456,6 +485,7 @@ function packageAdapterPlanSummary(root = workspaceRoot()) {
456
485
  command ? `${command.command} ${command.args.join(" ")}` : null
457
486
  ])),
458
487
  artifacts: adapter.artifacts,
488
+ capabilities: adapter.capabilities,
459
489
  releaseChecks: adapter.releaseChecks,
460
490
  metadata: adapter.metadata
461
491
  }));
@@ -561,6 +591,9 @@ function validateTreeseedPackageManifests(root = workspaceRoot()) {
561
591
  if (stringValue(deploymentSource.staging) !== "git") {
562
592
  errors.push("docker package must declare deploymentSource.staging: git");
563
593
  }
594
+ if (adapter.capabilities.localOnly && adapter.capabilities.deploy) {
595
+ errors.push("local-only package cannot declare hosted deployment capability");
596
+ }
564
597
  if (stringValue(deploymentSource.prod) !== "image") {
565
598
  errors.push("docker package must declare deploymentSource.prod: image");
566
599
  }
@@ -0,0 +1,37 @@
1
+ export type TreeseedPackageArtifactManifest = {
2
+ schemaVersion: 1;
3
+ kind: 'treeseed.package-artifact';
4
+ packageName: string;
5
+ packageVersion: string;
6
+ sourceSha: string;
7
+ createdAt: string;
8
+ file: string;
9
+ sha256: string;
10
+ size: number;
11
+ };
12
+ export declare function buildTreeseedPackageArtifact(input: {
13
+ packageRoot: string;
14
+ outputDir: string;
15
+ }): {
16
+ artifactPath: string;
17
+ manifestPath: string;
18
+ manifest: TreeseedPackageArtifactManifest;
19
+ };
20
+ export declare function verifyTreeseedPackageArtifact(input: {
21
+ manifestPath: string;
22
+ artifactPath?: string;
23
+ }): {
24
+ ok: true;
25
+ artifactPath: string;
26
+ manifestPath: string;
27
+ manifest: TreeseedPackageArtifactManifest;
28
+ };
29
+ export declare function hydrateTreeseedPackageArtifacts(input: {
30
+ artifactsRoot: string;
31
+ projectRoot: string;
32
+ }): {
33
+ ok: true;
34
+ projectRoot: string;
35
+ artifactsRoot: string;
36
+ packages: TreeseedPackageArtifactManifest[];
37
+ };