runcastle 1.0.7 → 1.0.9

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.
package/bin/runcastle.js CHANGED
@@ -27,7 +27,8 @@ __export(exports_paths, {
27
27
  envPath: () => envPath,
28
28
  dbPath: () => dbPath,
29
29
  dataDir: () => dataDir,
30
- configPath: () => configPath
30
+ configPath: () => configPath,
31
+ burnCacheDir: () => burnCacheDir
31
32
  });
32
33
  import { homedir } from "os";
33
34
  import { join } from "path";
@@ -52,6 +53,9 @@ function sessionDir(id) {
52
53
  function worktreeDir(projectId, slug) {
53
54
  return join(dataDir(), "worktrees", projectId, slug);
54
55
  }
56
+ function burnCacheDir(pm) {
57
+ return join(dataDir(), "cache", pm);
58
+ }
55
59
  function sandboxBuildDir() {
56
60
  return join(dataDir(), "sandbox-build");
57
61
  }
@@ -100,7 +104,9 @@ var init_config = __esm(() => {
100
104
  sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
101
105
  mainBranch: z.string().default("main"),
102
106
  sandboxImage: z.string().optional(),
103
- burnConcurrency: z.number().int().min(1).max(8).default(3)
107
+ burnConcurrency: z.number().int().min(1).max(8).default(3),
108
+ burnMaxIterations: z.number().int().min(1).max(10).default(3),
109
+ setupCommand: z.string().optional()
104
110
  }));
105
111
  });
106
112
 
@@ -136,6 +142,11 @@ function loadConfig(env = process.env) {
136
142
  if (env.RUNCASTLE_BURN_CONCURRENCY) {
137
143
  overrides.burnConcurrency = Number(env.RUNCASTLE_BURN_CONCURRENCY);
138
144
  }
145
+ if (env.RUNCASTLE_BURN_MAX_ITERATIONS) {
146
+ overrides.burnMaxIterations = Number(env.RUNCASTLE_BURN_MAX_ITERATIONS);
147
+ }
148
+ if (env.RUNCASTLE_SETUP_COMMAND)
149
+ overrides.setupCommand = env.RUNCASTLE_SETUP_COMMAND;
139
150
  const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
140
151
  return RuncastleConfig.parse({ ...base, ...overrides });
141
152
  }
@@ -2116,11 +2127,14 @@ async function reattachWorktree(path, branch) {
2116
2127
  await git(path).checkout(branch);
2117
2128
  } catch {}
2118
2129
  }
2130
+ function tempBranchSlugSegment(slug) {
2131
+ return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
2132
+ }
2119
2133
  function researchBranchName(slug, waypointSeq, unique) {
2120
- return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
2134
+ return `${RESEARCH_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${waypointSeq}-${unique}`;
2121
2135
  }
2122
2136
  function ticketBranchName(slug, ticketSeq, unique) {
2123
- return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
2137
+ return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2124
2138
  }
2125
2139
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2126
2140
  const g = git(repoPath);
@@ -2188,17 +2202,23 @@ async function cleanupTempBranches(repoPath) {
2188
2202
  const prefix = tempBranchPrefix(name);
2189
2203
  if (!prefix)
2190
2204
  continue;
2191
- const slug = name.slice(prefix.length).split("/")[0];
2192
- const target = featureBranch(slug);
2205
+ const seg = name.slice(prefix.length).split("/")[0];
2206
+ const targets = seg ? all.filter((b) => {
2207
+ if (!b.startsWith("feature/"))
2208
+ return false;
2209
+ const featureSlug = b.slice("feature/".length);
2210
+ return featureSlug === seg || tempBranchSlugSegment(featureSlug) === seg;
2211
+ }) : [];
2193
2212
  let merged = false;
2194
- if (slug && all.includes(target)) {
2213
+ for (const target of targets) {
2195
2214
  try {
2196
2215
  const tip = (await g.revparse([name])).trim();
2197
2216
  const base = (await g.raw(["merge-base", name, target])).trim();
2198
- merged = tip.length > 0 && tip === base;
2199
- } catch {
2200
- merged = false;
2201
- }
2217
+ if (tip.length > 0 && tip === base) {
2218
+ merged = true;
2219
+ break;
2220
+ }
2221
+ } catch {}
2202
2222
  }
2203
2223
  if (merged && await deleteBranchDetachingWorktrees(g, repoPath, name)) {
2204
2224
  deleted.push(name);
@@ -2345,7 +2365,7 @@ async function mergeInProgress(g) {
2345
2365
  return false;
2346
2366
  }
2347
2367
  }
2348
- var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, testDriveState;
2368
+ var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
2349
2369
  var init_git = __esm(() => {
2350
2370
  init_paths();
2351
2371
  init_errors();
@@ -2483,6 +2503,46 @@ var init_skills_root = __esm(() => {
2483
2503
  SKILLS_MARKER = join7("packs", "runcastle");
2484
2504
  });
2485
2505
 
2506
+ // src/services/agent-stream.ts
2507
+ function beginTranscript(ticketId) {
2508
+ transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
2509
+ }
2510
+ function appendTranscript(ticketId, chunk) {
2511
+ const t = transcripts.get(ticketId);
2512
+ if (!t)
2513
+ return;
2514
+ const full = { ...chunk, i: t.nextIndex, ts: chunk.ts ?? Date.now() };
2515
+ t.nextIndex += 1;
2516
+ t.chunks.push(full);
2517
+ t.bytes += full.text.length;
2518
+ while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
2519
+ const dropped = t.chunks.shift();
2520
+ if (!dropped)
2521
+ break;
2522
+ t.bytes -= dropped.text.length;
2523
+ }
2524
+ }
2525
+ function endTranscript(ticketId) {
2526
+ const t = transcripts.get(ticketId);
2527
+ if (t)
2528
+ t.live = false;
2529
+ }
2530
+ function readTranscript(ticketId, after = -1) {
2531
+ const t = transcripts.get(ticketId);
2532
+ if (!t)
2533
+ return { live: false, chunks: [], firstIndex: 0, nextIndex: 0 };
2534
+ return {
2535
+ live: t.live,
2536
+ chunks: after < 0 ? [...t.chunks] : t.chunks.filter((c) => c.i > after),
2537
+ firstIndex: t.chunks[0]?.i ?? t.nextIndex,
2538
+ nextIndex: t.nextIndex
2539
+ };
2540
+ }
2541
+ var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
2542
+ var init_agent_stream = __esm(() => {
2543
+ transcripts = new Map;
2544
+ });
2545
+
2486
2546
  // src/workflows/ticket-burner.ts
2487
2547
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2488
2548
  import { dirname as dirname3, join as join8 } from "path";
@@ -2595,6 +2655,64 @@ function parseEnvFile(content) {
2595
2655
  }
2596
2656
  return out;
2597
2657
  }
2658
+ function detectPackageManager(tc) {
2659
+ const pinned = tc.packageManagerField?.split("@")[0];
2660
+ if (pinned && PACKAGE_MANAGERS.includes(pinned)) {
2661
+ return pinned;
2662
+ }
2663
+ if (tc.lockfiles.bun)
2664
+ return "bun";
2665
+ if (tc.lockfiles.pnpm)
2666
+ return "pnpm";
2667
+ if (tc.lockfiles.yarn)
2668
+ return "yarn";
2669
+ if (tc.lockfiles.npm)
2670
+ return "npm";
2671
+ return tc.hasPackageJson ? "npm" : undefined;
2672
+ }
2673
+ function resolveSetupCommand(tc, override) {
2674
+ const trimmed = override?.trim();
2675
+ if (trimmed)
2676
+ return trimmed;
2677
+ const pm = detectPackageManager(tc);
2678
+ if (!pm)
2679
+ return;
2680
+ switch (pm) {
2681
+ case "bun":
2682
+ return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
2683
+ case "pnpm":
2684
+ return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
2685
+ case "yarn":
2686
+ return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
2687
+ case "npm":
2688
+ return tc.lockfiles.npm ? "npm ci" : "npm install";
2689
+ }
2690
+ }
2691
+ function cacheMountFor(pm, hostPath) {
2692
+ return { hostPath, sandboxPath: PM_CACHE_SANDBOX_PATHS[pm] };
2693
+ }
2694
+ function readRepoToolchain(repoPath) {
2695
+ const pkgPath = join8(repoPath, "package.json");
2696
+ const hasPackageJson = existsSync10(pkgPath);
2697
+ let packageManagerField;
2698
+ if (hasPackageJson) {
2699
+ try {
2700
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2701
+ if (typeof pkg.packageManager === "string")
2702
+ packageManagerField = pkg.packageManager;
2703
+ } catch {}
2704
+ }
2705
+ return {
2706
+ hasPackageJson,
2707
+ ...packageManagerField !== undefined ? { packageManagerField } : {},
2708
+ lockfiles: {
2709
+ bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
2710
+ pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
2711
+ yarn: existsSync10(join8(repoPath, "yarn.lock")),
2712
+ npm: existsSync10(join8(repoPath, "package-lock.json"))
2713
+ }
2714
+ };
2715
+ }
2598
2716
  function createStreamThrottle(emit2, opts = {}) {
2599
2717
  const intervalMs = opts.intervalMs ?? 2000;
2600
2718
  const maxChars = opts.maxChars ?? 500;
@@ -2653,10 +2771,11 @@ function createSerialQueue() {
2653
2771
  return next;
2654
2772
  };
2655
2773
  }
2656
- function firstLine(s) {
2657
- const i = s.indexOf(`
2658
- `);
2659
- return (i === -1 ? s : s.slice(0, i)).trim();
2774
+ function errorHeadline(s) {
2775
+ const lines = s.split(`
2776
+ `).map((l) => l.trim()).filter(Boolean);
2777
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
2778
+ return causes.at(-1) ?? lines[0] ?? "";
2660
2779
  }
2661
2780
  async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2662
2781
  const width = Math.max(1, Math.floor(concurrency));
@@ -2711,7 +2830,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2711
2830
  failTicket(seq, outcome.error, outcome.event);
2712
2831
  ctx.emitEvent({
2713
2832
  type: "ticket.failed",
2714
- message: `ticket ${t.seq} failed: ${firstLine(outcome.error)}`,
2833
+ message: `ticket ${t.seq} failed: ${errorHeadline(outcome.error)}`,
2715
2834
  ticketId: t.id,
2716
2835
  data: { error: outcome.error }
2717
2836
  });
@@ -2845,8 +2964,11 @@ function buildBurnAgent(config2, token, model) {
2845
2964
  }
2846
2965
  return agent;
2847
2966
  }
2848
- function selectSandbox(config2) {
2849
- const imageOpts = { imageName: resolveSandboxImage(config2) };
2967
+ function selectSandbox(config2, mounts = []) {
2968
+ const imageOpts = {
2969
+ imageName: resolveSandboxImage(config2),
2970
+ ...mounts.length > 0 ? { mounts } : {}
2971
+ };
2850
2972
  switch (config2.sandbox) {
2851
2973
  case "docker":
2852
2974
  return docker(imageOpts);
@@ -2866,24 +2988,62 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
2866
2988
  DOCS_DIGEST: readDocsDigestFromDisk(project.id, feature.slug),
2867
2989
  COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`
2868
2990
  });
2991
+ const toolchain = readRepoToolchain(project.repoPath);
2992
+ const pm = detectPackageManager(toolchain);
2993
+ const setupCommand = resolveSetupCommand(toolchain, config2.setupCommand);
2994
+ const mounts = [];
2995
+ if (config2.sandbox !== "noSandbox" && pm) {
2996
+ const hostCache = burnCacheDir(pm);
2997
+ mkdirSync4(hostCache, { recursive: true });
2998
+ mounts.push(cacheMountFor(pm, hostCache));
2999
+ }
3000
+ if (setupCommand) {
3001
+ ctx.emitEvent({
3002
+ type: "burn.setup",
3003
+ message: `installing deps before agent start: ${setupCommand}`,
3004
+ ticketId: ticket.id
3005
+ });
3006
+ }
2869
3007
  mkdirSync4(logsDir(), { recursive: true });
2870
3008
  const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
2871
3009
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
3010
+ beginTranscript(ticket.id);
3011
+ const onStreamEvent = (event) => {
3012
+ throttle.onEvent(event);
3013
+ if (event.type === "text") {
3014
+ appendTranscript(ticket.id, { kind: "text", text: event.message });
3015
+ } else if (event.type === "toolCall") {
3016
+ appendTranscript(ticket.id, {
3017
+ kind: "tool",
3018
+ text: event.formattedArgs ?? "",
3019
+ name: event.name
3020
+ });
3021
+ }
3022
+ };
2872
3023
  const runOptions = {
2873
3024
  agent: buildBurnAgent(config2, token, model),
2874
- sandbox: selectSandbox(config2),
3025
+ sandbox: selectSandbox(config2, mounts),
2875
3026
  cwd: project.repoPath,
2876
3027
  prompt,
2877
3028
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
2878
3029
  signal: ctx.signal,
2879
3030
  name: `ticket-${ticket.seq}`,
2880
- logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
3031
+ maxIterations: config2.burnMaxIterations,
3032
+ ...setupCommand ? {
3033
+ hooks: {
3034
+ sandbox: {
3035
+ onSandboxReady: [{ command: setupCommand, timeoutMs: SETUP_HOOK_TIMEOUT_MS }]
3036
+ }
3037
+ }
3038
+ } : {},
3039
+ logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
2881
3040
  };
2882
3041
  let result;
2883
3042
  try {
2884
3043
  result = await run(runOptions);
2885
3044
  } catch (err) {
2886
3045
  throttle.flush();
3046
+ endTranscript(ticket.id);
2887
3047
  if (ctx.signal.aborted)
2888
3048
  throw err;
2889
3049
  const msg = err instanceof Error ? err.message : String(err);
@@ -2900,6 +3060,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
2900
3060
  return { status: "failed", error: msg };
2901
3061
  }
2902
3062
  throttle.flush();
3063
+ endTranscript(ticket.id);
2903
3064
  const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
2904
3065
  const outcome = interpretRunResult(result, blocked);
2905
3066
  if (outcome.status !== "done")
@@ -2919,7 +3080,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
2919
3080
  }
2920
3081
  return {
2921
3082
  status: "failed",
2922
- error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${firstLine(detail)} \u2014 the branch is preserved for manual recovery`
3083
+ error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${errorHeadline(detail)} \u2014 the branch is preserved for manual recovery`
2923
3084
  };
2924
3085
  }
2925
3086
  return outcome;
@@ -2948,14 +3109,23 @@ function readTokenFromEnvFile(path) {
2948
3109
  const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
2949
3110
  return token && token.length > 0 ? token : undefined;
2950
3111
  }
2951
- var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, ticketBurner;
3112
+ var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
2952
3113
  var init_ticket_burner = __esm(() => {
2953
3114
  init_src();
2954
3115
  init_config_load();
2955
3116
  init_paths();
2956
3117
  init_skills_root();
3118
+ init_agent_stream();
2957
3119
  init_git();
2958
3120
  PLACEHOLDERS = ["TICKET_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "COMMIT_CONVENTION"];
3121
+ PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
3122
+ PM_CACHE_SANDBOX_PATHS = {
3123
+ bun: "~/.bun/install/cache",
3124
+ pnpm: "~/.local/share/pnpm/store",
3125
+ yarn: "~/.cache/yarn",
3126
+ npm: "~/.npm"
3127
+ };
3128
+ SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
2959
3129
  ticketBurner = {
2960
3130
  id: "ticket-burner",
2961
3131
  run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
@@ -3004,15 +3174,16 @@ async function researchRun(ctx, deps) {
3004
3174
  }
3005
3175
  ctx.emitEvent({
3006
3176
  type: "research.failed",
3007
- message: `waypoint ${waypoint.seq} research failed: ${firstLine2(outcome.error)}`,
3177
+ message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
3008
3178
  data: { waypointId: waypoint.id, error: outcome.error }
3009
3179
  });
3010
3180
  return { status: "failed", summary: outcome.error };
3011
3181
  }
3012
- function firstLine2(s) {
3013
- const i = s.indexOf(`
3014
- `);
3015
- return (i === -1 ? s : s.slice(0, i)).trim();
3182
+ function errorHeadline2(s) {
3183
+ const lines = s.split(`
3184
+ `).map((l) => l.trim()).filter(Boolean);
3185
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
3186
+ return causes.at(-1) ?? lines[0] ?? "";
3016
3187
  }
3017
3188
  function createResearchStreamThrottle(emit2, opts = {}) {
3018
3189
  return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
@@ -3089,7 +3260,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3089
3260
  const detail = merge.error ?? "merge failed";
3090
3261
  ctx.emitEvent({
3091
3262
  type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
3092
- message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${firstLine2(detail)} \u2014 the branch is preserved for manual recovery`,
3263
+ message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline2(detail)} \u2014 the branch is preserved for manual recovery`,
3093
3264
  data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
3094
3265
  });
3095
3266
  return {
@@ -4833,11 +5004,13 @@ var init_project = __esm(() => {
4833
5004
  import * as z8 from "zod";
4834
5005
  var runRouter;
4835
5006
  var init_run = __esm(() => {
5007
+ init_agent_stream();
4836
5008
  init_repo();
4837
5009
  init_runner();
4838
5010
  init_context();
4839
5011
  runRouter = router({
4840
5012
  get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
5013
+ agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
4841
5014
  cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
4842
5015
  cancelRun(input.runId);
4843
5016
  return { ok: true };
@@ -5073,6 +5246,22 @@ var init_settings = __esm(() => {
5073
5246
  valueSchema: z9.number().int().min(1).max(8),
5074
5247
  parseEnv: (raw) => Number(raw)
5075
5248
  },
5249
+ {
5250
+ key: "burnMaxIterations",
5251
+ configKey: "burnMaxIterations",
5252
+ envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
5253
+ restartRequired: false,
5254
+ valueSchema: z9.number().int().min(1).max(10),
5255
+ parseEnv: (raw) => Number(raw)
5256
+ },
5257
+ {
5258
+ key: "setupCommand",
5259
+ configKey: "setupCommand",
5260
+ envVar: "RUNCASTLE_SETUP_COMMAND",
5261
+ restartRequired: false,
5262
+ valueSchema: z9.string().min(1),
5263
+ parseEnv: idEnv
5264
+ },
5076
5265
  {
5077
5266
  key: "mainBranch",
5078
5267
  configKey: "mainBranch",