@treeseed/sdk 0.5.2 → 0.6.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 (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +465 -142
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2093 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. package/package.json +7 -1
@@ -13,20 +13,36 @@ import {
13
13
  } from "../../platform/environment.js";
14
14
  import { loadTreeseedManifest } from "../../platform/tenant-config.js";
15
15
  import {
16
+ buildProvisioningSummary,
16
17
  createPersistentDeployTarget,
17
18
  ensureGeneratedWranglerConfig,
18
19
  loadDeployState,
19
- markDeploymentInitialized,
20
- provisionCloudflareResources,
21
- syncCloudflareSecrets,
22
- verifyProvisionedCloudflareResources
20
+ syncCloudflareSecrets
23
21
  } from "./deploy.js";
22
+ import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
24
23
  import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
25
- import { validateRailwayDeployPrerequisites } from "./railway-deploy.js";
24
+ import {
25
+ buildRailwayCommandEnv
26
+ } from "./railway-deploy.js";
27
+ import {
28
+ normalizeRailwayEnvironmentName,
29
+ resolveRailwayWorkspace
30
+ } from "./railway-api.js";
31
+ import {
32
+ createGitHubApiClient,
33
+ ensureGitHubBranchFromBase,
34
+ listGitHubRepositorySecretNames,
35
+ listGitHubRepositoryVariableNames,
36
+ upsertGitHubRepositorySecret,
37
+ upsertGitHubRepositoryVariable,
38
+ upsertGitHubRepositoryVariableWithGhCli
39
+ } from "./github-api.js";
26
40
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
41
+ import { PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
27
42
  import {
28
43
  assertTreeseedKeyAgentResponse,
29
44
  getTreeseedKeyAgentPaths,
45
+ inspectTreeseedKeyAgentDiagnostics,
30
46
  readWrappedMachineKeyFile,
31
47
  replaceWrappedMachineKey,
32
48
  rotateWrappedMachineKeyPassphrase,
@@ -55,6 +71,30 @@ const CLI_CHECK_TIMEOUT_MS = 5e3;
55
71
  const DEPRECATED_LOCAL_ENV_FILES = [".env.local", ".dev.vars"];
56
72
  const warnedDeprecatedLocalEnvRoots = /* @__PURE__ */ new Set();
57
73
  const inlineTreeseedSecretSessions = /* @__PURE__ */ new Map();
74
+ const railwayConnectionCheckCache = /* @__PURE__ */ new Map();
75
+ function filterEnvironmentValuesByRegistry(values, registry) {
76
+ const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
77
+ return Object.fromEntries(
78
+ Object.entries(values).filter(([key]) => registeredKeys.has(key))
79
+ );
80
+ }
81
+ function inspectTreeseedPassphraseEnvDiagnostic(env = process.env) {
82
+ const configured = typeof env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] === "string" && env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV].trim().length > 0;
83
+ return {
84
+ envVar: TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
85
+ configured,
86
+ recommendedLaunch: `Export ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} in a shell and launch \`code .\` from that shell before starting the Codex session.`
87
+ };
88
+ }
89
+ async function inspectTreeseedKeyAgentTransportDiagnostic() {
90
+ const { socketPath, pidPath } = getTreeseedKeyAgentPaths();
91
+ const diagnostics = await inspectTreeseedKeyAgentDiagnostics(socketPath);
92
+ return {
93
+ socketPath,
94
+ pidPath,
95
+ ...diagnostics
96
+ };
97
+ }
58
98
  function createDefaultRemoteHost() {
59
99
  return {
60
100
  id: "official",
@@ -419,21 +459,26 @@ function unlockTreeseedSecretSessionFromEnv(tenantRoot, options = {}) {
419
459
  startTreeseedKeyAgentDaemon(tenantRoot);
420
460
  let response = { ok: false, code: "daemon_unavailable", message: "Treeseed key agent is unavailable." };
421
461
  for (let attempt = 0; attempt < 20; attempt += 1) {
422
- response = runTreeseedKeyAgentCommand([
423
- "unlock-from-env",
424
- "--key-path",
425
- keyPath,
426
- "--socket-path",
427
- socketPath,
428
- "--idle-timeout-ms",
429
- String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
430
- ...options.allowMigration === false ? [] : ["--allow-migration"],
431
- ...options.createIfMissing === false ? [] : ["--create-if-missing"]
432
- ]);
433
- if (response.code !== "daemon_unavailable") {
434
- break;
462
+ try {
463
+ const parsed = runTreeseedKeyAgentCommand([
464
+ "unlock-from-env",
465
+ "--key-path",
466
+ keyPath,
467
+ "--socket-path",
468
+ socketPath,
469
+ "--idle-timeout-ms",
470
+ String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
471
+ ...options.allowMigration === false ? [] : ["--allow-migration"],
472
+ ...options.createIfMissing === false ? [] : ["--create-if-missing"]
473
+ ]);
474
+ assertTreeseedKeyAgentResponse(parsed, `Unable to unlock the Treeseed secret session from ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV}.`);
475
+ return parsed.status;
476
+ } catch (error) {
477
+ if (attempt === 19) {
478
+ throw error;
479
+ }
480
+ sleepMs(25);
435
481
  }
436
- sleepMs(25);
437
482
  }
438
483
  assertTreeseedKeyAgentResponse(
439
484
  response,
@@ -1298,11 +1343,7 @@ function resolveTreeseedMachineEnvironmentValues(tenantRoot, scope) {
1298
1343
  };
1299
1344
  const entryById = new Map(registry.entries.map((entry) => [entry.id, entry]));
1300
1345
  const values = {};
1301
- const knownKeys = /* @__PURE__ */ new Set([
1302
- ...Object.keys(bucketValuesByScope.shared ?? {}),
1303
- ...Object.keys(bucketValuesByScope[scope] ?? {}),
1304
- ...registry.entries.map((entry) => entry.id)
1305
- ]);
1346
+ const knownKeys = new Set(registry.entries.map((entry) => entry.id));
1306
1347
  for (const entryId of knownKeys) {
1307
1348
  const resolved = resolveEntryValueFromBuckets(entryById.get(entryId), entryId, scope, bucketValuesByScope);
1308
1349
  if (typeof resolved === "string" && resolved.length > 0) {
@@ -1349,6 +1390,7 @@ function collectTreeseedEnvironmentContext(tenantRoot) {
1349
1390
  }
1350
1391
  function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
1351
1392
  warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1393
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1352
1394
  let machineValues = {};
1353
1395
  try {
1354
1396
  machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
@@ -1363,17 +1405,22 @@ function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
1363
1405
  normalizedEnv[canonicalKey] = normalizedEnv[legacyKey];
1364
1406
  }
1365
1407
  }
1366
- return {
1408
+ return filterEnvironmentValuesByRegistry({
1367
1409
  ...machineValues,
1368
1410
  ...Object.fromEntries(Object.entries(normalizedEnv).map(([key, value]) => [key, value ?? void 0]))
1369
- };
1411
+ }, registry);
1370
1412
  }
1371
1413
  function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
1372
1414
  warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1415
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1416
+ const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
1373
1417
  const values = {};
1374
1418
  const sources = {};
1375
1419
  const merge = (source, entries) => {
1376
1420
  for (const [key, value] of Object.entries(entries)) {
1421
+ if (!registeredKeys.has(key)) {
1422
+ continue;
1423
+ }
1377
1424
  if (typeof value !== "string" || value.length === 0) {
1378
1425
  continue;
1379
1426
  }
@@ -1470,7 +1517,7 @@ function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
1470
1517
  error.details = report.validation;
1471
1518
  throw error;
1472
1519
  }
1473
- function runGh(args, { cwd, dryRun = false, input } = {}) {
1520
+ function runGh(args, { cwd, dryRun = false, input, env } = {}) {
1474
1521
  if (dryRun) {
1475
1522
  return { status: 0, stdout: "", stderr: "" };
1476
1523
  }
@@ -1478,8 +1525,18 @@ function runGh(args, { cwd, dryRun = false, input } = {}) {
1478
1525
  cwd,
1479
1526
  stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
1480
1527
  encoding: "utf8",
1481
- input
1528
+ input,
1529
+ timeout: 15e3,
1530
+ env: {
1531
+ ...process.env,
1532
+ ...env ?? {},
1533
+ GH_PROMPT_DISABLED: "1",
1534
+ GH_NO_UPDATE_NOTIFIER: "1"
1535
+ }
1482
1536
  });
1537
+ if (result.error?.code === "ETIMEDOUT") {
1538
+ throw new Error(`gh ${args.join(" ")} timed out`);
1539
+ }
1483
1540
  if (result.status !== 0) {
1484
1541
  throw new Error(result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(" ")} failed`);
1485
1542
  }
@@ -1543,12 +1600,22 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1543
1600
  attemptedInstall: false,
1544
1601
  installedDuringConfig: false
1545
1602
  });
1546
- const wranglerCheck = checkCommand(process.execPath, [resolveWranglerBin(), "--version"], { cwd: tenantRoot, env });
1547
- const wranglerCli = toolStatus(
1548
- "wranglerCli",
1549
- wranglerCheck.ok,
1550
- wranglerCheck.ok ? wranglerCheck.stdout.split("\n")[0] ?? "Wrangler CLI detected." : wranglerCheck.detail || "Wrangler CLI is unavailable."
1551
- );
1603
+ const wranglerCli = (() => {
1604
+ try {
1605
+ const wranglerCheck = checkCommand(process.execPath, [resolveWranglerBin(), "--version"], { cwd: tenantRoot, env });
1606
+ return toolStatus(
1607
+ "wranglerCli",
1608
+ wranglerCheck.ok,
1609
+ wranglerCheck.ok ? wranglerCheck.stdout.split("\n")[0] ?? "Wrangler CLI detected." : wranglerCheck.detail || "Wrangler CLI is unavailable."
1610
+ );
1611
+ } catch (error) {
1612
+ return toolStatus(
1613
+ "wranglerCli",
1614
+ false,
1615
+ error instanceof Error && error.message ? error.message : "Wrangler CLI is unavailable."
1616
+ );
1617
+ }
1618
+ })();
1552
1619
  const railwayCheck = checkCommand("railway", ["--version"], { cwd: tenantRoot, env });
1553
1620
  const railwayCli = toolStatus(
1554
1621
  "railwayCli",
@@ -1627,6 +1694,9 @@ function providerConnectionResult(provider, ready, detail, extra = {}) {
1627
1694
  ...extra
1628
1695
  };
1629
1696
  }
1697
+ function isTransientProviderConnectionError(detail) {
1698
+ return /fetch failed|failed to fetch|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|api check failed|rate.?limit|too many requests|429/iu.test(detail || "");
1699
+ }
1630
1700
  function checkGitHubConnection({ tenantRoot, env }) {
1631
1701
  if (!env.GH_TOKEN) {
1632
1702
  return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
@@ -1636,76 +1706,127 @@ function checkGitHubConnection({ tenantRoot, env }) {
1636
1706
  }
1637
1707
  const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1638
1708
  const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
1639
- const result = spawnSync("gh", args, {
1640
- cwd: tenantRoot,
1641
- stdio: "pipe",
1642
- encoding: "utf8",
1643
- env: { ...process.env, ...env },
1644
- timeout: CLI_CHECK_TIMEOUT_MS
1645
- });
1646
- if (result.status !== 0) {
1647
- return providerConnectionResult("github", false, formatCheckOutput(result) || "GitHub API check failed.");
1648
- }
1649
- const resolved = result.stdout.trim();
1650
- return providerConnectionResult(
1651
- "github",
1652
- true,
1653
- repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
1654
- );
1655
- }
1656
- function checkCloudflareConnection({ tenantRoot, env }) {
1657
- if (!env.CLOUDFLARE_API_TOKEN) {
1658
- return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
1659
- }
1660
- try {
1661
- const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
1709
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1710
+ const result = spawnSync("gh", args, {
1662
1711
  cwd: tenantRoot,
1663
1712
  stdio: "pipe",
1664
1713
  encoding: "utf8",
1665
1714
  env: { ...process.env, ...env },
1666
1715
  timeout: CLI_CHECK_TIMEOUT_MS
1667
1716
  });
1668
- if (result.status !== 0) {
1669
- return providerConnectionResult("cloudflare", false, formatCheckOutput(result) || "Cloudflare Wrangler check failed.");
1717
+ if (result.status === 0) {
1718
+ const resolved = result.stdout.trim();
1719
+ return providerConnectionResult(
1720
+ "github",
1721
+ true,
1722
+ repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
1723
+ );
1724
+ }
1725
+ const detail = formatCheckOutput(result) || "GitHub API check failed.";
1726
+ if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1727
+ return providerConnectionResult("github", false, detail);
1670
1728
  }
1671
- return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
1672
- } catch (error) {
1673
- return providerConnectionResult("cloudflare", false, error instanceof Error ? error.message : "Cloudflare Wrangler check failed.");
1674
1729
  }
1730
+ return providerConnectionResult("github", false, "GitHub API check failed.");
1675
1731
  }
1676
- function checkRailwayConnection({ tenantRoot, env }) {
1677
- if (!env.RAILWAY_API_TOKEN && !env.RAILWAY_TOKEN) {
1678
- return providerConnectionResult("railway", false, "RAILWAY_API_TOKEN or RAILWAY_TOKEN is not configured.", { skipped: true });
1732
+ function checkCloudflareConnection({ tenantRoot, env }) {
1733
+ if (!env.CLOUDFLARE_API_TOKEN) {
1734
+ return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
1679
1735
  }
1680
- if (!commandAvailable("railway")) {
1681
- return providerConnectionResult("railway", false, "Railway CLI `railway` is not installed.");
1736
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1737
+ try {
1738
+ const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
1739
+ cwd: tenantRoot,
1740
+ stdio: "pipe",
1741
+ encoding: "utf8",
1742
+ env: { ...process.env, ...env },
1743
+ timeout: CLI_CHECK_TIMEOUT_MS
1744
+ });
1745
+ if (result.status === 0) {
1746
+ return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
1747
+ }
1748
+ const detail = formatCheckOutput(result) || "Cloudflare Wrangler check failed.";
1749
+ if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1750
+ return providerConnectionResult("cloudflare", false, detail);
1751
+ }
1752
+ } catch (error) {
1753
+ const detail = error instanceof Error ? error.message : "Cloudflare Wrangler check failed.";
1754
+ if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1755
+ return providerConnectionResult("cloudflare", false, detail);
1756
+ }
1757
+ }
1682
1758
  }
1683
- const result = spawnSync("railway", ["whoami"], {
1684
- cwd: tenantRoot,
1685
- stdio: "pipe",
1686
- encoding: "utf8",
1687
- env: { ...process.env, ...env },
1688
- timeout: CLI_CHECK_TIMEOUT_MS
1759
+ return providerConnectionResult(
1760
+ "cloudflare",
1761
+ false,
1762
+ "Cloudflare connectivity preflight hit transient fetch failures; bootstrap will continue and rely on live reconcile verification.",
1763
+ { skipped: true, warning: true, transient: true }
1764
+ );
1765
+ }
1766
+ async function checkRailwayConnection({ tenantRoot, env }) {
1767
+ if (!env.RAILWAY_API_TOKEN) {
1768
+ return providerConnectionResult("railway", false, "RAILWAY_API_TOKEN is not configured.", { skipped: true });
1769
+ }
1770
+ const workspaceName = env.TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(env);
1771
+ const cacheKey = JSON.stringify({
1772
+ tenantRoot,
1773
+ token: env.RAILWAY_API_TOKEN,
1774
+ workspaceName
1689
1775
  });
1690
- if (result.status !== 0) {
1691
- return providerConnectionResult("railway", false, formatCheckOutput(result) || "Railway CLI check failed.");
1776
+ const cached = railwayConnectionCheckCache.get(cacheKey);
1777
+ if (cached) {
1778
+ return await cached;
1779
+ }
1780
+ const checkPromise = (async () => {
1781
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1782
+ try {
1783
+ const whoami = checkCommand("railway", ["whoami"], { cwd: tenantRoot, env });
1784
+ if (!whoami.ok) {
1785
+ if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
1786
+ return providerConnectionResult(
1787
+ "railway",
1788
+ false,
1789
+ "Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
1790
+ { skipped: true, warning: true, rateLimited: true }
1791
+ );
1792
+ }
1793
+ throw new Error(whoami.detail || "Railway CLI authentication check failed.");
1794
+ }
1795
+ const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
1796
+ return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
1797
+ } catch (error) {
1798
+ const detail = error instanceof Error ? error.message : "Railway API check failed.";
1799
+ if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1800
+ return providerConnectionResult("railway", false, detail);
1801
+ }
1802
+ }
1803
+ }
1804
+ return providerConnectionResult("railway", false, "Railway API check failed.");
1805
+ })();
1806
+ railwayConnectionCheckCache.set(cacheKey, checkPromise);
1807
+ try {
1808
+ return await checkPromise;
1809
+ } catch (error) {
1810
+ railwayConnectionCheckCache.delete(cacheKey);
1811
+ throw error;
1692
1812
  }
1693
- return providerConnectionResult("railway", true, result.stdout.trim() || "Railway CLI check succeeded.");
1694
1813
  }
1695
- function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
1814
+ async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
1696
1815
  const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
1697
- const commandEnv = {
1816
+ const rawCommandEnv = {
1698
1817
  GH_TOKEN: values.GH_TOKEN,
1699
1818
  CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
1700
1819
  CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
1701
1820
  RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
1702
- RAILWAY_TOKEN: values.RAILWAY_TOKEN
1821
+ TREESEED_RAILWAY_WORKSPACE: values.TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(values)
1703
1822
  };
1823
+ const commandEnv = buildRailwayCommandEnv(rawCommandEnv);
1704
1824
  const checks = [
1705
1825
  checkGitHubConnection({ tenantRoot, env: commandEnv }),
1706
- checkCloudflareConnection({ tenantRoot, env: commandEnv }),
1707
- checkRailwayConnection({ tenantRoot, env: commandEnv })
1826
+ checkCloudflareConnection({ tenantRoot, env: commandEnv })
1708
1827
  ];
1828
+ const railwayCheck = await checkRailwayConnection({ tenantRoot, env: commandEnv });
1829
+ checks.push(railwayCheck);
1709
1830
  return {
1710
1831
  scope,
1711
1832
  ok: checks.every((check) => check.ready || check.skipped),
@@ -1722,14 +1843,20 @@ function formatTreeseedProviderConnectionReport(report) {
1722
1843
  }
1723
1844
  return lines.join("\n");
1724
1845
  }
1846
+ function formatTreeseedProviderConnectionFailures(reports) {
1847
+ const failing = reports.filter((report) => report.checks.some((check) => !check.ready && !check.skipped));
1848
+ if (failing.length === 0) {
1849
+ return "";
1850
+ }
1851
+ return [
1852
+ "Treeseed provider connection checks failed.",
1853
+ ...failing.map((report) => formatTreeseedProviderConnectionReport(report))
1854
+ ].join("\n");
1855
+ }
1725
1856
  function writeProviderConnectionReport(write, report) {
1726
1857
  write(formatTreeseedProviderConnectionReport(report));
1727
1858
  }
1728
- function listGitHubNames(command, repository, tenantRoot) {
1729
- const result = runGh([command, "list", "--repo", repository, "--json", "name"], { cwd: tenantRoot });
1730
- return new Set(JSON.parse(result.stdout || "[]").map((entry) => entry?.name).filter(Boolean));
1731
- }
1732
- function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1859
+ async function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1733
1860
  const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1734
1861
  if (!repository) {
1735
1862
  throw new Error("Unable to determine the GitHub repository from the origin remote.");
@@ -1737,8 +1864,14 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
1737
1864
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1738
1865
  const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1739
1866
  const relevant = registry.entries.filter((entry) => entry.scopes.includes(scope));
1740
- const secretNames = listGitHubNames("secret", repository, tenantRoot);
1741
- const variableNames = listGitHubNames("variable", repository, tenantRoot);
1867
+ const ghToken = values.GH_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
1868
+ const ghEnv = ghToken ? {
1869
+ GH_TOKEN: ghToken,
1870
+ GITHUB_TOKEN: ghToken
1871
+ } : {};
1872
+ const githubClient = createGitHubApiClient({ env: ghEnv });
1873
+ const secretNames = await listGitHubRepositorySecretNames(repository, { client: githubClient });
1874
+ const variableNames = await listGitHubRepositoryVariableNames(repository, { client: githubClient });
1742
1875
  const synced = {
1743
1876
  secrets: [],
1744
1877
  variables: []
@@ -1749,11 +1882,23 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
1749
1882
  continue;
1750
1883
  }
1751
1884
  if (entry.targets.includes("github-secret")) {
1752
- runGh(["secret", "set", entry.id, "--repo", repository, "--body", value], { cwd: tenantRoot, dryRun });
1885
+ if (!dryRun) {
1886
+ await upsertGitHubRepositorySecret(repository, entry.id, value, { client: githubClient });
1887
+ }
1753
1888
  synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
1754
1889
  }
1755
1890
  if (entry.targets.includes("github-variable")) {
1756
- runGh(["variable", "set", entry.id, "--repo", repository, "--body", value], { cwd: tenantRoot, dryRun });
1891
+ if (!dryRun) {
1892
+ try {
1893
+ upsertGitHubRepositoryVariableWithGhCli(repository, entry.id, value, { env: ghEnv });
1894
+ } catch (error) {
1895
+ const message = error instanceof Error ? error.message : String(error ?? "");
1896
+ if (!/not found|ENOENT|timed out|timeout|aborted|gh api exited/iu.test(message)) {
1897
+ throw error;
1898
+ }
1899
+ await upsertGitHubRepositoryVariable(repository, entry.id, value, { client: githubClient });
1900
+ }
1901
+ }
1757
1902
  synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
1758
1903
  }
1759
1904
  }
@@ -1805,7 +1950,7 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
1805
1950
  serviceId: service.railway?.serviceId ?? "",
1806
1951
  rootDir: resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir),
1807
1952
  baseUrl: environment?.baseUrl ?? service.publicBaseUrl ?? "(unset)",
1808
- environmentName: environment?.railwayEnvironment ?? scope,
1953
+ environmentName: normalizeRailwayEnvironmentName(environment?.railwayEnvironment ?? scope),
1809
1954
  secrets: railwaySecretNames,
1810
1955
  variables: railwayVariableNames,
1811
1956
  dryRun
@@ -1830,23 +1975,22 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
1830
1975
  services
1831
1976
  };
1832
1977
  }
1833
- function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1978
+ async function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1834
1979
  const normalizedScope = scope === "prod" ? "prod" : scope;
1835
1980
  const target = createPersistentDeployTarget(normalizedScope);
1836
- const summary = provisionCloudflareResources(tenantRoot, { dryRun, target });
1837
- ensureGeneratedWranglerConfig(tenantRoot, { target });
1838
- const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
1839
- if (!dryRun) {
1840
- markDeploymentInitialized(tenantRoot, { target });
1841
- }
1981
+ const summary = await reconcileTreeseedTarget({
1982
+ tenantRoot,
1983
+ target,
1984
+ env: process.env
1985
+ });
1842
1986
  return {
1843
1987
  scope: normalizedScope,
1844
1988
  target,
1845
1989
  summary,
1846
- secrets: syncedSecrets
1990
+ secrets: summary.results.filter((result) => result.unit.provider === "cloudflare").flatMap((result) => Object.keys(result.resourceLocators ?? {}))
1847
1991
  };
1848
1992
  }
1849
- function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks) {
1993
+ async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true } = {}) {
1850
1994
  const validationProblems = [...validation.missing, ...validation.invalid];
1851
1995
  const validationBlockers = validationProblems.map((problem) => problem.message);
1852
1996
  const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
@@ -1888,37 +2032,105 @@ function summarizePersistentReadiness(tenantRoot, scope, validation, connectionC
1888
2032
  }
1889
2033
  };
1890
2034
  }
1891
- const cloudflare = verifyProvisionedCloudflareResources(tenantRoot, { scope });
1892
- let railwayReady = true;
1893
- let railwayIssue = null;
1894
- try {
1895
- validateRailwayDeployPrerequisites(tenantRoot, scope);
1896
- } catch (error) {
1897
- railwayReady = false;
1898
- railwayIssue = error instanceof Error ? error.message : String(error);
1899
- }
1900
2035
  const configured = validation.ok;
1901
- const provisioned = cloudflare.ok && railwayReady;
1902
- const deployable = configured && provisioned && connectionReady;
1903
- const blockers = [
1904
- ...connectionIssues,
1905
- ...railwayIssue ? [railwayIssue] : []
1906
- ];
1907
- if (!cloudflare.ok) {
1908
- blockers.push("Cloudflare foundational resources have not been fully provisioned yet.");
2036
+ if (!includeReconcileStatus) {
2037
+ return {
2038
+ configured,
2039
+ provisioned: false,
2040
+ deployable: false,
2041
+ phase: "config_complete",
2042
+ blockers: [...connectionIssues],
2043
+ warnings: connectionWarnings,
2044
+ checks: {
2045
+ validation: validation.ok,
2046
+ connections: connectionReady,
2047
+ reconcile: "deferred"
2048
+ }
2049
+ };
1909
2050
  }
2051
+ const reconcile = await collectTreeseedReconcileStatus({
2052
+ tenantRoot,
2053
+ target: createPersistentDeployTarget(scope),
2054
+ env
2055
+ });
2056
+ const provisioned = reconcile.ready;
2057
+ const deployable = configured && provisioned && connectionReady;
2058
+ const blockers = [...connectionIssues, ...reconcile.blockers];
1910
2059
  return {
1911
2060
  configured,
1912
2061
  provisioned,
1913
2062
  deployable,
1914
2063
  phase: provisioned ? "provisioned" : "config_complete",
1915
2064
  blockers,
1916
- warnings: connectionWarnings,
2065
+ warnings: [...connectionWarnings, ...reconcile.warnings],
1917
2066
  checks: {
1918
2067
  validation: validation.ok,
1919
2068
  connections: connectionReady,
1920
- cloudflare: cloudflare.checks,
1921
- railway: railwayReady
2069
+ reconcile: reconcile.units
2070
+ }
2071
+ };
2072
+ }
2073
+ function summarizeReconciledPersistentReadiness(scope, validation, connectionChecks, reconciled) {
2074
+ const validationProblems = [...validation.missing, ...validation.invalid];
2075
+ const validationBlockers = validationProblems.map((problem) => problem.message);
2076
+ const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
2077
+ const connectionIssues = connectionChecks.filter((check) => !check.ready && !check.skipped).map((check) => `${check.provider}: ${check.detail}`);
2078
+ const connectionWarnings = connectionChecks.filter((check) => check.skipped).map((check) => `${check.provider}: ${check.detail}`);
2079
+ if (scope === "local") {
2080
+ return {
2081
+ configured: validation.ok,
2082
+ provisioned: true,
2083
+ deployable: validation.ok && connectionReady,
2084
+ phase: validation.ok ? "code_ready" : "config_incomplete",
2085
+ blockers: [
2086
+ ...validationBlockers,
2087
+ ...connectionIssues
2088
+ ],
2089
+ warnings: connectionWarnings,
2090
+ checks: {
2091
+ validation: validation.ok,
2092
+ connections: connectionReady
2093
+ }
2094
+ };
2095
+ }
2096
+ if (!validation.ok) {
2097
+ return {
2098
+ configured: false,
2099
+ provisioned: false,
2100
+ deployable: false,
2101
+ phase: "config_incomplete",
2102
+ blockers: [
2103
+ ...validationBlockers,
2104
+ ...connectionIssues
2105
+ ],
2106
+ warnings: connectionWarnings,
2107
+ checks: {
2108
+ validation: false,
2109
+ connections: connectionReady,
2110
+ reconcile: []
2111
+ }
2112
+ };
2113
+ }
2114
+ const actions = reconciled?.actions ?? [];
2115
+ const blockers = actions.filter((action) => action.verified !== true).flatMap((action) => [
2116
+ ...action.missing.map((entry) => `${action.provider}:${action.unitType}: ${entry}`),
2117
+ ...action.drifted.map((entry) => `${action.provider}:${action.unitType}: ${entry}`)
2118
+ ]);
2119
+ const provisioned = blockers.length === 0 && actions.length > 0;
2120
+ return {
2121
+ configured: true,
2122
+ provisioned,
2123
+ deployable: provisioned && connectionReady,
2124
+ phase: provisioned ? "provisioned" : "config_complete",
2125
+ blockers: [
2126
+ ...connectionIssues,
2127
+ ...blockers
2128
+ ],
2129
+ warnings: connectionWarnings,
2130
+ checks: {
2131
+ validation: true,
2132
+ connections: connectionReady,
2133
+ reconcile: actions
1922
2134
  }
1923
2135
  };
1924
2136
  }
@@ -2151,7 +2363,7 @@ function applyTreeseedConfigValues({
2151
2363
  sharedStorageMigrations
2152
2364
  };
2153
2365
  }
2154
- function finalizeTreeseedConfig({
2366
+ async function finalizeTreeseedConfig({
2155
2367
  tenantRoot,
2156
2368
  scopes = [...TREESEED_ENVIRONMENT_SCOPES],
2157
2369
  sync = "all",
@@ -2164,7 +2376,9 @@ function finalizeTreeseedConfig({
2164
2376
  const summary = {
2165
2377
  scopes,
2166
2378
  synced: {},
2167
- initialized: [],
2379
+ reconciled: [],
2380
+ deployed: [],
2381
+ resourceInventoryByScope: {},
2168
2382
  connectionChecks: [],
2169
2383
  validationByScope: {},
2170
2384
  readinessByScope: {}
@@ -2175,9 +2389,24 @@ function finalizeTreeseedConfig({
2175
2389
  }
2176
2390
  };
2177
2391
  progress(`Validating configuration for ${scopes.join(", ")}...`);
2392
+ const scopeSeedValues = Object.fromEntries(
2393
+ scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
2394
+ );
2178
2395
  for (const scope of scopes) {
2396
+ const seedValues = scopeSeedValues[scope];
2397
+ const suggestedValues = getTreeseedEnvironmentSuggestedValues({
2398
+ scope,
2399
+ purpose: "config",
2400
+ deployConfig: registry.context.deployConfig,
2401
+ tenantConfig: registry.context.tenantConfig,
2402
+ plugins: registry.context.plugins,
2403
+ values: seedValues
2404
+ });
2179
2405
  const validation = validateTreeseedEnvironmentValues({
2180
- values: resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
2406
+ values: {
2407
+ ...suggestedValues,
2408
+ ...seedValues
2409
+ },
2181
2410
  scope,
2182
2411
  purpose: "config",
2183
2412
  deployConfig: registry.context.deployConfig,
@@ -2187,21 +2416,37 @@ function finalizeTreeseedConfig({
2187
2416
  summary.validationByScope[scope] = validation;
2188
2417
  if (checkConnections) {
2189
2418
  progress(`Checking provider connectivity for ${scope}...`);
2190
- summary.connectionChecks.push(checkTreeseedProviderConnections({ tenantRoot, scope, env }));
2419
+ summary.connectionChecks.push(await checkTreeseedProviderConnections({ tenantRoot, scope, env: seedValues }));
2191
2420
  }
2192
2421
  }
2193
2422
  for (const scope of scopes) {
2194
- summary.readinessByScope[scope] = summarizePersistentReadiness(
2423
+ if (scope !== "local") {
2424
+ const target = createPersistentDeployTarget(scope);
2425
+ const deployState = loadDeployState(tenantRoot, registry.context.deployConfig, { target });
2426
+ const inventory = buildProvisioningSummary(registry.context.deployConfig, deployState, target);
2427
+ const railwayWorkspace = resolveRailwayWorkspace(scopeSeedValues[scope]);
2428
+ summary.resourceInventoryByScope[scope] = inventory;
2429
+ progress(
2430
+ `Resolved ${scope} resources: deployment=${inventory.identity?.deploymentKey}, pages=${inventory.resources?.pagesProject}, web-domain=${inventory.resources?.webDomain ?? "(none)"}, api-domain=${inventory.resources?.apiDomain ?? "(none)"}, r2=${inventory.resources?.contentBucket}, queue=${inventory.resources?.queue}, d1=${inventory.resources?.database}, railway=${inventory.resources?.railwayProject}, workspace=${railwayWorkspace}.`
2431
+ );
2432
+ }
2433
+ summary.readinessByScope[scope] = await summarizePersistentReadiness(
2195
2434
  tenantRoot,
2196
2435
  scope,
2197
2436
  summary.validationByScope[scope],
2198
- summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
2437
+ summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
2438
+ scopeSeedValues[scope],
2439
+ { includeReconcileStatus: !initializePersistent }
2199
2440
  );
2200
2441
  }
2201
2442
  const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
2202
2443
  if (invalidScopes.length > 0) {
2203
2444
  throw new Error(formatTreeseedConfigValidationFailure(summary.validationByScope, scopes));
2204
2445
  }
2446
+ const failingConnectionReports = summary.connectionChecks.filter((report) => report.ok !== true);
2447
+ if (failingConnectionReports.length > 0) {
2448
+ throw new Error(formatTreeseedProviderConnectionFailures(failingConnectionReports));
2449
+ }
2205
2450
  progress("Syncing managed service settings from treeseed.site.yaml...");
2206
2451
  syncManagedServiceSettingsFromDeployConfig(tenantRoot);
2207
2452
  if (initializePersistent) {
@@ -2209,34 +2454,110 @@ function finalizeTreeseedConfig({
2209
2454
  if (scope === "local") {
2210
2455
  continue;
2211
2456
  }
2212
- progress(`Initializing persistent ${scope} environment resources...`);
2457
+ progress(`Deriving desired units for ${scope}...`);
2458
+ const initialized = await reconcileTreeseedTarget({
2459
+ tenantRoot,
2460
+ target: createPersistentDeployTarget(scope),
2461
+ env: scopeSeedValues[scope],
2462
+ write: progress
2463
+ });
2464
+ summary.reconciled.push({
2465
+ scope,
2466
+ target: scope,
2467
+ units: initialized.units.length,
2468
+ actions: initialized.results.map((result) => ({
2469
+ unitId: result.unit.unitId,
2470
+ unitType: result.unit.unitType,
2471
+ provider: result.unit.provider,
2472
+ action: result.action,
2473
+ verified: result.verification?.verified === true,
2474
+ missing: result.verification?.missing ?? [],
2475
+ drifted: result.verification?.drifted ?? []
2476
+ }))
2477
+ });
2478
+ if (scope === "staging") {
2479
+ progress(`Ensuring ${STAGING_BRANCH} exists on origin from ${PRODUCTION_BRANCH}...`);
2480
+ const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
2481
+ if (!repository) {
2482
+ throw new Error("Unable to determine the GitHub repository from the origin remote for staging branch bootstrap.");
2483
+ }
2484
+ const branchBootstrap = await ensureGitHubBranchFromBase(repository, STAGING_BRANCH, {
2485
+ baseBranch: PRODUCTION_BRANCH,
2486
+ client: createGitHubApiClient({
2487
+ env: scopeSeedValues[scope]
2488
+ })
2489
+ });
2490
+ summary.deployed.push({
2491
+ scope,
2492
+ branchBootstrap,
2493
+ result: {}
2494
+ });
2495
+ }
2496
+ progress(`Deploying ${scope}...`);
2213
2497
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
2214
- const initialized = initializeTreeseedPersistentEnvironment({ tenantRoot, scope });
2215
- summary.initialized.push({
2498
+ process.env.TREESEED_RAILWAY_WORKSPACE = process.env.TREESEED_RAILWAY_WORKSPACE || scopeSeedValues[scope].TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(scopeSeedValues[scope]);
2499
+ const { deployProjectPlatform } = await import("./project-platform.js");
2500
+ const deployResult = await deployProjectPlatform({
2501
+ tenantRoot,
2216
2502
  scope,
2217
- secrets: initialized.secrets.length,
2218
- target: initialized.summary.target
2503
+ skipProvision: true
2219
2504
  });
2505
+ const deployEntry = summary.deployed.find((entry) => entry.scope === scope);
2506
+ if (deployEntry) {
2507
+ deployEntry.result = deployResult;
2508
+ } else {
2509
+ summary.deployed.push({
2510
+ scope,
2511
+ branchBootstrap: null,
2512
+ result: deployResult
2513
+ });
2514
+ }
2515
+ progress(`Re-verifying ${scope} after deployment...`);
2516
+ const finalized = await reconcileTreeseedTarget({
2517
+ tenantRoot,
2518
+ target: createPersistentDeployTarget(scope),
2519
+ env: scopeSeedValues[scope],
2520
+ write: progress
2521
+ });
2522
+ const index = summary.reconciled.findIndex((entry) => entry.scope === scope);
2523
+ const nextSummary = {
2524
+ scope,
2525
+ target: scope,
2526
+ units: finalized.units.length,
2527
+ actions: finalized.results.map((result) => ({
2528
+ unitId: result.unit.unitId,
2529
+ unitType: result.unit.unitType,
2530
+ provider: result.unit.provider,
2531
+ action: result.action,
2532
+ verified: result.verification?.verified === true,
2533
+ missing: result.verification?.missing ?? [],
2534
+ drifted: result.verification?.drifted ?? []
2535
+ }))
2536
+ };
2537
+ if (index >= 0) {
2538
+ summary.reconciled[index] = nextSummary;
2539
+ } else {
2540
+ summary.reconciled.push(nextSummary);
2541
+ }
2220
2542
  }
2221
2543
  }
2222
2544
  if (sync === "github" || sync === "all") {
2223
2545
  progress(`Syncing GitHub environment for ${scopes.at(-1) ?? "prod"}...`);
2224
- summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
2225
- }
2226
- if (sync === "cloudflare" || sync === "all") {
2227
- progress(`Syncing Cloudflare environment for ${scopes.at(-1) ?? "prod"}...`);
2228
- summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
2229
- }
2230
- if (sync === "railway" || sync === "all") {
2231
- progress(`Syncing Railway environment for ${scopes.at(-1) ?? "prod"}...`);
2232
- summary.synced.railway = syncTreeseedRailwayEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
2546
+ summary.synced.github = await syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
2233
2547
  }
2234
2548
  for (const scope of scopes) {
2235
- summary.readinessByScope[scope] = summarizePersistentReadiness(
2549
+ const reconciled = summary.reconciled.find((entry) => entry.scope === scope) ?? null;
2550
+ summary.readinessByScope[scope] = initializePersistent && reconciled ? summarizeReconciledPersistentReadiness(
2551
+ scope,
2552
+ summary.validationByScope[scope],
2553
+ summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
2554
+ reconciled
2555
+ ) : await summarizePersistentReadiness(
2236
2556
  tenantRoot,
2237
2557
  scope,
2238
2558
  summary.validationByScope[scope],
2239
- summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
2559
+ summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
2560
+ env
2240
2561
  );
2241
2562
  }
2242
2563
  return summary;
@@ -2294,6 +2615,8 @@ export {
2294
2615
  getTreeseedRemoteAuthPaths,
2295
2616
  initializeTreeseedPersistentEnvironment,
2296
2617
  inspectTreeseedKeyAgentStatus,
2618
+ inspectTreeseedKeyAgentTransportDiagnostic,
2619
+ inspectTreeseedPassphraseEnvDiagnostic,
2297
2620
  listDeprecatedTreeseedLocalEnvFiles,
2298
2621
  listRelevantTreeseedConfigEntries,
2299
2622
  loadTreeseedMachineConfig,