@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.
- package/dist/pushpals-cli.js +16 -2
- package/package.json +1 -1
- package/runtime/configs/default.toml +1 -0
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +40 -8
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +42 -1
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +13 -26
- package/runtime/sandbox/apps/workerpals/src/worktree_base_ref.ts +141 -0
- package/runtime/sandbox/configs/default.toml +1 -0
- package/runtime/sandbox/packages/shared/src/config.ts +9 -0
package/dist/pushpals-cli.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
@@ -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
|
|
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
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
this.
|
|
8171
|
-
|
|
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")
|
|
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
|
-
|
|
669
|
-
const integrationRemoteRef = `origin/${integrationBranch}`;
|
|
670
|
-
const candidates = new Set<string>([
|
|
671
|
+
return resolveFreshWorktreeBaseRef({
|
|
671
672
|
requestedRef,
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
+
}
|
|
@@ -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(
|