@pushpalsdev/cli 1.0.1
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/README.md +36 -0
- package/bin/pushpals.cjs +76 -0
- package/dist/pushpals-cli.js +1249 -0
- package/package.json +33 -0
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// ../../scripts/pushpals-cli.ts
|
|
5
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
|
|
9
|
+
// ../shared/src/config.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { join, resolve, isAbsolute } from "path";
|
|
12
|
+
var PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "..");
|
|
13
|
+
var DEFAULT_CONFIG_DIR = "configs";
|
|
14
|
+
var LEGACY_CONFIG_DIR = "config";
|
|
15
|
+
var TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
16
|
+
var FALSY = new Set(["0", "false", "no", "off"]);
|
|
17
|
+
var DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
|
|
18
|
+
var DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
|
|
19
|
+
var DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS = 192 * 1024;
|
|
20
|
+
var DEFAULT_WORKERPALS_OUTPUT_MAX_LINES = 600;
|
|
21
|
+
var DEFAULT_WORKERPALS_OUTPUT_MAX_HEAD_LINES = 120;
|
|
22
|
+
var DEFAULT_WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS = 180000;
|
|
23
|
+
var DEFAULT_WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS = 45000;
|
|
24
|
+
var DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS = 16000;
|
|
25
|
+
var DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS = 8000;
|
|
26
|
+
var DEFAULT_WORKERPALS_EXECUTOR_RESULT_PREFIX = "__PUSHPALS_OH_RESULT__ ";
|
|
27
|
+
var DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS = 12;
|
|
28
|
+
var DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS = 2400;
|
|
29
|
+
var DEFAULT_REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS = 420;
|
|
30
|
+
var DEFAULT_REMOTEBUDDY_MEMORY_RETENTION_DAYS = 30;
|
|
31
|
+
var cachedConfig = null;
|
|
32
|
+
var cachedConfigKey = "";
|
|
33
|
+
function firstNonEmpty(...values) {
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
const trimmed = (value ?? "").trim();
|
|
36
|
+
if (trimmed)
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
function parseBoolEnv(name) {
|
|
42
|
+
const raw = (process.env[name] ?? "").trim().toLowerCase();
|
|
43
|
+
if (!raw)
|
|
44
|
+
return null;
|
|
45
|
+
if (TRUTHY.has(raw))
|
|
46
|
+
return true;
|
|
47
|
+
if (FALSY.has(raw))
|
|
48
|
+
return false;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function parseIntEnv(name) {
|
|
52
|
+
const raw = (process.env[name] ?? "").trim();
|
|
53
|
+
if (!raw)
|
|
54
|
+
return null;
|
|
55
|
+
const parsed = Number.parseInt(raw, 10);
|
|
56
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
57
|
+
}
|
|
58
|
+
function parseTomlFile(path) {
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return {};
|
|
61
|
+
const raw = readFileSync(path, "utf-8");
|
|
62
|
+
const parsed = Bun.TOML.parse(raw);
|
|
63
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
64
|
+
return {};
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
function isObject(value) {
|
|
68
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
function mergeDeep(base, override) {
|
|
71
|
+
const out = { ...base };
|
|
72
|
+
for (const [key, value] of Object.entries(override)) {
|
|
73
|
+
const existing = out[key];
|
|
74
|
+
if (isObject(existing) && isObject(value)) {
|
|
75
|
+
out[key] = mergeDeep(existing, value);
|
|
76
|
+
} else {
|
|
77
|
+
out[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function getObject(parent, key) {
|
|
83
|
+
const value = parent[key];
|
|
84
|
+
if (isObject(value))
|
|
85
|
+
return value;
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
function asString(value, fallback) {
|
|
89
|
+
if (typeof value === "string" && value.trim())
|
|
90
|
+
return value.trim();
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
function asBoolean(value, fallback) {
|
|
94
|
+
if (typeof value === "boolean")
|
|
95
|
+
return value;
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
const lowered = value.trim().toLowerCase();
|
|
98
|
+
if (TRUTHY.has(lowered))
|
|
99
|
+
return true;
|
|
100
|
+
if (FALSY.has(lowered))
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
function asInt(value, fallback) {
|
|
106
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
107
|
+
return Math.floor(value);
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
110
|
+
if (Number.isFinite(parsed))
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
function asIntOrNull(value) {
|
|
116
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
117
|
+
return Math.floor(value);
|
|
118
|
+
if (typeof value === "string" && value.trim()) {
|
|
119
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
120
|
+
if (Number.isFinite(parsed))
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function asStringArray(value) {
|
|
126
|
+
if (!Array.isArray(value))
|
|
127
|
+
return [];
|
|
128
|
+
return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
function asCheckArray(value) {
|
|
131
|
+
if (!Array.isArray(value))
|
|
132
|
+
return [];
|
|
133
|
+
const checks = [];
|
|
134
|
+
for (const entry of value) {
|
|
135
|
+
if (!isObject(entry))
|
|
136
|
+
continue;
|
|
137
|
+
const name = asString(entry.name, "").trim();
|
|
138
|
+
const command = asString(entry.command, "").trim();
|
|
139
|
+
if (!name || !command)
|
|
140
|
+
continue;
|
|
141
|
+
const timeoutMs = Math.max(1000, asInt(entry.timeout_ms ?? entry.timeoutMs, 300000));
|
|
142
|
+
checks.push({ name, command, timeoutMs });
|
|
143
|
+
}
|
|
144
|
+
return checks;
|
|
145
|
+
}
|
|
146
|
+
function asStringNumberRecord(value) {
|
|
147
|
+
if (!isObject(value))
|
|
148
|
+
return {};
|
|
149
|
+
const out = {};
|
|
150
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
151
|
+
const name = key.trim();
|
|
152
|
+
if (!name)
|
|
153
|
+
continue;
|
|
154
|
+
const num = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw.trim(), 10) : Number.NaN;
|
|
155
|
+
if (!Number.isFinite(num))
|
|
156
|
+
continue;
|
|
157
|
+
out[name] = Math.max(0, Math.floor(num));
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function resolvePathFromRoot(projectRoot, value) {
|
|
162
|
+
if (!value)
|
|
163
|
+
return projectRoot;
|
|
164
|
+
if (isAbsolute(value))
|
|
165
|
+
return resolve(value);
|
|
166
|
+
return resolve(projectRoot, value);
|
|
167
|
+
}
|
|
168
|
+
function resolveRuntimeConfigDir(projectRoot, configuredDir) {
|
|
169
|
+
if (configuredDir && configuredDir.trim()) {
|
|
170
|
+
return resolvePathFromRoot(projectRoot, configuredDir);
|
|
171
|
+
}
|
|
172
|
+
const canonicalDir = resolvePathFromRoot(projectRoot, DEFAULT_CONFIG_DIR);
|
|
173
|
+
const legacyDir = resolvePathFromRoot(projectRoot, LEGACY_CONFIG_DIR);
|
|
174
|
+
if (existsSync(join(canonicalDir, "default.toml")))
|
|
175
|
+
return canonicalDir;
|
|
176
|
+
if (existsSync(join(legacyDir, "default.toml")))
|
|
177
|
+
return legacyDir;
|
|
178
|
+
return canonicalDir;
|
|
179
|
+
}
|
|
180
|
+
function parseTomlWithLegacyFallback(primaryPath, fallbackPath) {
|
|
181
|
+
if (existsSync(primaryPath))
|
|
182
|
+
return parseTomlFile(primaryPath);
|
|
183
|
+
if (fallbackPath && existsSync(fallbackPath))
|
|
184
|
+
return parseTomlFile(fallbackPath);
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
function normalizeBackend(value) {
|
|
188
|
+
const text = value.trim().toLowerCase();
|
|
189
|
+
if (!text)
|
|
190
|
+
return "lmstudio";
|
|
191
|
+
if (text === "openai_compatible")
|
|
192
|
+
return "lmstudio";
|
|
193
|
+
if (text === "ollama_chat")
|
|
194
|
+
return "ollama";
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
function normalizeWorkerImageRebuildMode(value) {
|
|
198
|
+
const text = value.trim().toLowerCase();
|
|
199
|
+
if (text === "always" || text === "1" || text === "true" || text === "yes" || text === "on") {
|
|
200
|
+
return "always";
|
|
201
|
+
}
|
|
202
|
+
if (text === "never" || text === "0" || text === "false" || text === "no" || text === "off") {
|
|
203
|
+
return "never";
|
|
204
|
+
}
|
|
205
|
+
return "auto";
|
|
206
|
+
}
|
|
207
|
+
function normalizeStartupPortConflictPolicy(value) {
|
|
208
|
+
const text = value.trim().toLowerCase().replace(/-/g, "_");
|
|
209
|
+
if (text === "terminate_pushpals" || text === "kill_pushpals" || text === "auto_kill_pushpals") {
|
|
210
|
+
return "terminate_pushpals";
|
|
211
|
+
}
|
|
212
|
+
return "fail";
|
|
213
|
+
}
|
|
214
|
+
function defaultApiKeyForBackend(backend, endpoint) {
|
|
215
|
+
const normalizedBackend = backend.trim().toLowerCase();
|
|
216
|
+
const normalizedEndpoint = endpoint.trim().toLowerCase();
|
|
217
|
+
const openAiKey = (process.env.OPENAI_API_KEY ?? "").trim();
|
|
218
|
+
if (normalizedBackend === "openai") {
|
|
219
|
+
return openAiKey;
|
|
220
|
+
}
|
|
221
|
+
if (normalizedBackend === "lmstudio") {
|
|
222
|
+
return "lmstudio";
|
|
223
|
+
}
|
|
224
|
+
if (normalizedEndpoint.includes("api.openai.com")) {
|
|
225
|
+
return openAiKey;
|
|
226
|
+
}
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
function resolveLlmConfig(serviceNode, envPrefix, defaults, globalSessionId) {
|
|
230
|
+
const llmNode = getObject(serviceNode, "llm");
|
|
231
|
+
const backend = normalizeBackend(firstNonEmpty(process.env[`${envPrefix}_LLM_BACKEND`], asString(llmNode.backend, defaults.backend), defaults.backend));
|
|
232
|
+
const endpoint = firstNonEmpty(process.env[`${envPrefix}_LLM_ENDPOINT`], asString(llmNode.endpoint, defaults.endpoint), defaults.endpoint);
|
|
233
|
+
const model = firstNonEmpty(process.env[`${envPrefix}_LLM_MODEL`], asString(llmNode.model, defaults.model), defaults.model);
|
|
234
|
+
const sessionId = firstNonEmpty(process.env[`${envPrefix}_LLM_SESSION_ID`], asString(llmNode.session_id, defaults.sessionId), process.env.PUSHPALS_LLM_SESSION_ID, globalSessionId);
|
|
235
|
+
const apiKey = firstNonEmpty(process.env[`${envPrefix}_LLM_API_KEY`], defaultApiKeyForBackend(backend, endpoint));
|
|
236
|
+
const reasoningEffort = firstNonEmpty(process.env[`${envPrefix}_LLM_REASONING_EFFORT`], asString(llmNode.reasoning_effort, ""));
|
|
237
|
+
const codexAuthMode = firstNonEmpty(process.env[`${envPrefix}_LLM_CODEX_AUTH_MODE`], asString(llmNode.codex_auth_mode, ""));
|
|
238
|
+
const codexBin = firstNonEmpty(process.env[`${envPrefix}_LLM_CODEX_BIN`], asString(llmNode.codex_bin, ""));
|
|
239
|
+
const codexTimeoutMs = Math.max(1e4, asInt(parseIntEnv(`${envPrefix}_LLM_CODEX_TIMEOUT_MS`) ?? llmNode.codex_timeout_ms, 120000));
|
|
240
|
+
return {
|
|
241
|
+
backend,
|
|
242
|
+
endpoint,
|
|
243
|
+
model,
|
|
244
|
+
sessionId,
|
|
245
|
+
apiKey,
|
|
246
|
+
reasoningEffort,
|
|
247
|
+
codexAuthMode,
|
|
248
|
+
codexBin,
|
|
249
|
+
codexTimeoutMs
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function loadPushPalsConfig(options = {}) {
|
|
253
|
+
const projectRoot = resolve(options.projectRoot ?? PROJECT_ROOT);
|
|
254
|
+
const configDir = resolveRuntimeConfigDir(projectRoot, options.configDir);
|
|
255
|
+
const legacyConfigDir = resolvePathFromRoot(projectRoot, LEGACY_CONFIG_DIR);
|
|
256
|
+
const fallbackConfigDir = !options.configDir && configDir !== legacyConfigDir ? legacyConfigDir : "";
|
|
257
|
+
const cacheKey = `${projectRoot}::${configDir}::${process.env.PUSHPALS_PROFILE ?? ""}`;
|
|
258
|
+
if (!options.reload && cachedConfig && cachedConfigKey === cacheKey) {
|
|
259
|
+
return cachedConfig;
|
|
260
|
+
}
|
|
261
|
+
const defaultToml = parseTomlWithLegacyFallback(join(configDir, "default.toml"), fallbackConfigDir ? join(fallbackConfigDir, "default.toml") : undefined);
|
|
262
|
+
const preferredProfile = firstNonEmpty(process.env.PUSHPALS_PROFILE, asString(defaultToml.profile, "dev"), "dev");
|
|
263
|
+
const profileToml = parseTomlWithLegacyFallback(join(configDir, `${preferredProfile}.toml`), fallbackConfigDir ? join(fallbackConfigDir, `${preferredProfile}.toml`) : undefined);
|
|
264
|
+
const localExampleToml = parseTomlWithLegacyFallback(join(configDir, "local.example.toml"), fallbackConfigDir ? join(fallbackConfigDir, "local.example.toml") : undefined);
|
|
265
|
+
const localToml = parseTomlWithLegacyFallback(join(configDir, "local.toml"), fallbackConfigDir ? join(fallbackConfigDir, "local.toml") : undefined);
|
|
266
|
+
const merged = mergeDeep(mergeDeep(mergeDeep(defaultToml, profileToml), localExampleToml), localToml);
|
|
267
|
+
const profile = firstNonEmpty(process.env.PUSHPALS_PROFILE, asString(merged.profile, preferredProfile), preferredProfile);
|
|
268
|
+
const sessionId = firstNonEmpty(process.env.PUSHPALS_SESSION_ID, asString(merged.session_id, "dev"), "dev");
|
|
269
|
+
const llmNode = getObject(merged, "llm");
|
|
270
|
+
const lmStudioNode = getObject(llmNode, "lmstudio");
|
|
271
|
+
const lmStudioContextWindow = Math.max(512, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_CONTEXT_WINDOW") ?? lmStudioNode.context_window, 4096));
|
|
272
|
+
const lmStudioMinOutputTokens = Math.max(64, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_MIN_OUTPUT_TOKENS") ?? lmStudioNode.min_output_tokens, 256));
|
|
273
|
+
const lmStudioTokenSafetyMargin = Math.max(16, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_TOKEN_SAFETY_MARGIN") ?? lmStudioNode.token_safety_margin, 64));
|
|
274
|
+
const lmStudioBatchTailMessages = Math.max(1, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_TAIL_MESSAGES") ?? lmStudioNode.batch_tail_messages, 3));
|
|
275
|
+
const lmStudioBatchChunkTokens = Math.max(0, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_CHUNK_TOKENS") ?? lmStudioNode.batch_chunk_tokens, 0));
|
|
276
|
+
const lmStudioBatchMemoryChars = Math.max(0, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_MEMORY_CHARS") ?? lmStudioNode.batch_memory_chars, 0));
|
|
277
|
+
const pathsNode = getObject(merged, "paths");
|
|
278
|
+
const dataDir = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.PUSHPALS_DATA_DIR, asString(pathsNode.data_dir, "outputs/data")));
|
|
279
|
+
const sharedDbPath = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.PUSHPALS_DB_PATH, asString(pathsNode.shared_db_path, join(dataDir, "pushpals.db"))));
|
|
280
|
+
const remotebuddyDbPath = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.REMOTEBUDDY_DB_PATH, asString(pathsNode.remotebuddy_db_path, join(dataDir, "remotebuddy-state.db"))));
|
|
281
|
+
const serverNode = getObject(merged, "server");
|
|
282
|
+
const serverPort = Math.max(1, asInt(parseIntEnv("PUSHPALS_PORT") ?? serverNode.port, 3001));
|
|
283
|
+
const serverUrl = firstNonEmpty(process.env.PUSHPALS_SERVER_URL, asString(serverNode.url, `http://localhost:${serverPort}`), `http://localhost:${serverPort}`);
|
|
284
|
+
const serverHost = asString(serverNode.host, "0.0.0.0");
|
|
285
|
+
const debugHttp = parseBoolEnv("PUSHPALS_DEBUG_HTTP") ?? asBoolean(serverNode.debug_http, false);
|
|
286
|
+
const staleClaimTtlMs = Math.max(5000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_TTL_MS") ?? serverNode.stale_claim_ttl_ms, 120000));
|
|
287
|
+
const staleClaimSweepIntervalMs = Math.max(1000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_SWEEP_INTERVAL_MS") ?? serverNode.stale_claim_sweep_interval_ms, 5000));
|
|
288
|
+
const globalStatusHeartbeatMs = parseIntEnv("PUSHPALS_STATUS_HEARTBEAT_MS");
|
|
289
|
+
const localNode = getObject(merged, "localbuddy");
|
|
290
|
+
const localPort = Math.max(1, asInt(parseIntEnv("LOCAL_AGENT_PORT") ?? localNode.port, 3003));
|
|
291
|
+
const localStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("LOCALBUDDY_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? localNode.status_heartbeat_ms, 120000));
|
|
292
|
+
const localLlm = resolveLlmConfig(localNode, "LOCALBUDDY", {
|
|
293
|
+
backend: "lmstudio",
|
|
294
|
+
endpoint: "http://127.0.0.1:1234",
|
|
295
|
+
model: "local-model",
|
|
296
|
+
sessionId: "localbuddy-dev"
|
|
297
|
+
}, sessionId);
|
|
298
|
+
const remoteNode = getObject(merged, "remotebuddy");
|
|
299
|
+
const remoteStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? remoteNode.status_heartbeat_ms, 120000));
|
|
300
|
+
const remotePollMs = Math.max(200, asInt(parseIntEnv("REMOTEBUDDY_POLL_MS") ?? remoteNode.poll_ms, 2000));
|
|
301
|
+
const remoteLlm = resolveLlmConfig(remoteNode, "REMOTEBUDDY", {
|
|
302
|
+
backend: "lmstudio",
|
|
303
|
+
endpoint: "http://127.0.0.1:1234",
|
|
304
|
+
model: "local-model",
|
|
305
|
+
sessionId: "remotebuddy-dev"
|
|
306
|
+
}, sessionId);
|
|
307
|
+
const remoteMemoryNode = getObject(remoteNode, "memory");
|
|
308
|
+
const remoteMemoryEnabled = parseBoolEnv("REMOTEBUDDY_MEMORY_ENABLED") ?? asBoolean(remoteMemoryNode.enabled, true);
|
|
309
|
+
const remoteMemoryIncludeCrossSession = parseBoolEnv("REMOTEBUDDY_MEMORY_INCLUDE_CROSS_SESSION") ?? asBoolean(remoteMemoryNode.include_cross_session, true);
|
|
310
|
+
const remoteMemoryMaxRecallItems = Math.max(1, Math.min(128, asInt(parseIntEnv("REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS") ?? remoteMemoryNode.max_recall_items, DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS)));
|
|
311
|
+
const remoteMemoryMaxRecallChars = Math.max(120, Math.min(64000, asInt(parseIntEnv("REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS") ?? remoteMemoryNode.max_recall_chars, DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS)));
|
|
312
|
+
const remoteMemoryMaxSummaryChars = Math.max(64, Math.min(16000, asInt(parseIntEnv("REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS") ?? remoteMemoryNode.max_summary_chars, DEFAULT_REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS)));
|
|
313
|
+
const remoteMemoryRetentionDays = Math.max(1, Math.min(3650, asInt(parseIntEnv("REMOTEBUDDY_MEMORY_RETENTION_DAYS") ?? remoteMemoryNode.retention_days, DEFAULT_REMOTEBUDDY_MEMORY_RETENTION_DAYS)));
|
|
314
|
+
const remoteAutonomyNode = getObject(remoteNode, "autonomy");
|
|
315
|
+
const remoteAutonomyReplayNode = getObject(remoteAutonomyNode, "replay");
|
|
316
|
+
const remoteAutonomyDispatchByTypeCfg = {
|
|
317
|
+
flaky_test: 4,
|
|
318
|
+
lint_fix: 3,
|
|
319
|
+
type_fix: 3,
|
|
320
|
+
small_refactor: 2,
|
|
321
|
+
feature_small: 2,
|
|
322
|
+
feature_medium: 1,
|
|
323
|
+
feature_large: 0,
|
|
324
|
+
docs: 1,
|
|
325
|
+
dep_bump: 0
|
|
326
|
+
};
|
|
327
|
+
const remoteAutonomyDispatchByType = {
|
|
328
|
+
...remoteAutonomyDispatchByTypeCfg,
|
|
329
|
+
...asStringNumberRecord(remoteAutonomyNode.max_dispatch_per_hour_by_type)
|
|
330
|
+
};
|
|
331
|
+
const remoteAutonomyDispatchByComponentCfg = {
|
|
332
|
+
"apps/server": 3,
|
|
333
|
+
"apps/remotebuddy": 2,
|
|
334
|
+
"apps/workerpals": 2,
|
|
335
|
+
"apps/client": 2,
|
|
336
|
+
"packages/protocol": 1,
|
|
337
|
+
"packages/shared": 2,
|
|
338
|
+
"tests/integration": 2,
|
|
339
|
+
"tests/unit": 2
|
|
340
|
+
};
|
|
341
|
+
const remoteAutonomyDispatchByComponentRaw = asStringNumberRecord(remoteAutonomyNode.max_dispatch_per_hour_by_component);
|
|
342
|
+
const remoteAutonomyDispatchByComponent = {
|
|
343
|
+
...remoteAutonomyDispatchByComponentCfg
|
|
344
|
+
};
|
|
345
|
+
const normalizeAutonomyComponentKey = (value) => value.trim().toLowerCase().replace(/\\/g, "/").replace(/_+/g, "/").replace(/-+/g, "/").replace(/\/+/g, "/");
|
|
346
|
+
const canonicalComponentByNormalized = new Map(Object.keys(remoteAutonomyDispatchByComponentCfg).map((key) => [normalizeAutonomyComponentKey(key), key]));
|
|
347
|
+
for (const [rawKey, rawValue] of Object.entries(remoteAutonomyDispatchByComponentRaw)) {
|
|
348
|
+
const normalized = normalizeAutonomyComponentKey(rawKey);
|
|
349
|
+
const canonical = canonicalComponentByNormalized.get(normalized);
|
|
350
|
+
if (!canonical)
|
|
351
|
+
continue;
|
|
352
|
+
const parsed = typeof rawValue === "number" ? rawValue : typeof rawValue === "string" ? Number.parseInt(rawValue.trim(), 10) : Number.NaN;
|
|
353
|
+
remoteAutonomyDispatchByComponent[canonical] = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
|
|
354
|
+
}
|
|
355
|
+
const workerNode = getObject(merged, "workerpals");
|
|
356
|
+
const workerOpenHandsNode = getObject(workerNode, "openhands");
|
|
357
|
+
const workerPollMs = Math.max(200, asInt(parseIntEnv("WORKERPALS_POLL_MS") ?? workerNode.poll_ms, 2000));
|
|
358
|
+
const workerHeartbeatMs = Math.max(200, asInt(parseIntEnv("WORKERPALS_HEARTBEAT_MS") ?? workerNode.heartbeat_ms, 5000));
|
|
359
|
+
const workerExecutor = firstNonEmpty(process.env.WORKERPALS_EXECUTOR, asString(workerNode.executor, "openhands"), "openhands").toLowerCase();
|
|
360
|
+
const workerOpenHandsPython = firstNonEmpty(process.env.WORKERPALS_OPENHANDS_PYTHON, asString(workerNode.openhands_python, "python"), "python");
|
|
361
|
+
const workerOpenHandsTimeoutMs = Math.max(1e4, asInt(parseIntEnv("WORKERPALS_OPENHANDS_TIMEOUT_MS") ?? workerNode.openhands_timeout_ms, 1800000));
|
|
362
|
+
const workerMiniswePython = firstNonEmpty(process.env.WORKERPALS_MINISWE_PYTHON, asString(workerNode.miniswe_python, "python"), "python");
|
|
363
|
+
const workerMinisweTimeoutMs = Math.max(1e4, asInt(parseIntEnv("WORKERPALS_MINISWE_TIMEOUT_MS") ?? workerNode.miniswe_timeout_ms, 1800000));
|
|
364
|
+
const workerOpenAICodexPython = firstNonEmpty(process.env.PUSHPALS_OPENAI_CODEX_PYTHON, asString(workerNode.openai_codex_python, "python"), "python");
|
|
365
|
+
const workerOpenAICodexTimeoutMs = Math.max(1e4, asInt(workerNode.openai_codex_timeout_ms, 7200000));
|
|
366
|
+
const workerQualityMaxAutoRevisions = Math.max(0, Math.min(10, asInt(parseIntEnv("WORKERPALS_QUALITY_MAX_AUTO_REVISIONS") ?? workerNode.quality_max_auto_revisions, 4)));
|
|
367
|
+
const workerFileModifyingJobs = (() => {
|
|
368
|
+
const envRaw = firstNonEmpty(process.env.WORKERPALS_FILE_MODIFYING_JOBS);
|
|
369
|
+
const parsed = envRaw ? envRaw.split(",").map((entry) => entry.trim()).filter(Boolean) : asStringArray(workerNode.file_modifying_jobs);
|
|
370
|
+
const out = parsed.length > 0 ? parsed : DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS;
|
|
371
|
+
return [...new Set(out)];
|
|
372
|
+
})();
|
|
373
|
+
const workerOutputMaxChars = Math.max(8192, Math.min(4194304, asInt(parseIntEnv("WORKERPALS_OUTPUT_MAX_CHARS") ?? workerNode.output_max_chars, DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS)));
|
|
374
|
+
const workerOutputMaxLines = Math.max(50, Math.min(20000, asInt(parseIntEnv("WORKERPALS_OUTPUT_MAX_LINES") ?? workerNode.output_max_lines, DEFAULT_WORKERPALS_OUTPUT_MAX_LINES)));
|
|
375
|
+
const workerOutputMaxHeadLines = Math.max(1, Math.min(workerOutputMaxLines, asInt(parseIntEnv("WORKERPALS_OUTPUT_MAX_HEAD_LINES") ?? workerNode.output_max_head_lines, DEFAULT_WORKERPALS_OUTPUT_MAX_HEAD_LINES)));
|
|
376
|
+
const workerQualityValidationStepTimeoutMs = Math.max(1000, asInt(parseIntEnv("WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS") ?? workerNode.quality_validation_step_timeout_ms, DEFAULT_WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS));
|
|
377
|
+
const workerQualityCriticTimeoutMs = Math.max(1000, asInt(parseIntEnv("WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS") ?? workerNode.quality_critic_timeout_ms, DEFAULT_WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS));
|
|
378
|
+
const workerQualitySoftPassOnExhausted = parseBoolEnv("WORKERPALS_QUALITY_SOFT_PASS_ON_EXHAUSTED") ?? asBoolean(workerNode.quality_soft_pass_on_exhausted, true);
|
|
379
|
+
const workerQualityCriticMinScore = (() => {
|
|
380
|
+
const configThresholdRaw = workerNode.quality_critic_min_score == null ? "" : String(workerNode.quality_critic_min_score);
|
|
381
|
+
const raw = firstNonEmpty(process.env.WORKERPALS_QUALITY_CRITIC_MIN_SCORE, configThresholdRaw, String(DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE));
|
|
382
|
+
const parsed = Number.parseFloat(raw);
|
|
383
|
+
if (!Number.isFinite(parsed))
|
|
384
|
+
return DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE;
|
|
385
|
+
return Math.max(0, Math.min(10, parsed));
|
|
386
|
+
})();
|
|
387
|
+
const workerQualityCriticMaxDiffChars = Math.max(256, Math.min(524288, asInt(parseIntEnv("WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS") ?? workerNode.quality_critic_max_diff_chars, DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS)));
|
|
388
|
+
const workerQualityCriticMaxValidationOutputChars = Math.max(256, Math.min(524288, asInt(parseIntEnv("WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS") ?? workerNode.quality_critic_max_validation_output_chars, DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS)));
|
|
389
|
+
const workerExecutorResultPrefix = (() => {
|
|
390
|
+
if (process.env.WORKERPALS_EXECUTOR_RESULT_PREFIX !== undefined) {
|
|
391
|
+
const raw = process.env.WORKERPALS_EXECUTOR_RESULT_PREFIX;
|
|
392
|
+
if (typeof raw === "string" && raw.length > 0)
|
|
393
|
+
return raw;
|
|
394
|
+
}
|
|
395
|
+
if (Object.prototype.hasOwnProperty.call(workerNode, "executor_result_prefix") && typeof workerNode.executor_result_prefix === "string" && workerNode.executor_result_prefix.length > 0) {
|
|
396
|
+
return workerNode.executor_result_prefix;
|
|
397
|
+
}
|
|
398
|
+
return DEFAULT_WORKERPALS_EXECUTOR_RESULT_PREFIX;
|
|
399
|
+
})();
|
|
400
|
+
const workerOpenHandsStuckGuardEnabled = parseBoolEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_ENABLED") ?? asBoolean(workerNode.openhands_stuck_guard_enabled, true);
|
|
401
|
+
const workerOpenHandsStuckGuardExploreLimit = Math.max(6, asInt(parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_EXPLORE_LIMIT") ?? workerNode.openhands_stuck_guard_explore_limit, 18));
|
|
402
|
+
const workerOpenHandsStuckGuardMinElapsedMs = Math.max(60000, asInt(parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_MIN_ELAPSED_MS") ?? workerNode.openhands_stuck_guard_min_elapsed_ms, 180000));
|
|
403
|
+
const workerOpenHandsStuckGuardBroadScanLimit = Math.max(1, asInt(parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_BROAD_SCAN_LIMIT") ?? workerNode.openhands_stuck_guard_broad_scan_limit, 2));
|
|
404
|
+
const workerOpenHandsStuckGuardNoProgressMaxMs = Math.max(60000, asInt(parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_NO_PROGRESS_MAX_MS") ?? workerNode.openhands_stuck_guard_no_progress_max_ms, 300000));
|
|
405
|
+
const workerOpenHandsAutoSteerEnabled = parseBoolEnv("WORKERPALS_OPENHANDS_AUTO_STEER_ENABLED") ?? asBoolean(workerOpenHandsNode.auto_steer_enabled, true);
|
|
406
|
+
const workerOpenHandsAutoSteerInitialDelaySec = Math.max(0, Math.min(600, asInt(parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_INITIAL_DELAY_SEC") ?? workerOpenHandsNode.auto_steer_initial_delay_sec, 90)));
|
|
407
|
+
const workerOpenHandsAutoSteerIntervalSec = Math.max(15, Math.min(600, asInt(parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_INTERVAL_SEC") ?? workerOpenHandsNode.auto_steer_interval_sec, 60)));
|
|
408
|
+
const workerOpenHandsAutoSteerMaxNudges = Math.max(0, Math.min(120, asInt(parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_MAX_NUDGES") ?? workerOpenHandsNode.auto_steer_max_nudges, 30)));
|
|
409
|
+
const workerRequirePush = parseBoolEnv("WORKERPALS_REQUIRE_PUSH") ?? asBoolean(workerNode.require_push, false);
|
|
410
|
+
const workerPushAgentBranchEnv = parseBoolEnv("WORKERPALS_PUSH_AGENT_BRANCH");
|
|
411
|
+
const workerPushAgentBranch = workerRequirePush || (workerPushAgentBranchEnv ?? asBoolean(workerNode.push_agent_branch, false));
|
|
412
|
+
const workerSkipDockerSelfCheck = parseBoolEnv("WORKERPALS_SKIP_DOCKER_SELF_CHECK") ?? asBoolean(workerNode.skip_docker_self_check, false);
|
|
413
|
+
const workerDockerAgentStartupTimeoutMs = Math.max(1e4, Math.min(180000, asInt(parseIntEnv("WORKERPALS_DOCKER_AGENT_STARTUP_TIMEOUT_MS") ?? workerNode.docker_agent_startup_timeout_ms, 45000)));
|
|
414
|
+
const workerDockerWarmMaxAttempts = Math.max(1, Math.min(5, asInt(parseIntEnv("WORKERPALS_DOCKER_WARM_MAX_ATTEMPTS") ?? workerNode.docker_warm_max_attempts, 3)));
|
|
415
|
+
const workerDockerWarmRetryBackoffMs = Math.max(250, Math.min(60000, asInt(parseIntEnv("WORKERPALS_DOCKER_WARM_RETRY_BACKOFF_MS") ?? workerNode.docker_warm_retry_backoff_ms, 2000)));
|
|
416
|
+
const workerDockerJobMaxAttempts = Math.max(1, Math.min(3, asInt(parseIntEnv("WORKERPALS_DOCKER_JOB_MAX_ATTEMPTS") ?? workerNode.docker_job_max_attempts, 2)));
|
|
417
|
+
const workerDockerJobRetryBackoffMs = Math.max(250, Math.min(60000, asInt(parseIntEnv("WORKERPALS_DOCKER_JOB_RETRY_BACKOFF_MS") ?? workerNode.docker_job_retry_backoff_ms, 3000)));
|
|
418
|
+
const workerDockerWarmMemoryMb = Math.max(512, Math.min(32768, asInt(parseIntEnv("WORKERPALS_DOCKER_WARM_MEMORY_MB") ?? workerNode.docker_warm_memory_mb, 2048)));
|
|
419
|
+
const workerDockerWarmCpus = Math.max(1, Math.min(16, asInt(parseIntEnv("WORKERPALS_DOCKER_WARM_CPUS") ?? workerNode.docker_warm_cpus, 2)));
|
|
420
|
+
const workerLlm = resolveLlmConfig(workerNode, "WORKERPALS", {
|
|
421
|
+
backend: "lmstudio",
|
|
422
|
+
endpoint: "http://127.0.0.1:1234",
|
|
423
|
+
model: "local-model",
|
|
424
|
+
sessionId: "workerpals-dev"
|
|
425
|
+
}, sessionId);
|
|
426
|
+
const scmNode = getObject(merged, "source_control_manager");
|
|
427
|
+
const scmRepoPath = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REPO_PATH, asString(scmNode.repo_path, ".worktrees/source_control_manager"), ".worktrees/source_control_manager"));
|
|
428
|
+
const scmRemote = asString(process.env.SOURCE_CONTROL_MANAGER_REMOTE ?? scmNode.remote, "origin");
|
|
429
|
+
const scmMainBranch = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_MAIN_BRANCH, process.env.PUSHPALS_INTEGRATION_BRANCH, asString(scmNode.pushpals_branch, "main_agents"), "main_agents");
|
|
430
|
+
const scmBaseBranch = firstNonEmpty(process.env.PUSHPALS_INTEGRATION_BASE_BRANCH, asString(scmNode.base_branch, "main"), "main");
|
|
431
|
+
const scmBranchPrefix = asString(process.env.SOURCE_CONTROL_MANAGER_BRANCH_PREFIX ?? scmNode.branch_prefix, "agent/");
|
|
432
|
+
const scmPollIntervalSeconds = Math.max(1, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_POLL_INTERVAL_SECONDS") ?? scmNode.poll_interval_seconds, 10));
|
|
433
|
+
const scmChecks = asCheckArray(scmNode.checks);
|
|
434
|
+
const scmStateDir = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_STATE_DIR, asString(scmNode.state_dir, join(dataDir, "source_control_manager")), join(dataDir, "source_control_manager")));
|
|
435
|
+
const scmPort = Math.max(1, Math.min(65535, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_PORT") ?? scmNode.port, 3002)));
|
|
436
|
+
const scmDeleteAfterMerge = parseBoolEnv("SOURCE_CONTROL_MANAGER_DELETE_AFTER_MERGE") ?? asBoolean(scmNode.delete_after_merge, false);
|
|
437
|
+
const scmMaxAttempts = Math.max(1, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_MAX_ATTEMPTS") ?? scmNode.max_attempts, 3));
|
|
438
|
+
const scmMergeStrategyRaw = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_MERGE_STRATEGY, asString(scmNode.merge_strategy, "cherry-pick"), "cherry-pick");
|
|
439
|
+
const scmMergeStrategy = scmMergeStrategyRaw === "no-ff" || scmMergeStrategyRaw === "ff-only" ? scmMergeStrategyRaw : "cherry-pick";
|
|
440
|
+
let scmPushMainAfterMerge = asBoolean(scmNode.push_main_after_merge, true);
|
|
441
|
+
const scmPushMainAfterMergeEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_PUSH_MAIN_AFTER_MERGE");
|
|
442
|
+
if (scmPushMainAfterMergeEnv != null)
|
|
443
|
+
scmPushMainAfterMerge = scmPushMainAfterMergeEnv;
|
|
444
|
+
const scmNoPushEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_NO_PUSH");
|
|
445
|
+
if (scmNoPushEnv != null)
|
|
446
|
+
scmPushMainAfterMerge = !scmNoPushEnv;
|
|
447
|
+
let scmOpenPrAfterPush = asBoolean(scmNode.open_pr_after_push, true);
|
|
448
|
+
const scmOpenPrAfterPushEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_OPEN_PR_AFTER_PUSH");
|
|
449
|
+
if (scmOpenPrAfterPushEnv != null)
|
|
450
|
+
scmOpenPrAfterPush = scmOpenPrAfterPushEnv;
|
|
451
|
+
const scmDisableAutoPrEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_DISABLE_AUTO_PR");
|
|
452
|
+
if (scmDisableAutoPrEnv != null)
|
|
453
|
+
scmOpenPrAfterPush = !scmDisableAutoPrEnv;
|
|
454
|
+
const scmPrBaseBranch = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_PR_BASE_BRANCH, asString(scmNode.pr_base_branch, scmBaseBranch), scmBaseBranch);
|
|
455
|
+
const scmPrTitle = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_PR_TITLE, asString(scmNode.pr_title, ""));
|
|
456
|
+
const scmPrBody = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_PR_BODY, asString(scmNode.pr_body, ""));
|
|
457
|
+
const scmPrDraft = parseBoolEnv("SOURCE_CONTROL_MANAGER_PR_DRAFT") ?? asBoolean(scmNode.pr_draft, false);
|
|
458
|
+
const scmStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? scmNode.status_heartbeat_ms, 120000));
|
|
459
|
+
const scmSkipCleanCheck = parseBoolEnv("SOURCE_CONTROL_MANAGER_SKIP_CLEAN_CHECK") ?? asBoolean(scmNode.skip_clean_check, false);
|
|
460
|
+
const scmAutoCreateMainBranch = parseBoolEnv("SOURCE_CONTROL_MANAGER_AUTO_CREATE_MAIN_BRANCH") ?? asBoolean(scmNode.auto_create_main_branch, false);
|
|
461
|
+
const scmReviewAgentNode = getObject(scmNode, "review_agent");
|
|
462
|
+
const scmReviewAgentEnabled = parseBoolEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_ENABLED") ?? asBoolean(scmReviewAgentNode.enabled, false);
|
|
463
|
+
const scmReviewAgentPollIntervalMs = Math.max(5000, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_POLL_INTERVAL_MS") ?? scmReviewAgentNode.poll_interval_ms, 60000));
|
|
464
|
+
const scmReviewAgentReviewerMdPath = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_REVIEWER_MD_PATH, asString(scmReviewAgentNode.reviewer_md_path, "prompts/review_agent/reviewer.md"), "prompts/review_agent/reviewer.md");
|
|
465
|
+
const scmReviewAgentPassThreshold = (() => {
|
|
466
|
+
const configThresholdRaw = scmReviewAgentNode.pass_threshold == null ? "" : String(scmReviewAgentNode.pass_threshold);
|
|
467
|
+
const raw = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_PASS_THRESHOLD, configThresholdRaw, "9.5");
|
|
468
|
+
const parsed = Number.parseFloat(raw);
|
|
469
|
+
return Number.isFinite(parsed) ? Math.max(1, Math.min(10, parsed)) : 9.5;
|
|
470
|
+
})();
|
|
471
|
+
const scmReviewAgentMaxPrCommentsBeforeGiveUp = Math.max(1, Math.min(100, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_MAX_PR_COMMENTS_BEFORE_GIVE_UP") ?? scmReviewAgentNode.max_pr_comments_before_give_up, 10)));
|
|
472
|
+
const scmReviewAgentMergeMethodRaw = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_MERGE_METHOD, asString(scmReviewAgentNode.merge_method, "squash"), "squash").toLowerCase();
|
|
473
|
+
const scmReviewAgentMergeMethod = scmReviewAgentMergeMethodRaw === "merge" || scmReviewAgentMergeMethodRaw === "rebase" ? scmReviewAgentMergeMethodRaw : "squash";
|
|
474
|
+
const scmReviewAgentCodexBin = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_BIN, asString(scmReviewAgentNode.codex_bin, "bun x --yes @openai/codex"), "bun x --yes @openai/codex");
|
|
475
|
+
const scmReviewAgentCodexAuthMode = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_AUTH_MODE, asString(scmReviewAgentNode.codex_auth_mode, "chatgpt"), "chatgpt");
|
|
476
|
+
const scmReviewAgentCodexHomeDir = firstNonEmpty(process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_HOME_DIR, asString(scmReviewAgentNode.codex_home_dir, ""));
|
|
477
|
+
const scmReviewAgentCodexTimeoutMs = Math.max(30000, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_TIMEOUT_MS") ?? scmReviewAgentNode.codex_timeout_ms, 300000));
|
|
478
|
+
const startupNode = getObject(merged, "startup");
|
|
479
|
+
const startupWorkerImageRebuild = normalizeWorkerImageRebuildMode(firstNonEmpty(process.env.PUSHPALS_WORKER_IMAGE_REBUILD, asString(startupNode.worker_image_rebuild, "auto"), "auto"));
|
|
480
|
+
const startupLogConfigOnStart = parseBoolEnv("PUSHPALS_LOG_CONFIG_ON_START") ?? asBoolean(startupNode.log_config_on_start, true);
|
|
481
|
+
const startupSyncIntegrationWithMain = parseBoolEnv("PUSHPALS_SYNC_INTEGRATION_WITH_MAIN") ?? asBoolean(startupNode.sync_integration_with_main, true);
|
|
482
|
+
const startupSkipLlmPreflight = parseBoolEnv("PUSHPALS_SKIP_LLM_PREFLIGHT") ?? asBoolean(startupNode.skip_llm_preflight, false);
|
|
483
|
+
const startupAutoStartLmStudio = parseBoolEnv("PUSHPALS_AUTO_START_LMSTUDIO") ?? asBoolean(startupNode.auto_start_lmstudio, true);
|
|
484
|
+
const startupLmStudioReadyTimeoutMs = Math.max(1000, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_READY_TIMEOUT_MS") ?? startupNode.lmstudio_ready_timeout_ms, 120000));
|
|
485
|
+
const startupLmStudioCli = firstNonEmpty(process.env.PUSHPALS_LMSTUDIO_CLI, asString(startupNode.lmstudio_cli, "lms"), "lms");
|
|
486
|
+
const startupLmStudioPort = Math.max(1, Math.min(65535, asInt(parseIntEnv("PUSHPALS_LMSTUDIO_PORT") ?? startupNode.lmstudio_port, 1234)));
|
|
487
|
+
const startupLmStudioStartArgs = firstNonEmpty(process.env.PUSHPALS_LMSTUDIO_START_ARGS, asString(startupNode.lmstudio_start_args, ""));
|
|
488
|
+
const startupWarmup = parseBoolEnv("PUSHPALS_STARTUP_WARMUP") ?? asBoolean(startupNode.startup_warmup, true);
|
|
489
|
+
const startupWarmupTimeoutMs = Math.max(15000, asInt(parseIntEnv("PUSHPALS_STARTUP_WARMUP_TIMEOUT_MS") ?? startupNode.startup_warmup_timeout_ms, 120000));
|
|
490
|
+
const startupWarmupPollMs = Math.max(250, Math.min(5000, asInt(parseIntEnv("PUSHPALS_STARTUP_WARMUP_POLL_MS") ?? startupNode.startup_warmup_poll_ms, 1000)));
|
|
491
|
+
const startupAllowExternalClean = parseBoolEnv("PUSHPALS_ALLOW_EXTERNAL_CLEAN") ?? asBoolean(startupNode.allow_external_clean, false);
|
|
492
|
+
const startupPortPreflight = parseBoolEnv("PUSHPALS_STARTUP_PORT_PREFLIGHT") ?? asBoolean(startupNode.port_preflight, true);
|
|
493
|
+
const startupPortConflictPolicy = normalizeStartupPortConflictPolicy(firstNonEmpty(process.env.PUSHPALS_STARTUP_PORT_CONFLICT_POLICY, asString(startupNode.port_conflict_policy, "terminate_pushpals"), "terminate_pushpals"));
|
|
494
|
+
const clientNode = getObject(merged, "client");
|
|
495
|
+
const authToken = firstNonEmpty(process.env.PUSHPALS_AUTH_TOKEN) || null;
|
|
496
|
+
const gitToken = firstNonEmpty(process.env.PUSHPALS_GIT_TOKEN, process.env.GITHUB_TOKEN, process.env.GH_TOKEN) || null;
|
|
497
|
+
const config = {
|
|
498
|
+
projectRoot,
|
|
499
|
+
configDir,
|
|
500
|
+
profile,
|
|
501
|
+
sessionId,
|
|
502
|
+
authToken,
|
|
503
|
+
gitToken,
|
|
504
|
+
llm: {
|
|
505
|
+
lmstudio: {
|
|
506
|
+
contextWindow: lmStudioContextWindow,
|
|
507
|
+
minOutputTokens: lmStudioMinOutputTokens,
|
|
508
|
+
tokenSafetyMargin: lmStudioTokenSafetyMargin,
|
|
509
|
+
batchTailMessages: lmStudioBatchTailMessages,
|
|
510
|
+
batchChunkTokens: lmStudioBatchChunkTokens,
|
|
511
|
+
batchMemoryChars: lmStudioBatchMemoryChars
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
paths: {
|
|
515
|
+
dataDir,
|
|
516
|
+
sharedDbPath,
|
|
517
|
+
remotebuddyDbPath
|
|
518
|
+
},
|
|
519
|
+
server: {
|
|
520
|
+
url: serverUrl,
|
|
521
|
+
host: serverHost,
|
|
522
|
+
port: serverPort,
|
|
523
|
+
debugHttp,
|
|
524
|
+
staleClaimTtlMs,
|
|
525
|
+
staleClaimSweepIntervalMs
|
|
526
|
+
},
|
|
527
|
+
localbuddy: {
|
|
528
|
+
port: localPort,
|
|
529
|
+
statusHeartbeatMs: localStatusHeartbeatMs,
|
|
530
|
+
llm: localLlm
|
|
531
|
+
},
|
|
532
|
+
remotebuddy: {
|
|
533
|
+
pollMs: remotePollMs,
|
|
534
|
+
statusHeartbeatMs: remoteStatusHeartbeatMs,
|
|
535
|
+
workerpalOnlineTtlMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_ONLINE_TTL_MS") ?? remoteNode.workerpal_online_ttl_ms, 15000)),
|
|
536
|
+
waitForWorkerpalMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_WAIT_FOR_WORKERPAL_MS") ?? remoteNode.wait_for_workerpal_ms, 15000)),
|
|
537
|
+
autoSpawnWorkerpals: parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ?? asBoolean(remoteNode.auto_spawn_workerpals, true),
|
|
538
|
+
maxWorkerpals: Math.max(1, asInt(remoteNode.max_workerpals, 20)),
|
|
539
|
+
workerpalStartupTimeoutMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS") ?? remoteNode.workerpal_startup_timeout_ms, 1e4)),
|
|
540
|
+
workerpalDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_DOCKER") ?? asBoolean(remoteNode.workerpal_docker, true),
|
|
541
|
+
workerpalRequireDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_REQUIRE_DOCKER") ?? asBoolean(remoteNode.workerpal_require_docker, true),
|
|
542
|
+
workerpalImage: firstNonEmpty(process.env.REMOTEBUDDY_WORKERPAL_IMAGE, asString(remoteNode.workerpal_image, "")) || null,
|
|
543
|
+
workerpalPollMs: asIntOrNull(parseIntEnv("REMOTEBUDDY_WORKERPAL_POLL_MS")) ?? asIntOrNull(remoteNode.workerpal_poll_ms),
|
|
544
|
+
workerpalHeartbeatMs: asIntOrNull(parseIntEnv("REMOTEBUDDY_WORKERPAL_HEARTBEAT_MS")) ?? asIntOrNull(remoteNode.workerpal_heartbeat_ms),
|
|
545
|
+
workerpalLabels: firstNonEmpty(process.env.REMOTEBUDDY_WORKERPAL_LABELS) ? firstNonEmpty(process.env.REMOTEBUDDY_WORKERPAL_LABELS).split(",").map((value) => value.trim()).filter(Boolean) : asStringArray(remoteNode.workerpal_labels),
|
|
546
|
+
executionBudgetInteractiveMs: Math.max(60000, asInt(parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_INTERACTIVE_MS") ?? remoteNode.execution_budget_interactive_ms, 300000)),
|
|
547
|
+
executionBudgetNormalMs: Math.max(120000, asInt(parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_NORMAL_MS") ?? remoteNode.execution_budget_normal_ms, 900000)),
|
|
548
|
+
executionBudgetBackgroundMs: Math.max(180000, asInt(parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_BACKGROUND_MS") ?? remoteNode.execution_budget_background_ms, 1800000)),
|
|
549
|
+
finalizationBudgetMs: Math.max(30000, asInt(parseIntEnv("REMOTEBUDDY_FINALIZATION_BUDGET_MS") ?? remoteNode.finalization_budget_ms, 120000)),
|
|
550
|
+
crashRestartEnabled: parseBoolEnv("REMOTEBUDDY_CRASH_RESTART_ENABLED") ?? asBoolean(remoteNode.crash_restart_enabled, true),
|
|
551
|
+
crashRestartMaxRestarts: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_CRASH_RESTART_MAX_RESTARTS") ?? remoteNode.crash_restart_max_restarts, 3)),
|
|
552
|
+
crashRestartBackoffMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_CRASH_RESTART_BACKOFF_MS") ?? remoteNode.crash_restart_backoff_ms, 3000)),
|
|
553
|
+
memory: {
|
|
554
|
+
enabled: remoteMemoryEnabled,
|
|
555
|
+
includeCrossSession: remoteMemoryIncludeCrossSession,
|
|
556
|
+
maxRecallItems: remoteMemoryMaxRecallItems,
|
|
557
|
+
maxRecallChars: remoteMemoryMaxRecallChars,
|
|
558
|
+
maxSummaryChars: remoteMemoryMaxSummaryChars,
|
|
559
|
+
retentionDays: remoteMemoryRetentionDays
|
|
560
|
+
},
|
|
561
|
+
autonomy: {
|
|
562
|
+
enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, false),
|
|
563
|
+
killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
|
|
564
|
+
tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
|
|
565
|
+
heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
|
|
566
|
+
visionContextMaxChars: Math.max(1000, Math.min(1e6, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ?? remoteAutonomyNode.vision_context_max_chars, 65536))),
|
|
567
|
+
ideationBudgetMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ?? remoteAutonomyNode.ideation_budget_ms, 20000)),
|
|
568
|
+
llmTimeoutMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_LLM_TIMEOUT_MS") ?? remoteAutonomyNode.llm_timeout_ms, 12000)),
|
|
569
|
+
allowDirtyWorktree: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_DIRTY_WORKTREE") ?? asBoolean(remoteAutonomyNode.allow_dirty_worktree, false),
|
|
570
|
+
ideationMaxCandidates: Math.max(1, Math.min(100, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_MAX_CANDIDATES") ?? remoteAutonomyNode.ideation_max_candidates, 20))),
|
|
571
|
+
topK: Math.max(1, Math.min(20, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TOP_K") ?? remoteAutonomyNode.top_k, 3))),
|
|
572
|
+
exploreRate: Math.max(0, Math.min(1, (() => {
|
|
573
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_EXPLORE_RATE, asString(remoteAutonomyNode.explore_rate, "0.3"), "0.3")));
|
|
574
|
+
return Number.isFinite(parsed) ? parsed : 0.3;
|
|
575
|
+
})())),
|
|
576
|
+
minConfidence: Math.max(0, Math.min(1, (() => {
|
|
577
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_MIN_CONFIDENCE, asString(remoteAutonomyNode.min_confidence, "0.65"), "0.65")));
|
|
578
|
+
return Number.isFinite(parsed) ? parsed : 0.65;
|
|
579
|
+
})())),
|
|
580
|
+
maxConcurrentObjectives: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_CONCURRENT_OBJECTIVES") ?? remoteAutonomyNode.max_concurrent_objectives, 2)),
|
|
581
|
+
maxDispatchPerHour: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_DISPATCH_PER_HOUR") ?? remoteAutonomyNode.max_dispatch_per_hour, 6)),
|
|
582
|
+
maxDispatchPerHourByType: remoteAutonomyDispatchByType,
|
|
583
|
+
maxDispatchPerHourByComponent: remoteAutonomyDispatchByComponent,
|
|
584
|
+
maxTokenUsagePerHour: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_TOKEN_USAGE_PER_HOUR") ?? remoteAutonomyNode.max_token_usage_per_hour, 120000)),
|
|
585
|
+
maxRuntimeMsPerHour: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_RUNTIME_MS_PER_HOUR") ?? remoteAutonomyNode.max_runtime_ms_per_hour, 5400000)),
|
|
586
|
+
cooldownFailStreakThreshold: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_COOLDOWN_FAIL_STREAK_THRESHOLD") ?? remoteAutonomyNode.cooldown_fail_streak_threshold, 2)),
|
|
587
|
+
cooldownMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_COOLDOWN_MS") ?? remoteAutonomyNode.cooldown_ms, 1800000)),
|
|
588
|
+
staleObjectiveTtlMs: Math.max(60000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STALE_OBJECTIVE_TTL_MS") ?? remoteAutonomyNode.stale_objective_ttl_ms, 2700000)),
|
|
589
|
+
staleObjectiveSweepIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STALE_OBJECTIVE_SWEEP_INTERVAL_MS") ?? remoteAutonomyNode.stale_objective_sweep_interval_ms, 60000)),
|
|
590
|
+
autoFreezeFailStreakThreshold: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_AUTO_FREEZE_FAIL_STREAK_THRESHOLD") ?? remoteAutonomyNode.auto_freeze_fail_streak_threshold, 3)),
|
|
591
|
+
autoFreezeDurationMs: Math.max(60000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_AUTO_FREEZE_DURATION_MS") ?? remoteAutonomyNode.auto_freeze_duration_ms, 1800000)),
|
|
592
|
+
evaluatorWindowHours: Math.max(1, Math.min(168, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_WINDOW_HOURS") ?? remoteAutonomyNode.evaluator_window_hours, 24))),
|
|
593
|
+
evaluatorMinSamples: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_MIN_SAMPLES") ?? remoteAutonomyNode.evaluator_min_samples, 6)),
|
|
594
|
+
evaluatorMinSuccessRate: Math.max(0, Math.min(1, (() => {
|
|
595
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_EVALUATOR_MIN_SUCCESS_RATE, asString(remoteAutonomyNode.evaluator_min_success_rate, "0.45"), "0.45")));
|
|
596
|
+
return Number.isFinite(parsed) ? parsed : 0.45;
|
|
597
|
+
})())),
|
|
598
|
+
evaluatorMaxRegretRate: Math.max(0, Math.min(1, (() => {
|
|
599
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_EVALUATOR_MAX_REGRET_RATE, asString(remoteAutonomyNode.evaluator_max_regret_rate, "0.35"), "0.35")));
|
|
600
|
+
return Number.isFinite(parsed) ? parsed : 0.35;
|
|
601
|
+
})())),
|
|
602
|
+
evaluatorRunIntervalMs: Math.max(1e4, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_RUN_INTERVAL_MS") ?? remoteAutonomyNode.evaluator_run_interval_ms, 120000)),
|
|
603
|
+
alertQueuePendingThreshold: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_ALERT_QUEUE_PENDING_THRESHOLD") ?? remoteAutonomyNode.alert_queue_pending_threshold, 20)),
|
|
604
|
+
alertJobFailureRateThreshold: Math.max(0, Math.min(1, (() => {
|
|
605
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_ALERT_JOB_FAILURE_RATE_THRESHOLD, asString(remoteAutonomyNode.alert_job_failure_rate_threshold, "0.3"), "0.3")));
|
|
606
|
+
return Number.isFinite(parsed) ? parsed : 0.3;
|
|
607
|
+
})())),
|
|
608
|
+
alertAutonomyFailureRateThreshold: Math.max(0, Math.min(1, (() => {
|
|
609
|
+
const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_ALERT_AUTONOMY_FAILURE_RATE_THRESHOLD, asString(remoteAutonomyNode.alert_autonomy_failure_rate_threshold, "0.45"), "0.45")));
|
|
610
|
+
return Number.isFinite(parsed) ? parsed : 0.45;
|
|
611
|
+
})())),
|
|
612
|
+
allowReadAnywhere: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ?? asBoolean(remoteAutonomyNode.allow_read_anywhere, false),
|
|
613
|
+
prFeedbackCommentRows: Math.max(1, Math.min(200, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_ROWS") ?? remoteAutonomyNode.pr_feedback_comment_rows, 16))),
|
|
614
|
+
prFeedbackCommentChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_CHARS") ?? remoteAutonomyNode.pr_feedback_comment_chars, 600))),
|
|
615
|
+
prFeedbackSummaryChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_SUMMARY_CHARS") ?? remoteAutonomyNode.pr_feedback_summary_chars, 600))),
|
|
616
|
+
questionTtlMs: Math.max(60000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_QUESTION_TTL_MS") ?? remoteAutonomyNode.question_ttl_ms, 259200000)),
|
|
617
|
+
policyVersion: firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_POLICY_VERSION, asString(remoteAutonomyNode.policy_version, "policy-v3.3"), "policy-v3.3"),
|
|
618
|
+
impactModelVersion: firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_IMPACT_MODEL_VERSION, asString(remoteAutonomyNode.impact_model_version, "impact-v1"), "impact-v1"),
|
|
619
|
+
replay: {
|
|
620
|
+
storePromptPayloads: parseBoolEnv("REMOTEBUDDY_AUTONOMY_REPLAY_STORE_PROMPT_PAYLOADS") ?? asBoolean(remoteAutonomyReplayNode.store_prompt_payloads, false),
|
|
621
|
+
maxRunsWithPayloads: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_REPLAY_MAX_RUNS_WITH_PAYLOADS") ?? remoteAutonomyReplayNode.max_runs_with_payloads, 50)),
|
|
622
|
+
maxPayloadBytes: Math.max(1024, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_REPLAY_MAX_PAYLOAD_BYTES") ?? remoteAutonomyReplayNode.max_payload_bytes, 262144))
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
llm: remoteLlm
|
|
626
|
+
},
|
|
627
|
+
workerpals: {
|
|
628
|
+
pollMs: workerPollMs,
|
|
629
|
+
heartbeatMs: workerHeartbeatMs,
|
|
630
|
+
executor: workerExecutor,
|
|
631
|
+
openhandsPython: workerOpenHandsPython,
|
|
632
|
+
openhandsTimeoutMs: workerOpenHandsTimeoutMs,
|
|
633
|
+
miniswePython: workerMiniswePython,
|
|
634
|
+
minisweTimeoutMs: workerMinisweTimeoutMs,
|
|
635
|
+
openaiCodexPython: workerOpenAICodexPython,
|
|
636
|
+
openaiCodexTimeoutMs: workerOpenAICodexTimeoutMs,
|
|
637
|
+
openhandsStuckGuardEnabled: workerOpenHandsStuckGuardEnabled,
|
|
638
|
+
openhandsStuckGuardExploreLimit: workerOpenHandsStuckGuardExploreLimit,
|
|
639
|
+
openhandsStuckGuardMinElapsedMs: workerOpenHandsStuckGuardMinElapsedMs,
|
|
640
|
+
openhandsStuckGuardBroadScanLimit: workerOpenHandsStuckGuardBroadScanLimit,
|
|
641
|
+
openhandsStuckGuardNoProgressMaxMs: workerOpenHandsStuckGuardNoProgressMaxMs,
|
|
642
|
+
openhandsAutoSteerEnabled: workerOpenHandsAutoSteerEnabled,
|
|
643
|
+
openhandsAutoSteerInitialDelaySec: workerOpenHandsAutoSteerInitialDelaySec,
|
|
644
|
+
openhandsAutoSteerIntervalSec: workerOpenHandsAutoSteerIntervalSec,
|
|
645
|
+
openhandsAutoSteerMaxNudges: workerOpenHandsAutoSteerMaxNudges,
|
|
646
|
+
requirePush: workerRequirePush,
|
|
647
|
+
pushAgentBranch: workerPushAgentBranch,
|
|
648
|
+
requireDocker: parseBoolEnv("WORKERPALS_REQUIRE_DOCKER") ?? asBoolean(workerNode.require_docker, false),
|
|
649
|
+
skipDockerSelfCheck: workerSkipDockerSelfCheck,
|
|
650
|
+
dockerImage: firstNonEmpty(process.env.WORKERPALS_DOCKER_IMAGE, asString(workerNode.docker_image, "pushpals-worker-sandbox:latest"), "pushpals-worker-sandbox:latest"),
|
|
651
|
+
dockerTimeoutMs: Math.max(1e4, asInt(parseIntEnv("WORKERPALS_DOCKER_TIMEOUT_MS") ?? workerNode.docker_timeout_ms, 7260000)),
|
|
652
|
+
dockerIdleTimeoutMs: Math.max(0, asInt(parseIntEnv("WORKERPALS_DOCKER_IDLE_TIMEOUT_MS") ?? workerNode.docker_idle_timeout_ms, 600000)),
|
|
653
|
+
dockerAgentStartupTimeoutMs: workerDockerAgentStartupTimeoutMs,
|
|
654
|
+
dockerWarmMaxAttempts: workerDockerWarmMaxAttempts,
|
|
655
|
+
dockerWarmRetryBackoffMs: workerDockerWarmRetryBackoffMs,
|
|
656
|
+
dockerJobMaxAttempts: workerDockerJobMaxAttempts,
|
|
657
|
+
dockerJobRetryBackoffMs: workerDockerJobRetryBackoffMs,
|
|
658
|
+
dockerWarmMemoryMb: workerDockerWarmMemoryMb,
|
|
659
|
+
dockerWarmCpus: workerDockerWarmCpus,
|
|
660
|
+
fileModifyingJobs: workerFileModifyingJobs,
|
|
661
|
+
outputMaxChars: workerOutputMaxChars,
|
|
662
|
+
outputMaxLines: workerOutputMaxLines,
|
|
663
|
+
outputMaxHeadLines: workerOutputMaxHeadLines,
|
|
664
|
+
qualityMaxAutoRevisions: workerQualityMaxAutoRevisions,
|
|
665
|
+
qualityValidationStepTimeoutMs: workerQualityValidationStepTimeoutMs,
|
|
666
|
+
qualityCriticTimeoutMs: workerQualityCriticTimeoutMs,
|
|
667
|
+
qualitySoftPassOnExhausted: workerQualitySoftPassOnExhausted,
|
|
668
|
+
qualityCriticMinScore: workerQualityCriticMinScore,
|
|
669
|
+
qualityCriticMaxDiffChars: workerQualityCriticMaxDiffChars,
|
|
670
|
+
qualityCriticMaxValidationOutputChars: workerQualityCriticMaxValidationOutputChars,
|
|
671
|
+
executorResultPrefix: workerExecutorResultPrefix,
|
|
672
|
+
dockerNetworkMode: asString(process.env.WORKERPALS_DOCKER_NETWORK_MODE ?? workerNode.docker_network_mode, "bridge"),
|
|
673
|
+
baseRef: firstNonEmpty(process.env.WORKERPALS_BASE_REF, asString(workerNode.base_ref, "origin/main_agents"), "origin/main_agents"),
|
|
674
|
+
labels: firstNonEmpty(process.env.WORKERPALS_LABELS) ? firstNonEmpty(process.env.WORKERPALS_LABELS).split(",").map((value) => value.trim()).filter(Boolean) : asStringArray(workerNode.labels),
|
|
675
|
+
failureCooldownMs: Math.max(0, asInt(parseIntEnv("WORKERPALS_FAILURE_COOLDOWN_MS") ?? parseIntEnv("WORKERPALS_DOCKER_FAILURE_COOLDOWN_MS") ?? workerNode.failure_cooldown_ms, 20000)),
|
|
676
|
+
llm: workerLlm
|
|
677
|
+
},
|
|
678
|
+
sourceControlManager: {
|
|
679
|
+
repoPath: scmRepoPath,
|
|
680
|
+
remote: scmRemote,
|
|
681
|
+
mainBranch: scmMainBranch,
|
|
682
|
+
baseBranch: scmBaseBranch,
|
|
683
|
+
branchPrefix: scmBranchPrefix,
|
|
684
|
+
pollIntervalSeconds: scmPollIntervalSeconds,
|
|
685
|
+
checks: scmChecks,
|
|
686
|
+
stateDir: scmStateDir,
|
|
687
|
+
port: scmPort,
|
|
688
|
+
deleteAfterMerge: scmDeleteAfterMerge,
|
|
689
|
+
maxAttempts: scmMaxAttempts,
|
|
690
|
+
mergeStrategy: scmMergeStrategy,
|
|
691
|
+
pushMainAfterMerge: scmPushMainAfterMerge,
|
|
692
|
+
openPrAfterPush: scmOpenPrAfterPush,
|
|
693
|
+
prBaseBranch: scmPrBaseBranch,
|
|
694
|
+
prTitle: scmPrTitle || null,
|
|
695
|
+
prBody: scmPrBody || null,
|
|
696
|
+
prDraft: scmPrDraft,
|
|
697
|
+
statusHeartbeatMs: scmStatusHeartbeatMs,
|
|
698
|
+
skipCleanCheck: scmSkipCleanCheck,
|
|
699
|
+
autoCreateMainBranch: scmAutoCreateMainBranch,
|
|
700
|
+
reviewAgent: {
|
|
701
|
+
enabled: scmReviewAgentEnabled,
|
|
702
|
+
pollIntervalMs: scmReviewAgentPollIntervalMs,
|
|
703
|
+
reviewerMdPath: scmReviewAgentReviewerMdPath,
|
|
704
|
+
passThreshold: scmReviewAgentPassThreshold,
|
|
705
|
+
maxPrCommentsBeforeGiveUp: scmReviewAgentMaxPrCommentsBeforeGiveUp,
|
|
706
|
+
mergeMethod: scmReviewAgentMergeMethod,
|
|
707
|
+
codexBin: scmReviewAgentCodexBin,
|
|
708
|
+
codexAuthMode: scmReviewAgentCodexAuthMode,
|
|
709
|
+
codexHomeDir: scmReviewAgentCodexHomeDir,
|
|
710
|
+
codexTimeoutMs: scmReviewAgentCodexTimeoutMs
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
startup: {
|
|
714
|
+
workerImageRebuild: startupWorkerImageRebuild,
|
|
715
|
+
logConfigOnStart: startupLogConfigOnStart,
|
|
716
|
+
syncIntegrationWithMain: startupSyncIntegrationWithMain,
|
|
717
|
+
skipLlmPreflight: startupSkipLlmPreflight,
|
|
718
|
+
autoStartLmStudio: startupAutoStartLmStudio,
|
|
719
|
+
lmStudioReadyTimeoutMs: startupLmStudioReadyTimeoutMs,
|
|
720
|
+
lmStudioCli: startupLmStudioCli,
|
|
721
|
+
lmStudioPort: startupLmStudioPort,
|
|
722
|
+
lmStudioStartArgs: startupLmStudioStartArgs,
|
|
723
|
+
startupWarmup,
|
|
724
|
+
startupWarmupTimeoutMs,
|
|
725
|
+
startupWarmupPollMs,
|
|
726
|
+
allowExternalClean: startupAllowExternalClean,
|
|
727
|
+
portPreflight: startupPortPreflight,
|
|
728
|
+
portConflictPolicy: startupPortConflictPolicy
|
|
729
|
+
},
|
|
730
|
+
client: {
|
|
731
|
+
localAgentUrl: firstNonEmpty(process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, asString(clientNode.local_agent_url, `http://localhost:${localPort}`), `http://localhost:${localPort}`),
|
|
732
|
+
traceTailLines: Math.max(10, asInt(parseIntEnv("EXPO_PUBLIC_PUSHPALS_TRACE_TAIL_LINES") ?? clientNode.trace_tail_lines, 100))
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
cachedConfig = config;
|
|
736
|
+
cachedConfigKey = cacheKey;
|
|
737
|
+
return config;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ../../scripts/pushpals-cli.ts
|
|
741
|
+
var DEFAULT_MONITOR_PORT = 8081;
|
|
742
|
+
var MONITOR_SCAN_PORTS = 32;
|
|
743
|
+
var HTTP_TIMEOUT_MS = 2500;
|
|
744
|
+
var LOCALBUDDY_TIMEOUT_MS = 4000;
|
|
745
|
+
var SSE_RECONNECT_MS = 1500;
|
|
746
|
+
var stateVersion = 1;
|
|
747
|
+
function printUsage() {
|
|
748
|
+
console.log("PushPals CLI");
|
|
749
|
+
console.log("");
|
|
750
|
+
console.log("Usage:");
|
|
751
|
+
console.log(" pushpals [options]");
|
|
752
|
+
console.log("");
|
|
753
|
+
console.log("Options:");
|
|
754
|
+
console.log(" --server-url <url> Override PushPals server URL");
|
|
755
|
+
console.log(" --local-agent-url <url> Override LocalBuddy URL");
|
|
756
|
+
console.log(" --session-id <id> Override session ID");
|
|
757
|
+
console.log(" --hub-url <url> Override monitoring hub URL");
|
|
758
|
+
console.log(" --no-stream Disable live session event stream");
|
|
759
|
+
console.log(" -h, --help Show this help");
|
|
760
|
+
console.log("");
|
|
761
|
+
console.log("Chat commands:");
|
|
762
|
+
console.log(" /hub Print monitoring hub URL");
|
|
763
|
+
console.log(" /open Open monitoring hub in browser");
|
|
764
|
+
console.log(" /status Print active endpoints");
|
|
765
|
+
console.log(" /exit, /quit Quit CLI");
|
|
766
|
+
console.log("");
|
|
767
|
+
console.log("Notes:");
|
|
768
|
+
console.log(" - Must be run from inside a git repository.");
|
|
769
|
+
console.log(" - LocalBuddy must be running and attached to the same repo root.");
|
|
770
|
+
}
|
|
771
|
+
function parseArgs(argv) {
|
|
772
|
+
const options = { noStream: false };
|
|
773
|
+
for (let i = 0;i < argv.length; i++) {
|
|
774
|
+
const arg = argv[i];
|
|
775
|
+
if (arg === "-h" || arg === "--help") {
|
|
776
|
+
printUsage();
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
if (arg === "--no-stream") {
|
|
780
|
+
options.noStream = true;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (arg === "--server-url") {
|
|
784
|
+
options.serverUrl = argv[++i];
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (arg === "--local-agent-url") {
|
|
788
|
+
options.localAgentUrl = argv[++i];
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (arg === "--session-id") {
|
|
792
|
+
options.sessionId = argv[++i];
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (arg === "--hub-url") {
|
|
796
|
+
options.monitoringHubUrl = argv[++i];
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
console.error(`[pushpals] Unknown argument: ${arg}`);
|
|
800
|
+
printUsage();
|
|
801
|
+
process.exit(2);
|
|
802
|
+
}
|
|
803
|
+
return options;
|
|
804
|
+
}
|
|
805
|
+
function normalizeUrl(value, fallback = "") {
|
|
806
|
+
const text = String(value ?? "").trim();
|
|
807
|
+
const selected = text || fallback;
|
|
808
|
+
return selected.replace(/\/+$/, "");
|
|
809
|
+
}
|
|
810
|
+
function parsePositiveInt(value, fallback) {
|
|
811
|
+
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
|
|
812
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
813
|
+
return fallback;
|
|
814
|
+
return parsed;
|
|
815
|
+
}
|
|
816
|
+
function normalizePath(value) {
|
|
817
|
+
const normalized = resolve2(value).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
818
|
+
if (process.platform === "win32")
|
|
819
|
+
return normalized.toLowerCase();
|
|
820
|
+
return normalized;
|
|
821
|
+
}
|
|
822
|
+
async function runGit(args, cwd) {
|
|
823
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
824
|
+
cwd,
|
|
825
|
+
stdout: "pipe",
|
|
826
|
+
stderr: "pipe"
|
|
827
|
+
});
|
|
828
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
829
|
+
new Response(proc.stdout).text(),
|
|
830
|
+
new Response(proc.stderr).text(),
|
|
831
|
+
proc.exited
|
|
832
|
+
]);
|
|
833
|
+
return { ok: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
834
|
+
}
|
|
835
|
+
async function resolveCurrentGitRepoRoot(cwd) {
|
|
836
|
+
const inside = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
837
|
+
if (!inside.ok || inside.stdout !== "true")
|
|
838
|
+
return null;
|
|
839
|
+
const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
840
|
+
if (!root.ok || !root.stdout)
|
|
841
|
+
return null;
|
|
842
|
+
return resolve2(root.stdout);
|
|
843
|
+
}
|
|
844
|
+
async function fetchWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
845
|
+
const controller = new AbortController;
|
|
846
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
847
|
+
try {
|
|
848
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
849
|
+
} finally {
|
|
850
|
+
clearTimeout(timer);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function fetchJsonWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
854
|
+
try {
|
|
855
|
+
const response = await fetchWithTimeout(url, init, timeoutMs);
|
|
856
|
+
if (!response.ok)
|
|
857
|
+
return null;
|
|
858
|
+
return await response.json();
|
|
859
|
+
} catch {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function authHeaders(authToken) {
|
|
864
|
+
if (!authToken)
|
|
865
|
+
return {};
|
|
866
|
+
return { Authorization: `Bearer ${authToken}` };
|
|
867
|
+
}
|
|
868
|
+
async function probeLocalBuddy(localAgentUrl, authToken) {
|
|
869
|
+
return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, { headers: authHeaders(authToken) }, LOCALBUDDY_TIMEOUT_MS);
|
|
870
|
+
}
|
|
871
|
+
function readCliState(pathValue) {
|
|
872
|
+
if (!existsSync2(pathValue))
|
|
873
|
+
return {};
|
|
874
|
+
try {
|
|
875
|
+
const raw = readFileSync2(pathValue, "utf8");
|
|
876
|
+
const parsed = JSON.parse(raw);
|
|
877
|
+
if (!parsed || typeof parsed !== "object")
|
|
878
|
+
return {};
|
|
879
|
+
return {
|
|
880
|
+
monitoringHubUrl: typeof parsed.monitoringHubUrl === "string" ? parsed.monitoringHubUrl : undefined,
|
|
881
|
+
serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : undefined,
|
|
882
|
+
localAgentUrl: typeof parsed.localAgentUrl === "string" ? parsed.localAgentUrl : undefined,
|
|
883
|
+
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : undefined,
|
|
884
|
+
repoRoot: typeof parsed.repoRoot === "string" ? parsed.repoRoot : undefined,
|
|
885
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined
|
|
886
|
+
};
|
|
887
|
+
} catch {
|
|
888
|
+
return {};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function writeCliState(pathValue, state) {
|
|
892
|
+
const payload = {
|
|
893
|
+
version: stateVersion,
|
|
894
|
+
...state,
|
|
895
|
+
updatedAt: new Date().toISOString()
|
|
896
|
+
};
|
|
897
|
+
mkdirSync(dirname(pathValue), { recursive: true });
|
|
898
|
+
writeFileSync(pathValue, `${JSON.stringify(payload, null, 2)}
|
|
899
|
+
`, "utf8");
|
|
900
|
+
}
|
|
901
|
+
async function looksLikeMonitoringHub(url) {
|
|
902
|
+
try {
|
|
903
|
+
const response = await fetchWithTimeout(url, {}, 700);
|
|
904
|
+
if (!response.ok)
|
|
905
|
+
return false;
|
|
906
|
+
const contentType = String(response.headers.get("content-type") ?? "").toLowerCase();
|
|
907
|
+
if (!contentType.includes("text/html"))
|
|
908
|
+
return false;
|
|
909
|
+
const text = await response.text();
|
|
910
|
+
const sample = text.slice(0, 8192).toLowerCase();
|
|
911
|
+
return sample.includes("pushpals") || sample.includes("mission control") || sample.includes("jobs & traces");
|
|
912
|
+
} catch {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function resolveMonitoringHubUrl(preferredUrl, fallbackPort) {
|
|
917
|
+
const explicit = normalizeUrl(preferredUrl);
|
|
918
|
+
if (explicit)
|
|
919
|
+
return explicit;
|
|
920
|
+
const basePort = fallbackPort;
|
|
921
|
+
for (let port = basePort;port < basePort + MONITOR_SCAN_PORTS; port++) {
|
|
922
|
+
const candidate = `http://localhost:${port}`;
|
|
923
|
+
if (await looksLikeMonitoringHub(candidate))
|
|
924
|
+
return candidate;
|
|
925
|
+
}
|
|
926
|
+
return `http://localhost:${basePort}`;
|
|
927
|
+
}
|
|
928
|
+
async function sendMessageToLocalBuddy(localAgentUrl, text) {
|
|
929
|
+
let response;
|
|
930
|
+
try {
|
|
931
|
+
response = await fetchWithTimeout(`${localAgentUrl}/message`, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
headers: { "Content-Type": "application/json" },
|
|
934
|
+
body: JSON.stringify({ text })
|
|
935
|
+
}, 30000);
|
|
936
|
+
} catch (err) {
|
|
937
|
+
console.error(`[pushpals] Failed to reach LocalBuddy: ${String(err)}`);
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
if (!response.ok) {
|
|
941
|
+
const detail = await response.text().catch(() => "");
|
|
942
|
+
console.error(`[pushpals] LocalBuddy rejected message: HTTP ${response.status} ${detail}`);
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
const reader = response.body?.getReader();
|
|
946
|
+
if (!reader) {
|
|
947
|
+
console.error("[pushpals] LocalBuddy response stream missing.");
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
let buffer = "";
|
|
951
|
+
const decoder = new TextDecoder;
|
|
952
|
+
let complete = false;
|
|
953
|
+
let ok = true;
|
|
954
|
+
while (true) {
|
|
955
|
+
const { done, value } = await reader.read();
|
|
956
|
+
if (done)
|
|
957
|
+
break;
|
|
958
|
+
buffer += decoder.decode(value, { stream: true });
|
|
959
|
+
const chunks = buffer.split(`
|
|
960
|
+
|
|
961
|
+
`);
|
|
962
|
+
buffer = chunks.pop() ?? "";
|
|
963
|
+
for (const chunk of chunks) {
|
|
964
|
+
const dataLine = chunk.split(/\r?\n/).map((line) => line.trim()).find((line) => line.startsWith("data: "));
|
|
965
|
+
if (!dataLine)
|
|
966
|
+
continue;
|
|
967
|
+
try {
|
|
968
|
+
const payload = JSON.parse(dataLine.slice(6));
|
|
969
|
+
const type = String(payload.type ?? "").trim().toLowerCase();
|
|
970
|
+
const message = String(payload.message ?? "").trim();
|
|
971
|
+
if (type === "status" && message) {
|
|
972
|
+
console.log(`[localbuddy] ${message}`);
|
|
973
|
+
} else if (type === "error") {
|
|
974
|
+
ok = false;
|
|
975
|
+
console.log(`[localbuddy] ERROR: ${message || "Unknown failure"}`);
|
|
976
|
+
} else if (type === "complete") {
|
|
977
|
+
complete = true;
|
|
978
|
+
const requestId = payload.data && typeof payload.data.requestId === "string" ? payload.data.requestId : "";
|
|
979
|
+
if (requestId) {
|
|
980
|
+
console.log(`[localbuddy] requestId=${requestId}`);
|
|
981
|
+
} else if (message) {
|
|
982
|
+
console.log(`[localbuddy] ${message}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} catch {}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return ok && complete;
|
|
989
|
+
}
|
|
990
|
+
function formatSessionEventLine(event) {
|
|
991
|
+
const type = String(event.type ?? "").toLowerCase();
|
|
992
|
+
const from = String(event.from ?? "");
|
|
993
|
+
const payload = event.payload ?? {};
|
|
994
|
+
if (type === "message")
|
|
995
|
+
return null;
|
|
996
|
+
if (type === "assistant_message") {
|
|
997
|
+
const text = String(payload.text ?? "").trim();
|
|
998
|
+
if (!text)
|
|
999
|
+
return null;
|
|
1000
|
+
return `assistant> ${text}`;
|
|
1001
|
+
}
|
|
1002
|
+
if (type === "task_progress") {
|
|
1003
|
+
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
1004
|
+
const message = String(payload.message ?? "").trim();
|
|
1005
|
+
return message ? `[task ${taskId}] ${message}` : null;
|
|
1006
|
+
}
|
|
1007
|
+
if (type === "task_failed") {
|
|
1008
|
+
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
1009
|
+
const message = String(payload.message ?? "").trim();
|
|
1010
|
+
return `[task ${taskId}] failed: ${message || "unknown"}`;
|
|
1011
|
+
}
|
|
1012
|
+
if (type === "task_completed") {
|
|
1013
|
+
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
1014
|
+
const summary = String(payload.summary ?? "").trim();
|
|
1015
|
+
return `[task ${taskId}] completed${summary ? `: ${summary}` : ""}`;
|
|
1016
|
+
}
|
|
1017
|
+
if (type === "job_failed") {
|
|
1018
|
+
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
1019
|
+
const message = String(payload.message ?? "").trim();
|
|
1020
|
+
return `[job ${jobId}] failed: ${message || "unknown"}`;
|
|
1021
|
+
}
|
|
1022
|
+
if (type === "error") {
|
|
1023
|
+
const message = String(payload.message ?? "").trim();
|
|
1024
|
+
return `[event error] ${message || "unknown"}`;
|
|
1025
|
+
}
|
|
1026
|
+
if (type === "status") {
|
|
1027
|
+
const state = String(payload.state ?? "").trim();
|
|
1028
|
+
const detail = String(payload.detail ?? "").trim();
|
|
1029
|
+
const source = from || String(payload.agentId ?? "status");
|
|
1030
|
+
return detail ? `[status ${source}] ${state || "unknown"} - ${detail}` : `[status ${source}] ${state || "unknown"}`;
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
async function runSessionStream(serverUrl, sessionId, authToken, print, signal) {
|
|
1035
|
+
let cursor = 0;
|
|
1036
|
+
while (!signal.aborted) {
|
|
1037
|
+
const headers = authHeaders(authToken);
|
|
1038
|
+
try {
|
|
1039
|
+
const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/events${cursor > 0 ? `?after=${cursor}` : ""}`, { headers }, 15000);
|
|
1040
|
+
if (!response.ok || !response.body) {
|
|
1041
|
+
print(`[pushpals] Session stream unavailable: HTTP ${response.status}`);
|
|
1042
|
+
await Bun.sleep(SSE_RECONNECT_MS);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
const reader = response.body.getReader();
|
|
1046
|
+
const decoder = new TextDecoder;
|
|
1047
|
+
let buffer = "";
|
|
1048
|
+
while (!signal.aborted) {
|
|
1049
|
+
const { done, value } = await reader.read();
|
|
1050
|
+
if (done)
|
|
1051
|
+
break;
|
|
1052
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1053
|
+
const blocks = buffer.split(`
|
|
1054
|
+
|
|
1055
|
+
`);
|
|
1056
|
+
buffer = blocks.pop() ?? "";
|
|
1057
|
+
for (const block of blocks) {
|
|
1058
|
+
if (!block.trim())
|
|
1059
|
+
continue;
|
|
1060
|
+
let blockCursor = 0;
|
|
1061
|
+
let rawData = "";
|
|
1062
|
+
for (const line2 of block.split(/\r?\n/)) {
|
|
1063
|
+
if (line2.startsWith("id:")) {
|
|
1064
|
+
const idText = line2.slice(3).trim();
|
|
1065
|
+
const parsed2 = Number.parseInt(idText, 10);
|
|
1066
|
+
if (Number.isFinite(parsed2) && parsed2 > 0) {
|
|
1067
|
+
blockCursor = parsed2;
|
|
1068
|
+
}
|
|
1069
|
+
} else if (line2.startsWith("data:")) {
|
|
1070
|
+
rawData += `${line2.slice(5).trim()}
|
|
1071
|
+
`;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (!rawData.trim())
|
|
1075
|
+
continue;
|
|
1076
|
+
let parsed = null;
|
|
1077
|
+
try {
|
|
1078
|
+
parsed = JSON.parse(rawData.trim());
|
|
1079
|
+
} catch {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const serverCursor = typeof parsed.cursor === "number" && Number.isFinite(parsed.cursor) ? parsed.cursor : 0;
|
|
1083
|
+
cursor = Math.max(cursor, blockCursor, serverCursor);
|
|
1084
|
+
if (!parsed.envelope)
|
|
1085
|
+
continue;
|
|
1086
|
+
const line = formatSessionEventLine(parsed.envelope);
|
|
1087
|
+
if (line)
|
|
1088
|
+
print(line);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
} catch {}
|
|
1092
|
+
if (!signal.aborted) {
|
|
1093
|
+
await Bun.sleep(SSE_RECONNECT_MS);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async function openMonitoringHub(url) {
|
|
1098
|
+
let cmd = null;
|
|
1099
|
+
if (process.platform === "win32") {
|
|
1100
|
+
const escaped = url.replace(/'/g, "''");
|
|
1101
|
+
cmd = ["powershell", "-NoProfile", "-Command", `Start-Process '${escaped}'`];
|
|
1102
|
+
} else if (process.platform === "darwin") {
|
|
1103
|
+
cmd = ["open", url];
|
|
1104
|
+
} else {
|
|
1105
|
+
cmd = ["xdg-open", url];
|
|
1106
|
+
}
|
|
1107
|
+
const proc = Bun.spawn(cmd, {
|
|
1108
|
+
stdin: "ignore",
|
|
1109
|
+
stdout: "ignore",
|
|
1110
|
+
stderr: "ignore"
|
|
1111
|
+
});
|
|
1112
|
+
const code = await proc.exited;
|
|
1113
|
+
return code === 0;
|
|
1114
|
+
}
|
|
1115
|
+
async function main() {
|
|
1116
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
1117
|
+
if (!parsed)
|
|
1118
|
+
return;
|
|
1119
|
+
const config = loadPushPalsConfig();
|
|
1120
|
+
const cwd = process.cwd();
|
|
1121
|
+
const repoRoot = await resolveCurrentGitRepoRoot(cwd);
|
|
1122
|
+
if (!repoRoot) {
|
|
1123
|
+
console.error("[pushpals] Refusing to start: current directory is not a git repository.");
|
|
1124
|
+
console.error(`[pushpals] cwd=${cwd}`);
|
|
1125
|
+
console.error("[pushpals] Run from a repo directory, or initialize one with `git init`.");
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
const serverUrl = normalizeUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
|
|
1129
|
+
const localAgentUrl = normalizeUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
|
|
1130
|
+
const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
|
|
1131
|
+
const authToken = config.authToken;
|
|
1132
|
+
const health = await probeLocalBuddy(localAgentUrl, authToken);
|
|
1133
|
+
if (!health?.ok) {
|
|
1134
|
+
console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
|
|
1135
|
+
console.error("[pushpals] Start the stack first, then rerun `pushpals`.");
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
const localBuddyRepo = health.repo ? resolve2(health.repo) : "";
|
|
1139
|
+
if (!localBuddyRepo) {
|
|
1140
|
+
console.error("[pushpals] LocalBuddy health response did not include repo path.");
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
|
|
1144
|
+
console.error("[pushpals] Repo mismatch detected.");
|
|
1145
|
+
console.error(`[pushpals] currentRepo=${repoRoot}`);
|
|
1146
|
+
console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
|
|
1147
|
+
console.error("[pushpals] LocalBuddy must run against the same repo. Start PushPals from this repo and retry.");
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
const localBuddySessionId = health.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
|
|
1151
|
+
if (sessionId && sessionId !== localBuddySessionId) {
|
|
1152
|
+
console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
|
|
1153
|
+
}
|
|
1154
|
+
const statePath = resolve2(repoRoot, ".git", "pushpals-cli-state.json");
|
|
1155
|
+
const saved = readCliState(statePath);
|
|
1156
|
+
const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
|
|
1157
|
+
const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
|
|
1158
|
+
const monitoringHubUrl = await resolveMonitoringHubUrl(preferredHubUrl, monitorPort);
|
|
1159
|
+
writeCliState(statePath, {
|
|
1160
|
+
monitoringHubUrl,
|
|
1161
|
+
serverUrl,
|
|
1162
|
+
localAgentUrl,
|
|
1163
|
+
sessionId: localBuddySessionId,
|
|
1164
|
+
repoRoot
|
|
1165
|
+
});
|
|
1166
|
+
console.log("[pushpals] Connected.");
|
|
1167
|
+
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1168
|
+
console.log(`serverUrl=${serverUrl}`);
|
|
1169
|
+
console.log(`localAgentUrl=${localAgentUrl}`);
|
|
1170
|
+
console.log(`sessionId=${localBuddySessionId}`);
|
|
1171
|
+
console.log(`repoRoot=${repoRoot}`);
|
|
1172
|
+
console.log(`cliStateFile=${statePath}`);
|
|
1173
|
+
console.log("[pushpals] Type a message and press Enter. Use /exit to quit.");
|
|
1174
|
+
const streamAbort = new AbortController;
|
|
1175
|
+
let rl = null;
|
|
1176
|
+
const printIncoming = (line) => {
|
|
1177
|
+
if (!line)
|
|
1178
|
+
return;
|
|
1179
|
+
if (rl) {
|
|
1180
|
+
process.stdout.write(`
|
|
1181
|
+
${line}
|
|
1182
|
+
`);
|
|
1183
|
+
rl.prompt();
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
console.log(line);
|
|
1187
|
+
};
|
|
1188
|
+
const streamTask = parsed.noStream ? Promise.resolve() : runSessionStream(serverUrl, localBuddySessionId, authToken, printIncoming, streamAbort.signal);
|
|
1189
|
+
let shuttingDown = false;
|
|
1190
|
+
const requestStop = () => {
|
|
1191
|
+
if (shuttingDown)
|
|
1192
|
+
return;
|
|
1193
|
+
shuttingDown = true;
|
|
1194
|
+
streamAbort.abort();
|
|
1195
|
+
if (rl)
|
|
1196
|
+
rl.close();
|
|
1197
|
+
};
|
|
1198
|
+
process.once("SIGINT", requestStop);
|
|
1199
|
+
process.once("SIGTERM", requestStop);
|
|
1200
|
+
rl = createInterface({
|
|
1201
|
+
input: process.stdin,
|
|
1202
|
+
output: process.stdout,
|
|
1203
|
+
terminal: true
|
|
1204
|
+
});
|
|
1205
|
+
rl.setPrompt("you> ");
|
|
1206
|
+
rl.prompt();
|
|
1207
|
+
for await (const rawLine of rl) {
|
|
1208
|
+
const text = String(rawLine ?? "").trim();
|
|
1209
|
+
if (!text) {
|
|
1210
|
+
rl.prompt();
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (text === "/exit" || text === "/quit") {
|
|
1214
|
+
requestStop();
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
if (text === "/hub") {
|
|
1218
|
+
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1219
|
+
rl.prompt();
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (text === "/status") {
|
|
1223
|
+
console.log(`serverUrl=${serverUrl}`);
|
|
1224
|
+
console.log(`localAgentUrl=${localAgentUrl}`);
|
|
1225
|
+
console.log(`sessionId=${sessionId}`);
|
|
1226
|
+
console.log(`repoRoot=${repoRoot}`);
|
|
1227
|
+
console.log(`monitoringHubUrl=${monitoringHubUrl}`);
|
|
1228
|
+
rl.prompt();
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (text === "/open") {
|
|
1232
|
+
const opened = await openMonitoringHub(monitoringHubUrl);
|
|
1233
|
+
console.log(opened ? `[pushpals] Opened ${monitoringHubUrl}` : `[pushpals] Failed to open browser. Use this link: ${monitoringHubUrl}`);
|
|
1234
|
+
rl.prompt();
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
const ok = await sendMessageToLocalBuddy(localAgentUrl, text);
|
|
1238
|
+
if (!ok) {
|
|
1239
|
+
console.log("[pushpals] Message failed.");
|
|
1240
|
+
}
|
|
1241
|
+
rl.prompt();
|
|
1242
|
+
}
|
|
1243
|
+
requestStop();
|
|
1244
|
+
await Promise.race([streamTask, Bun.sleep(2000)]);
|
|
1245
|
+
}
|
|
1246
|
+
main().catch((err) => {
|
|
1247
|
+
console.error(`[pushpals] Fatal: ${String(err)}`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
});
|