@pushpalsdev/cli 1.1.19 → 1.1.20

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.
@@ -1040,6 +1040,7 @@ function loadPushPalsConfig(options = {}) {
1040
1040
  enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, true),
1041
1041
  killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
1042
1042
  tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
1043
+ startupGraceMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ?? remoteAutonomyNode.startup_grace_ms, 120000)),
1043
1044
  heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
1044
1045
  visionContextMaxChars: Math.max(1000, Math.min(1e6, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ?? remoteAutonomyNode.vision_context_max_chars, 65536))),
1045
1046
  ideationBudgetMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ?? remoteAutonomyNode.ideation_budget_ms, 20000)),
@@ -1648,7 +1649,8 @@ var DEFAULT_STARTUP_GIT_PROBE_TIMEOUT_MS = 5000;
1648
1649
  var DEFAULT_STARTUP_GIT_REMOTE_TIMEOUT_MS = 1e4;
1649
1650
  var DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS = 5000;
1650
1651
  var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
1651
- var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
1652
+ var DEFAULT_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 5000;
1653
+ var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS_ENV = "PUSHPALS_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS";
1652
1654
  var BLOCKING_WORKERPAL_IMAGE_BUILD_ENV = "PUSHPALS_BLOCKING_WORKERPAL_IMAGE_BUILD";
1653
1655
  var CLI_SESSION_JOB_LOG_MAX_CHARS = 700;
1654
1656
  var CLI_SESSION_SHOW_JOB_EVENTS_ENV = "PUSHPALS_CLI_SHOW_JOB_EVENTS";
@@ -1681,6 +1683,13 @@ function formatTimestampedCliLine(line, at = new Date) {
1681
1683
  function isTruthyCliEnvValue(value) {
1682
1684
  return /^(1|true|yes|on)$/i.test(String(value ?? "").trim());
1683
1685
  }
1686
+ function parseCliIntEnv(name, env = process.env) {
1687
+ const raw = env[name];
1688
+ if (raw == null || String(raw).trim() === "")
1689
+ return null;
1690
+ const parsed = Number.parseInt(String(raw), 10);
1691
+ return Number.isFinite(parsed) ? parsed : null;
1692
+ }
1684
1693
  function shouldShowCliSessionOperationalEvents(env = process.env) {
1685
1694
  return isTruthyCliEnvValue(env[CLI_SESSION_SHOW_JOB_EVENTS_ENV]);
1686
1695
  }
@@ -3899,8 +3908,12 @@ async function precheckWorkerpalDockerAvailability(opts) {
3899
3908
  function resolveWorkerpalCapacityTimeoutMs(config) {
3900
3909
  return Math.max(config.remotebuddy.waitForWorkerpalMs, config.remotebuddy.workerpalStartupTimeoutMs, config.remotebuddy.workerpalDocker ? config.workerpals.dockerAgentStartupTimeoutMs + 15000 : 0, 1e4);
3901
3910
  }
3911
+ function resolveWorkerpalStartupReadinessProbeMaxMs(env = process.env) {
3912
+ const configured = parseCliIntEnv(WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS_ENV, env);
3913
+ return Math.max(1000, configured ?? DEFAULT_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS);
3914
+ }
3902
3915
  function resolveWorkerpalStartupReadinessProbeTimeoutMs(config) {
3903
- return Math.max(5000, Math.min(resolveWorkerpalCapacityTimeoutMs(config), WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS));
3916
+ return Math.max(1000, Math.min(resolveWorkerpalCapacityTimeoutMs(config), resolveWorkerpalStartupReadinessProbeMaxMs()));
3904
3917
  }
3905
3918
  function shouldPrepareEmbeddedWorkerpalDockerImageBlocking(opts = {}) {
3906
3919
  const env = opts.env ?? process.env;
@@ -6082,6 +6095,7 @@ export {
6082
6095
  shouldPrepareEmbeddedWorkerpalDockerImageBlocking,
6083
6096
  shouldDeferRemoteBuddySessionConsumerReadiness,
6084
6097
  runCommandWithEnv,
6098
+ resolveWorkerpalStartupReadinessProbeMaxMs,
6085
6099
  resolveWorkerpalDockerProbe,
6086
6100
  resolveWorkerExecutionReadiness,
6087
6101
  resolveWindowsWhereExecutableCandidatesForEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.19",
3
+ "version": "1.1.20",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -75,6 +75,7 @@ session_id = "remotebuddy-dev"
75
75
  enabled = true
76
76
  kill_switch_enabled = false
77
77
  tick_interval_ms = 300000
78
+ startup_grace_ms = 120000
78
79
  heartbeat_log_ms = 30000
79
80
  vision_context_max_chars = 65536
80
81
  ideation_budget_ms = 20000
@@ -1323,6 +1323,7 @@ function loadPushPalsConfig(options = {}) {
1323
1323
  enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, true),
1324
1324
  killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
1325
1325
  tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
1326
+ startupGraceMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ?? remoteAutonomyNode.startup_grace_ms, 120000)),
1326
1327
  heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
1327
1328
  visionContextMaxChars: Math.max(1000, Math.min(1e6, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ?? remoteAutonomyNode.vision_context_max_chars, 65536))),
1328
1329
  ideationBudgetMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ?? remoteAutonomyNode.ideation_budget_ms, 20000)),
@@ -6569,6 +6570,7 @@ class RemoteBuddyAutonomousEngine {
6569
6570
  cfg;
6570
6571
  runtimeEnabled = true;
6571
6572
  timer = null;
6573
+ startupGraceTimer = null;
6572
6574
  startupFastTickTimer = null;
6573
6575
  heartbeatTimer = null;
6574
6576
  inFlight = false;
@@ -6641,7 +6643,8 @@ class RemoteBuddyAutonomousEngine {
6641
6643
  console.log(`[RemoteBuddyAutonomousEngine] heartbeat: status=running run=${this.currentRunId} phase=${this.currentPhase} run_elapsed_ms=${runElapsedMs} phase_elapsed_ms=${phaseElapsedMs}`);
6642
6644
  return;
6643
6645
  }
6644
- const nextTickInMs = this.timer && this.nextTickAtMs > 0 ? Math.max(0, this.nextTickAtMs - now) : 0;
6646
+ const hasScheduledTick = Boolean(this.timer || this.startupGraceTimer || this.startupFastTickTimer);
6647
+ const nextTickInMs = hasScheduledTick && this.nextTickAtMs > 0 ? Math.max(0, this.nextTickAtMs - now) : 0;
6645
6648
  const lastAgeMs = this.lastCompletedAtMs > 0 ? Math.max(0, now - this.lastCompletedAtMs) : -1;
6646
6649
  console.log(`[RemoteBuddyAutonomousEngine] heartbeat: status=idle last_outcome=${this.lastOutcome} detail=${this.lastDetail} last_tick_age_ms=${lastAgeMs} next_tick_in_ms=${nextTickInMs}`);
6647
6650
  }
@@ -6667,6 +6670,15 @@ class RemoteBuddyAutonomousEngine {
6667
6670
  startupFastTickDelayMs() {
6668
6671
  return Math.max(1000, Math.min(STARTUP_FAST_TICK_MAX_DELAY_MS, Math.floor(this.cfg.tickIntervalMs / 10)));
6669
6672
  }
6673
+ startupGraceMs() {
6674
+ return Math.max(0, this.cfg.startupGraceMs ?? 0);
6675
+ }
6676
+ clearStartupGraceTimer() {
6677
+ if (this.startupGraceTimer) {
6678
+ clearTimeout(this.startupGraceTimer);
6679
+ this.startupGraceTimer = null;
6680
+ }
6681
+ }
6670
6682
  clearStartupFastTickTimer() {
6671
6683
  if (this.startupFastTickTimer) {
6672
6684
  clearTimeout(this.startupFastTickTimer);
@@ -8160,22 +8172,42 @@ Scope:
8160
8172
  });
8161
8173
  }
8162
8174
  start() {
8163
- if (!this.runtimeEnabled || this.timer)
8175
+ if (!this.runtimeEnabled || this.timer || this.startupGraceTimer)
8164
8176
  return;
8165
8177
  console.log(`[RemoteBuddyAutonomousEngine] Using dedicated autonomy worktree ${this.autonomyRepo} (remote=${this.gitRemote} integration=${this.integrationBranch} base=${this.baseBranch}).`);
8166
8178
  this.startupFastTickAttemptsRemaining = STARTUP_FAST_TICK_MAX_ATTEMPTS;
8167
- this.nextTickAtMs = Date.now();
8168
- this.timer = setInterval(() => {
8169
- this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
8170
- this.tick();
8171
- }, this.cfg.tickIntervalMs);
8179
+ const startInterval = () => {
8180
+ if (this.timer)
8181
+ return;
8182
+ this.timer = setInterval(() => {
8183
+ this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
8184
+ this.tick();
8185
+ }, this.cfg.tickIntervalMs);
8186
+ };
8187
+ const firstTickDelayMs = this.startupGraceMs();
8188
+ this.nextTickAtMs = Date.now() + firstTickDelayMs;
8172
8189
  this.heartbeatTimer = setInterval(() => {
8173
8190
  this.logHeartbeat();
8174
8191
  }, this.cfg.heartbeatLogMs);
8175
8192
  this.logHeartbeat();
8193
+ if (firstTickDelayMs > 0) {
8194
+ console.log(`[RemoteBuddyAutonomousEngine] startup autonomy tick delayed by ${firstTickDelayMs}ms to leave cold-start capacity available for user work.`);
8195
+ this.startupGraceTimer = setTimeout(() => {
8196
+ this.startupGraceTimer = null;
8197
+ if (!this.runtimeEnabled)
8198
+ return;
8199
+ startInterval();
8200
+ this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
8201
+ this.tick();
8202
+ }, firstTickDelayMs);
8203
+ return;
8204
+ }
8205
+ startInterval();
8206
+ this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
8176
8207
  this.tick();
8177
8208
  }
8178
8209
  stop() {
8210
+ this.clearStartupGraceTimer();
8179
8211
  this.clearStartupFastTickTimer();
8180
8212
  if (this.timer) {
8181
8213
  clearInterval(this.timer);
@@ -9014,7 +9046,7 @@ class RemoteBuddyOrchestrator {
9014
9046
  console.log(`[RemoteBuddy] Budgets: interactive=${this.executionBudgetInteractiveMs}ms normal=${this.executionBudgetNormalMs}ms background=${this.executionBudgetBackgroundMs}ms finalization=${this.finalizationBudgetMs}ms`);
9015
9047
  console.log(`[RemoteBuddy] Failure log fetch on job failures: ${this.fetchFailureLogsOnJobFailure ? "on" : "off"}`);
9016
9048
  console.log(`[RemoteBuddy] Persistent memory: ${this.memoryEnabled ? "on" : "off"} crossSession=${this.memoryIncludeCrossSession ? "on" : "off"} recallItems=${this.memoryMaxRecallItems} recallChars=${this.memoryMaxRecallChars} retentionDays=${this.memoryRetentionDays}`);
9017
- console.log(`[RemoteBuddy] Autonomous engine: ${CONFIG.remotebuddy.autonomy.enabled ? "enabled" : "disabled"} tick=${CONFIG.remotebuddy.autonomy.tickIntervalMs}ms maxConcurrentObjectives=${CONFIG.remotebuddy.autonomy.maxConcurrentObjectives} maxDispatchPerHour=${CONFIG.remotebuddy.autonomy.maxDispatchPerHour} exploreRate=${CONFIG.remotebuddy.autonomy.exploreRate.toFixed(2)} allowDirtyWorktree=${CONFIG.remotebuddy.autonomy.allowDirtyWorktree ? "on" : "off"}`);
9049
+ console.log(`[RemoteBuddy] Autonomous engine: ${CONFIG.remotebuddy.autonomy.enabled ? "enabled" : "disabled"} tick=${CONFIG.remotebuddy.autonomy.tickIntervalMs}ms startupGrace=${CONFIG.remotebuddy.autonomy.startupGraceMs}ms maxConcurrentObjectives=${CONFIG.remotebuddy.autonomy.maxConcurrentObjectives} maxDispatchPerHour=${CONFIG.remotebuddy.autonomy.maxDispatchPerHour} exploreRate=${CONFIG.remotebuddy.autonomy.exploreRate.toFixed(2)} allowDirtyWorktree=${CONFIG.remotebuddy.autonomy.allowDirtyWorktree ? "on" : "off"}`);
9018
9050
  console.log(`[RemoteBuddy] Autonomy runtime-config polling: every ${this.autonomyConfigPollMs}ms`);
9019
9051
  }
9020
9052
  async emitStartupStatus() {
@@ -34,6 +34,7 @@ import type {
34
34
  DockerWarmShellResult,
35
35
  DockerWarmStartupContext,
36
36
  } from "./backends/types.js";
37
+ import { resolveFreshWorktreeBaseRef } from "./worktree_base_ref.js";
37
38
 
38
39
  const DEFAULT_OPENHANDS_MODEL = "local-model";
39
40
  const DEFAULT_CONFIG = loadPushPalsConfig();
@@ -2106,7 +2107,27 @@ export class DockerExecutor {
2106
2107
  reviewAgent && typeof reviewAgent.resolutionType === "string"
2107
2108
  ? reviewAgent.resolutionType.trim().toLowerCase()
2108
2109
  : "";
2109
- if (resolutionType !== "merge_conflict") return this.options.baseRef;
2110
+ if (resolutionType !== "merge_conflict") {
2111
+ return resolveFreshWorktreeBaseRef({
2112
+ requestedRef: this.options.baseRef,
2113
+ integrationBranch:
2114
+ this.config.sourceControlManager.mainBranch ||
2115
+ this.config.workerpals.baseRef ||
2116
+ this.options.baseRef,
2117
+ sourceBaseBranch: this.config.sourceControlManager.baseBranch,
2118
+ git: (args) => this.runGitBaseRefCommand(args),
2119
+ log: (level, message) => {
2120
+ const line = `[DockerExecutor] ${message}`;
2121
+ if (level === "warn") {
2122
+ console.warn(line);
2123
+ onLog?.("stderr", line);
2124
+ } else {
2125
+ console.log(line);
2126
+ onLog?.("stdout", line);
2127
+ }
2128
+ },
2129
+ });
2130
+ }
2110
2131
 
2111
2132
  const normalizedHeadRef = normalizeMergeConflictHeadRef(reviewAgent?.prHeadRef);
2112
2133
  if (!normalizedHeadRef) {
@@ -2150,6 +2171,26 @@ export class DockerExecutor {
2150
2171
  return remoteRef;
2151
2172
  }
2152
2173
 
2174
+ private async runGitBaseRefCommand(
2175
+ args: string[],
2176
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
2177
+ const proc = Bun.spawn(["git", ...args], {
2178
+ cwd: this.options.repo,
2179
+ stdout: "pipe",
2180
+ stderr: "pipe",
2181
+ });
2182
+ const [exitCode, stdout, stderr] = await Promise.all([
2183
+ proc.exited,
2184
+ new Response(proc.stdout).text(),
2185
+ new Response(proc.stderr).text(),
2186
+ ]);
2187
+ return {
2188
+ ok: exitCode === 0,
2189
+ stdout,
2190
+ stderr,
2191
+ };
2192
+ }
2193
+
2153
2194
  /**
2154
2195
  * Pull the Docker image
2155
2196
  */
@@ -47,6 +47,7 @@ import { DockerExecutionExhaustedError, DockerExecutor } from "./docker_executor
47
47
  import { forceDeleteWorktreePath } from "./common/worktree_cleanup.js";
48
48
  import { WorkerServerTransport, type WorkerHeartbeatPayload } from "./common/server_transport.js";
49
49
  import { DEFAULT_DOCKER_TIMEOUT_MS, parseDockerTimeoutMs } from "./timeout_policy.js";
50
+ import { resolveFreshWorktreeBaseRef } from "./worktree_base_ref.js";
50
51
 
51
52
  type CommitRef = {
52
53
  branch: string;
@@ -314,6 +315,8 @@ async function reportWorkerLlmUsage(
314
315
  }
315
316
 
316
317
  function integrationBranchName(): string {
318
+ const configuredIntegrationBranch = CONFIG.sourceControlManager.mainBranch.trim();
319
+ if (configuredIntegrationBranch) return configuredIntegrationBranch;
317
320
  const configuredBaseRef = CONFIG.workerpals.baseRef.trim();
318
321
  if (!configuredBaseRef) return "main_agents";
319
322
  return configuredBaseRef.replace(/^origin\//, "").trim() || "main_agents";
@@ -665,33 +668,17 @@ async function runJob(
665
668
  }
666
669
 
667
670
  async function resolveWorktreeBaseRef(repo: string, requestedRef: string): Promise<string> {
668
- const integrationBranch = integrationBranchName();
669
- const integrationRemoteRef = `origin/${integrationBranch}`;
670
- const candidates = new Set<string>([
671
+ return resolveFreshWorktreeBaseRef({
671
672
  requestedRef,
672
- integrationRemoteRef,
673
- integrationBranch,
674
- "HEAD",
675
- ]);
676
- if (requestedRef.startsWith("origin/")) {
677
- const branch = requestedRef.slice("origin/".length);
678
- const fetchResult = await git(repo, ["fetch", "origin", branch, "--quiet"]);
679
- if (!fetchResult.ok) {
680
- console.warn(
681
- `[WorkerPals] Could not refresh ${requestedRef}; continuing with local refs (${fetchResult.stderr || fetchResult.stdout})`,
682
- );
683
- }
684
- candidates.add(branch);
685
- } else if (requestedRef !== "HEAD") {
686
- candidates.add(`origin/${requestedRef}`);
687
- }
688
-
689
- for (const ref of candidates) {
690
- const parsed = await git(repo, ["rev-parse", "--verify", "--quiet", ref]);
691
- if (parsed.ok) return ref;
692
- }
693
-
694
- return "HEAD";
673
+ integrationBranch: integrationBranchName(),
674
+ sourceBaseBranch: CONFIG.sourceControlManager.baseBranch,
675
+ git: (args) => git(repo, args),
676
+ log: (level, message) => {
677
+ const line = `[WorkerPals] ${message}`;
678
+ if (level === "warn") console.warn(line);
679
+ else console.log(line);
680
+ },
681
+ });
695
682
  }
696
683
 
697
684
  async function createIsolatedWorktree(
@@ -0,0 +1,141 @@
1
+ export type GitBaseRefCommandResult = {
2
+ ok: boolean;
3
+ stdout?: string;
4
+ stderr?: string;
5
+ };
6
+
7
+ export type GitBaseRefCommand = (args: string[]) => Promise<GitBaseRefCommandResult>;
8
+
9
+ export type WorktreeBaseRefLogLevel = "info" | "warn";
10
+
11
+ export type ResolveFreshWorktreeBaseRefOptions = {
12
+ requestedRef: string;
13
+ integrationBranch: string;
14
+ sourceBaseBranch: string;
15
+ remote?: string;
16
+ git: GitBaseRefCommand;
17
+ log?: (level: WorktreeBaseRefLogLevel, message: string) => void;
18
+ };
19
+
20
+ function normalizeBranchName(value: string): string {
21
+ return value
22
+ .trim()
23
+ .replace(/^refs\/heads\//, "")
24
+ .replace(/^origin\//, "")
25
+ .replace(/^\/+|\/+$/g, "");
26
+ }
27
+
28
+ function normalizeRequestedRef(value: string): string {
29
+ return value.trim() || "HEAD";
30
+ }
31
+
32
+ function remoteRef(remote: string, branch: string): string {
33
+ return `${remote}/${branch}`;
34
+ }
35
+
36
+ function isIntegrationBaseRequest(ref: string, integrationBranch: string, remote: string): boolean {
37
+ const normalized = normalizeRequestedRef(ref);
38
+ const branch = normalizeBranchName(normalized);
39
+ return (
40
+ branch === integrationBranch ||
41
+ normalized === remoteRef(remote, integrationBranch) ||
42
+ normalized === `refs/remotes/${remote}/${integrationBranch}`
43
+ );
44
+ }
45
+
46
+ async function fetchRemoteBranch(
47
+ git: GitBaseRefCommand,
48
+ remote: string,
49
+ branch: string,
50
+ ): Promise<GitBaseRefCommandResult> {
51
+ if (!remote || !branch || branch === "HEAD") return { ok: true };
52
+ return git(["fetch", remote, branch, "--quiet"]);
53
+ }
54
+
55
+ async function refExists(git: GitBaseRefCommand, ref: string): Promise<boolean> {
56
+ const result = await git(["rev-parse", "--verify", "--quiet", ref]);
57
+ return result.ok;
58
+ }
59
+
60
+ async function isAncestor(
61
+ git: GitBaseRefCommand,
62
+ ancestorRef: string,
63
+ descendantRef: string,
64
+ ): Promise<boolean> {
65
+ const result = await git(["merge-base", "--is-ancestor", ancestorRef, descendantRef]);
66
+ return result.ok;
67
+ }
68
+
69
+ export async function resolveExistingWorktreeBaseRef(
70
+ options: Omit<ResolveFreshWorktreeBaseRefOptions, "sourceBaseBranch" | "log">,
71
+ ): Promise<string> {
72
+ const remote = (options.remote ?? "origin").trim() || "origin";
73
+ const requestedRef = normalizeRequestedRef(options.requestedRef);
74
+ const integrationBranch = normalizeBranchName(options.integrationBranch) || "main_agents";
75
+ const integrationRemoteRef = remoteRef(remote, integrationBranch);
76
+ const candidates = new Set<string>([
77
+ requestedRef,
78
+ integrationRemoteRef,
79
+ integrationBranch,
80
+ "HEAD",
81
+ ]);
82
+
83
+ if (requestedRef.startsWith(`${remote}/`)) {
84
+ const branch = requestedRef.slice(`${remote}/`.length);
85
+ await fetchRemoteBranch(options.git, remote, branch);
86
+ candidates.add(branch);
87
+ } else if (requestedRef !== "HEAD") {
88
+ candidates.add(remoteRef(remote, requestedRef));
89
+ }
90
+
91
+ for (const ref of candidates) {
92
+ if (await refExists(options.git, ref)) return ref;
93
+ }
94
+
95
+ return "HEAD";
96
+ }
97
+
98
+ export async function resolveFreshWorktreeBaseRef(
99
+ options: ResolveFreshWorktreeBaseRefOptions,
100
+ ): Promise<string> {
101
+ const remote = (options.remote ?? "origin").trim() || "origin";
102
+ const requestedRef = normalizeRequestedRef(options.requestedRef);
103
+ const integrationBranch = normalizeBranchName(options.integrationBranch) || "main_agents";
104
+ const sourceBaseBranch = normalizeBranchName(options.sourceBaseBranch) || "main";
105
+ const resolvedRef = await resolveExistingWorktreeBaseRef({
106
+ requestedRef,
107
+ integrationBranch,
108
+ remote,
109
+ git: options.git,
110
+ });
111
+
112
+ if (
113
+ !sourceBaseBranch ||
114
+ sourceBaseBranch === integrationBranch ||
115
+ !isIntegrationBaseRequest(requestedRef, integrationBranch, remote)
116
+ ) {
117
+ return resolvedRef;
118
+ }
119
+
120
+ const sourceBaseRef = remoteRef(remote, sourceBaseBranch);
121
+ const fetchSource = await fetchRemoteBranch(options.git, remote, sourceBaseBranch);
122
+ if (!fetchSource.ok) {
123
+ options.log?.(
124
+ "warn",
125
+ `Could not refresh ${sourceBaseRef}; checking local ref before keeping ${resolvedRef} (${fetchSource.stderr || fetchSource.stdout || "fetch failed"}).`,
126
+ );
127
+ }
128
+
129
+ if (!(await refExists(options.git, sourceBaseRef))) return resolvedRef;
130
+
131
+ if (resolvedRef !== "HEAD" && (await refExists(options.git, resolvedRef))) {
132
+ const sourceAlreadyIncluded = await isAncestor(options.git, sourceBaseRef, resolvedRef);
133
+ if (sourceAlreadyIncluded) return resolvedRef;
134
+ }
135
+
136
+ options.log?.(
137
+ "warn",
138
+ `Worktree base ${resolvedRef} does not contain ${sourceBaseRef}; using ${sourceBaseRef} for new WorkerPal jobs to avoid stale integration-branch checkouts.`,
139
+ );
140
+ return sourceBaseRef;
141
+ }
@@ -75,6 +75,7 @@ session_id = "remotebuddy-dev"
75
75
  enabled = true
76
76
  kill_switch_enabled = false
77
77
  tick_interval_ms = 300000
78
+ startup_grace_ms = 120000
78
79
  heartbeat_log_ms = 30000
79
80
  vision_context_max_chars = 65536
80
81
  ideation_budget_ms = 20000
@@ -128,6 +128,7 @@ export interface PushPalsConfig {
128
128
  enabled: boolean;
129
129
  killSwitchEnabled: boolean;
130
130
  tickIntervalMs: number;
131
+ startupGraceMs: number;
131
132
  heartbeatLogMs: number;
132
133
  visionContextMaxChars: number;
133
134
  ideationBudgetMs: number;
@@ -1671,6 +1672,14 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
1671
1672
  120_000,
1672
1673
  ),
1673
1674
  ),
1675
+ startupGraceMs: Math.max(
1676
+ 0,
1677
+ asInt(
1678
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ??
1679
+ remoteAutonomyNode.startup_grace_ms,
1680
+ 120_000,
1681
+ ),
1682
+ ),
1674
1683
  heartbeatLogMs: Math.max(
1675
1684
  1_000,
1676
1685
  asInt(