@pushpalsdev/cli 1.0.18 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/pushpals-cli.js +277 -12
  2. package/package.json +1 -1
  3. package/runtime/sandbox/apps/workerpals/.python-version +1 -0
  4. package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
  5. package/runtime/sandbox/apps/workerpals/package.json +25 -0
  6. package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
  7. package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +111 -0
  8. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
  9. package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
  10. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
  11. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
  12. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
  13. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
  14. package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
  15. package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
  16. package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
  17. package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
  18. package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
  19. package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
  20. package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
  21. package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
  22. package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
  23. package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
  24. package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
  25. package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
  26. package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
  27. package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
  28. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
  29. package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
  30. package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
  31. package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
  32. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
  33. package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
  34. package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
  35. package/runtime/sandbox/bun.lock +2591 -0
  36. package/runtime/sandbox/configs/backend.toml +79 -0
  37. package/runtime/sandbox/configs/default.toml +260 -0
  38. package/runtime/sandbox/configs/dev.toml +2 -0
  39. package/runtime/sandbox/configs/local.example.toml +129 -0
  40. package/runtime/sandbox/package.json +65 -0
  41. package/runtime/sandbox/packages/protocol/README.md +168 -0
  42. package/runtime/sandbox/packages/protocol/package.json +37 -0
  43. package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
  44. package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
  45. package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
  46. package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
  47. package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
  48. package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
  49. package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
  50. package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
  51. package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
  52. package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
  53. package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
  54. package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
  55. package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
  56. package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
  57. package/runtime/sandbox/packages/shared/package.json +19 -0
  58. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
  59. package/runtime/sandbox/packages/shared/src/client_preflight.ts +297 -0
  60. package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
  61. package/runtime/sandbox/packages/shared/src/config.ts +2201 -0
  62. package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
  63. package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
  64. package/runtime/sandbox/packages/shared/src/index.ts +100 -0
  65. package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
  66. package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +329 -0
  67. package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
  68. package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
  69. package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
  70. package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
  71. package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
  72. package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
  73. package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
  74. package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
  75. package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
  76. package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
  77. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
  78. package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
  79. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
  80. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
  81. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
  82. package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
  83. package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
  84. package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
  85. package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
  86. package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
  87. package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
  88. package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
  89. package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
  90. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
  91. package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
  92. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
  93. package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
  94. package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
  95. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
  96. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
  97. package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
  98. package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
  99. package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
  100. package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
  101. package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
  102. package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
  103. package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
  104. package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
  105. package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
  106. package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
@@ -0,0 +1,2201 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join, resolve, isAbsolute } from "path";
3
+ import { normalizeAutonomyComponentArea } from "./autonomy_policy.js";
4
+ import { normalizeLoopbackHost, normalizeLoopbackHttpUrl } from "./local_network.js";
5
+
6
+ type TomlValue = string | number | boolean | null | TomlObject | TomlValue[];
7
+ interface TomlObject {
8
+ [key: string]: TomlValue;
9
+ }
10
+
11
+ const PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "..");
12
+ const DEFAULT_CONFIG_DIR = "configs";
13
+ const LEGACY_CONFIG_DIR = "config";
14
+
15
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
16
+ const FALSY = new Set(["0", "false", "no", "off"]);
17
+ const DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
18
+ const DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 1;
19
+ const DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
20
+ const DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS = 192 * 1024;
21
+ const DEFAULT_WORKERPALS_OUTPUT_MAX_LINES = 600;
22
+ const DEFAULT_WORKERPALS_OUTPUT_MAX_HEAD_LINES = 120;
23
+ const DEFAULT_WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS = 180_000;
24
+ const DEFAULT_WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS = 45_000;
25
+ const DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS = 16_000;
26
+ const DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS = 8_000;
27
+ const DEFAULT_WORKERPALS_EXECUTOR_RESULT_PREFIX = "__PUSHPALS_OH_RESULT__ ";
28
+ const DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS = 12;
29
+ const DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS = 2400;
30
+ const DEFAULT_REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS = 420;
31
+ const DEFAULT_REMOTEBUDDY_MEMORY_RETENTION_DAYS = 30;
32
+ const REDACTED_LOG_VALUE = "[REDACTED]";
33
+ const SENSITIVE_CONFIG_KEY_PATTERN =
34
+ /(token|secret|password|api[_-]?key|private[_-]?key|access[_-]?key)/i;
35
+
36
+ export interface PushPalsLlmConfig {
37
+ backend: string;
38
+ endpoint: string;
39
+ model: string;
40
+ apiKey: string;
41
+ sessionId: string;
42
+ reasoningEffort: string;
43
+ codexAuthMode: string;
44
+ codexBin: string;
45
+ codexTimeoutMs: number;
46
+ }
47
+
48
+ export interface PushPalsLmStudioConfig {
49
+ contextWindow: number;
50
+ minOutputTokens: number;
51
+ tokenSafetyMargin: number;
52
+ batchTailMessages: number;
53
+ batchChunkTokens: number;
54
+ batchMemoryChars: number;
55
+ }
56
+
57
+ export interface PushPalsCheckConfig {
58
+ name: string;
59
+ command: string;
60
+ timeoutMs: number;
61
+ }
62
+
63
+ export interface PushPalsConfig {
64
+ projectRoot: string;
65
+ configDir: string;
66
+ profile: string;
67
+ sessionId: string;
68
+ authToken: string | null;
69
+ gitToken: string | null;
70
+ llm: {
71
+ lmstudio: PushPalsLmStudioConfig;
72
+ };
73
+ paths: {
74
+ dataDir: string;
75
+ sharedDbPath: string;
76
+ remotebuddyDbPath: string;
77
+ };
78
+ server: {
79
+ url: string;
80
+ host: string;
81
+ port: number;
82
+ debugHttp: boolean;
83
+ staleClaimTtlMs: number;
84
+ staleClaimSweepIntervalMs: number;
85
+ };
86
+ localbuddy: {
87
+ enabled: boolean;
88
+ port: number;
89
+ statusHeartbeatMs: number;
90
+ llm: PushPalsLlmConfig;
91
+ };
92
+ remotebuddy: {
93
+ pollMs: number;
94
+ statusHeartbeatMs: number;
95
+ workerpalOnlineTtlMs: number;
96
+ waitForWorkerpalMs: number;
97
+ autoSpawnWorkerpals: boolean;
98
+ maxWorkerpals: number;
99
+ workerpalStartupTimeoutMs: number;
100
+ workerpalDocker: boolean;
101
+ workerpalRequireDocker: boolean;
102
+ workerpalImage: string | null;
103
+ workerpalPollMs: number | null;
104
+ workerpalHeartbeatMs: number | null;
105
+ workerpalLabels: string[];
106
+ executionBudgetInteractiveMs: number;
107
+ executionBudgetNormalMs: number;
108
+ executionBudgetBackgroundMs: number;
109
+ finalizationBudgetMs: number;
110
+ crashRestartEnabled: boolean;
111
+ crashRestartMaxRestarts: number;
112
+ crashRestartBackoffMs: number;
113
+ memory: {
114
+ enabled: boolean;
115
+ includeCrossSession: boolean;
116
+ maxRecallItems: number;
117
+ maxRecallChars: number;
118
+ maxSummaryChars: number;
119
+ retentionDays: number;
120
+ };
121
+ autonomy: {
122
+ enabled: boolean;
123
+ killSwitchEnabled: boolean;
124
+ tickIntervalMs: number;
125
+ heartbeatLogMs: number;
126
+ visionContextMaxChars: number;
127
+ ideationBudgetMs: number;
128
+ llmTimeoutMs: number;
129
+ allowDirtyWorktree: boolean;
130
+ ideationMaxCandidates: number;
131
+ topK: number;
132
+ exploreRate: number;
133
+ minConfidence: number;
134
+ maxConcurrentObjectives: number;
135
+ maxDispatchPerHour: number;
136
+ maxDispatchPerHourByType: Record<string, number>;
137
+ maxDispatchPerHourByComponent: Record<string, number>;
138
+ maxTokenUsagePerHour: number;
139
+ maxRuntimeMsPerHour: number;
140
+ cooldownFailStreakThreshold: number;
141
+ cooldownMs: number;
142
+ staleObjectiveTtlMs: number;
143
+ staleObjectiveSweepIntervalMs: number;
144
+ autoFreezeFailStreakThreshold: number;
145
+ autoFreezeDurationMs: number;
146
+ evaluatorWindowHours: number;
147
+ evaluatorMinSamples: number;
148
+ evaluatorMinSuccessRate: number;
149
+ evaluatorMaxRegretRate: number;
150
+ evaluatorRunIntervalMs: number;
151
+ alertQueuePendingThreshold: number;
152
+ alertJobFailureRateThreshold: number;
153
+ alertAutonomyFailureRateThreshold: number;
154
+ allowReadAnywhere: boolean;
155
+ prFeedbackCommentRows: number;
156
+ prFeedbackCommentChars: number;
157
+ prFeedbackSummaryChars: number;
158
+ questionTtlMs: number;
159
+ policyVersion: string;
160
+ impactModelVersion: string;
161
+ replay: {
162
+ storePromptPayloads: boolean;
163
+ maxRunsWithPayloads: number;
164
+ maxPayloadBytes: number;
165
+ };
166
+ };
167
+ llm: PushPalsLlmConfig;
168
+ };
169
+ workerpals: {
170
+ pollMs: number;
171
+ heartbeatMs: number;
172
+ executor: string;
173
+ openhandsPython: string;
174
+ openhandsTimeoutMs: number;
175
+ miniswePython: string;
176
+ minisweTimeoutMs: number;
177
+ openaiCodexPython: string;
178
+ openaiCodexTimeoutMs: number;
179
+ openhandsStuckGuardEnabled: boolean;
180
+ openhandsStuckGuardExploreLimit: number;
181
+ openhandsStuckGuardMinElapsedMs: number;
182
+ openhandsStuckGuardBroadScanLimit: number;
183
+ openhandsStuckGuardNoProgressMaxMs: number;
184
+ openhandsAutoSteerEnabled: boolean;
185
+ openhandsAutoSteerInitialDelaySec: number;
186
+ openhandsAutoSteerIntervalSec: number;
187
+ openhandsAutoSteerMaxNudges: number;
188
+ requirePush: boolean;
189
+ pushAgentBranch: boolean;
190
+ requireDocker: boolean;
191
+ skipDockerSelfCheck: boolean;
192
+ dockerImage: string;
193
+ dockerTimeoutMs: number;
194
+ dockerIdleTimeoutMs: number;
195
+ dockerAgentStartupTimeoutMs: number;
196
+ dockerWarmMaxAttempts: number;
197
+ dockerWarmRetryBackoffMs: number;
198
+ dockerJobMaxAttempts: number;
199
+ dockerJobRetryBackoffMs: number;
200
+ dockerNetworkMode: string;
201
+ dockerWarmMemoryMb: number;
202
+ dockerWarmCpus: number;
203
+ fileModifyingJobs: string[];
204
+ outputMaxChars: number;
205
+ outputMaxLines: number;
206
+ outputMaxHeadLines: number;
207
+ qualityMaxAutoRevisions: number;
208
+ qualityValidationStepTimeoutMs: number;
209
+ qualityCriticTimeoutMs: number;
210
+ qualitySoftPassOnExhausted: boolean;
211
+ qualityCriticMinScore: number;
212
+ qualityCriticMaxDiffChars: number;
213
+ qualityCriticMaxValidationOutputChars: number;
214
+ executorResultPrefix: string;
215
+ baseRef: string;
216
+ labels: string[];
217
+ failureCooldownMs: number;
218
+ llm: PushPalsLlmConfig;
219
+ };
220
+ sourceControlManager: {
221
+ repoPath: string;
222
+ remote: string;
223
+ mainBranch: string;
224
+ baseBranch: string;
225
+ branchPrefix: string;
226
+ pollIntervalSeconds: number;
227
+ checks: PushPalsCheckConfig[];
228
+ stateDir: string;
229
+ port: number;
230
+ deleteAfterMerge: boolean;
231
+ maxAttempts: number;
232
+ mergeStrategy: "cherry-pick" | "no-ff" | "ff-only";
233
+ pushMainAfterMerge: boolean;
234
+ openPrAfterPush: boolean;
235
+ prBaseBranch: string;
236
+ prTitle: string | null;
237
+ prBody: string | null;
238
+ prDraft: boolean;
239
+ statusHeartbeatMs: number;
240
+ skipCleanCheck: boolean;
241
+ autoCreateMainBranch: boolean;
242
+ reviewAgent: {
243
+ enabled: boolean;
244
+ pollIntervalMs: number;
245
+ reviewerMdPath: string;
246
+ passThreshold: number;
247
+ maxPrCommentsBeforeGiveUp: number;
248
+ mergeMethod: "squash" | "merge" | "rebase";
249
+ codexBin: string;
250
+ codexAuthMode: string;
251
+ codexHomeDir: string;
252
+ codexTimeoutMs: number;
253
+ };
254
+ };
255
+ startup: {
256
+ workerImageRebuild: "auto" | "always" | "never";
257
+ logConfigOnStart: boolean;
258
+ syncIntegrationWithMain: boolean;
259
+ skipLlmPreflight: boolean;
260
+ autoStartLmStudio: boolean;
261
+ lmStudioReadyTimeoutMs: number;
262
+ lmStudioCli: string;
263
+ lmStudioPort: number;
264
+ lmStudioStartArgs: string;
265
+ startupWarmup: boolean;
266
+ startupWarmupTimeoutMs: number;
267
+ startupWarmupPollMs: number;
268
+ allowExternalClean: boolean;
269
+ portPreflight: boolean;
270
+ portConflictPolicy: "fail" | "terminate_pushpals";
271
+ };
272
+ client: {
273
+ localAgentUrl: string;
274
+ traceTailLines: number;
275
+ };
276
+ }
277
+
278
+ interface LoadOptions {
279
+ projectRoot?: string;
280
+ configDir?: string;
281
+ reload?: boolean;
282
+ }
283
+
284
+ let cachedConfig: PushPalsConfig | null = null;
285
+ let cachedConfigKey = "";
286
+
287
+ export function invalidatePushPalsConfigCache(): void {
288
+ cachedConfig = null;
289
+ cachedConfigKey = "";
290
+ }
291
+
292
+ function firstNonEmpty(...values: Array<string | null | undefined>): string {
293
+ for (const value of values) {
294
+ const trimmed = (value ?? "").trim();
295
+ if (trimmed) return trimmed;
296
+ }
297
+ return "";
298
+ }
299
+
300
+ function parseBoolEnv(name: string): boolean | null {
301
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
302
+ if (!raw) return null;
303
+ if (TRUTHY.has(raw)) return true;
304
+ if (FALSY.has(raw)) return false;
305
+ return null;
306
+ }
307
+
308
+ function parseIntEnv(name: string): number | null {
309
+ const raw = (process.env[name] ?? "").trim();
310
+ if (!raw) return null;
311
+ const parsed = Number.parseInt(raw, 10);
312
+ return Number.isFinite(parsed) ? parsed : null;
313
+ }
314
+
315
+ function parseTomlFile(path: string): TomlObject {
316
+ if (!existsSync(path)) return {};
317
+ const raw = readFileSync(path, "utf-8");
318
+ const parsed = Bun.TOML.parse(raw) as unknown;
319
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
320
+ return parsed as TomlObject;
321
+ }
322
+
323
+ function isObject(value: unknown): value is TomlObject {
324
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
325
+ }
326
+
327
+ function mergeDeep(base: TomlObject, override: TomlObject): TomlObject {
328
+ const out: TomlObject = { ...base };
329
+ for (const [key, value] of Object.entries(override)) {
330
+ const existing = out[key];
331
+ if (isObject(existing) && isObject(value)) {
332
+ out[key] = mergeDeep(existing, value);
333
+ } else {
334
+ out[key] = value;
335
+ }
336
+ }
337
+ return out;
338
+ }
339
+
340
+ function getObject(parent: TomlObject, key: string): TomlObject {
341
+ const value = parent[key];
342
+ if (isObject(value)) return value;
343
+ return {};
344
+ }
345
+
346
+ function asString(value: unknown, fallback: string): string {
347
+ if (typeof value === "string" && value.trim()) return value.trim();
348
+ return fallback;
349
+ }
350
+
351
+ function asBoolean(value: unknown, fallback: boolean): boolean {
352
+ if (typeof value === "boolean") return value;
353
+ if (typeof value === "string") {
354
+ const lowered = value.trim().toLowerCase();
355
+ if (TRUTHY.has(lowered)) return true;
356
+ if (FALSY.has(lowered)) return false;
357
+ }
358
+ return fallback;
359
+ }
360
+
361
+ function asInt(value: unknown, fallback: number): number {
362
+ if (typeof value === "number" && Number.isFinite(value)) return Math.floor(value);
363
+ if (typeof value === "string") {
364
+ const parsed = Number.parseInt(value.trim(), 10);
365
+ if (Number.isFinite(parsed)) return parsed;
366
+ }
367
+ return fallback;
368
+ }
369
+
370
+ function asIntOrNull(value: unknown): number | null {
371
+ if (typeof value === "number" && Number.isFinite(value)) return Math.floor(value);
372
+ if (typeof value === "string" && value.trim()) {
373
+ const parsed = Number.parseInt(value.trim(), 10);
374
+ if (Number.isFinite(parsed)) return parsed;
375
+ }
376
+ return null;
377
+ }
378
+
379
+ function asStringArray(value: unknown): string[] {
380
+ if (!Array.isArray(value)) return [];
381
+ return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
382
+ }
383
+
384
+ function asCheckArray(value: unknown): PushPalsCheckConfig[] {
385
+ if (!Array.isArray(value)) return [];
386
+ const checks: PushPalsCheckConfig[] = [];
387
+ for (const entry of value) {
388
+ if (!isObject(entry)) continue;
389
+ const name = asString(entry.name, "").trim();
390
+ const command = asString(entry.command, "").trim();
391
+ if (!name || !command) continue;
392
+ const timeoutMs = Math.max(1_000, asInt(entry.timeout_ms ?? entry.timeoutMs, 300_000));
393
+ checks.push({ name, command, timeoutMs });
394
+ }
395
+ return checks;
396
+ }
397
+
398
+ function asStringNumberRecord(value: unknown): Record<string, number> {
399
+ if (!isObject(value)) return {};
400
+ const out: Record<string, number> = {};
401
+ for (const [key, raw] of Object.entries(value)) {
402
+ const name = key.trim();
403
+ if (!name) continue;
404
+ const num =
405
+ typeof raw === "number"
406
+ ? raw
407
+ : typeof raw === "string"
408
+ ? Number.parseInt(raw.trim(), 10)
409
+ : Number.NaN;
410
+ if (!Number.isFinite(num)) continue;
411
+ out[name] = Math.max(0, Math.floor(num));
412
+ }
413
+ return out;
414
+ }
415
+
416
+ function resolvePathFromRoot(projectRoot: string, value: string): string {
417
+ if (!value) return projectRoot;
418
+ if (isAbsolute(value)) return resolve(value);
419
+ return resolve(projectRoot, value);
420
+ }
421
+
422
+ function resolveRuntimeConfigDir(projectRoot: string, configuredDir?: string): string {
423
+ if (configuredDir && configuredDir.trim()) {
424
+ return resolvePathFromRoot(projectRoot, configuredDir);
425
+ }
426
+
427
+ const canonicalDir = resolvePathFromRoot(projectRoot, DEFAULT_CONFIG_DIR);
428
+ const legacyDir = resolvePathFromRoot(projectRoot, LEGACY_CONFIG_DIR);
429
+ if (existsSync(join(canonicalDir, "default.toml"))) return canonicalDir;
430
+ if (existsSync(join(legacyDir, "default.toml"))) return legacyDir;
431
+ return canonicalDir;
432
+ }
433
+
434
+ function parseTomlWithLegacyFallback(
435
+ primaryPath: string,
436
+ fallbackPath?: string,
437
+ ): TomlObject {
438
+ if (existsSync(primaryPath)) return parseTomlFile(primaryPath);
439
+ if (fallbackPath && existsSync(fallbackPath)) return parseTomlFile(fallbackPath);
440
+ return {};
441
+ }
442
+
443
+ function normalizeBackend(value: string): string {
444
+ const text = value.trim().toLowerCase();
445
+ if (!text) return "lmstudio";
446
+ if (text === "openai_compatible") return "lmstudio";
447
+ if (text === "ollama_chat") return "ollama";
448
+ return text;
449
+ }
450
+
451
+ function normalizeWorkerImageRebuildMode(value: string): "auto" | "always" | "never" {
452
+ const text = value.trim().toLowerCase();
453
+ if (text === "always" || text === "1" || text === "true" || text === "yes" || text === "on") {
454
+ return "always";
455
+ }
456
+ if (text === "never" || text === "0" || text === "false" || text === "no" || text === "off") {
457
+ return "never";
458
+ }
459
+ return "auto";
460
+ }
461
+
462
+ function normalizeStartupPortConflictPolicy(value: string): "fail" | "terminate_pushpals" {
463
+ const text = value.trim().toLowerCase().replace(/-/g, "_");
464
+ if (
465
+ text === "terminate_pushpals" ||
466
+ text === "kill_pushpals" ||
467
+ text === "auto_kill_pushpals"
468
+ ) {
469
+ return "terminate_pushpals";
470
+ }
471
+ return "fail";
472
+ }
473
+
474
+ function defaultApiKeyForBackend(backend: string, endpoint: string): string {
475
+ const normalizedBackend = backend.trim().toLowerCase();
476
+ const normalizedEndpoint = endpoint.trim().toLowerCase();
477
+ const openAiKey = (process.env.OPENAI_API_KEY ?? "").trim();
478
+
479
+ if (normalizedBackend === "openai") {
480
+ return openAiKey;
481
+ }
482
+ if (normalizedBackend === "lmstudio") {
483
+ return "lmstudio";
484
+ }
485
+
486
+ // Safety: if backend is omitted/legacy but endpoint points to OpenAI,
487
+ // still allow OPENAI_API_KEY as fallback.
488
+ if (normalizedEndpoint.includes("api.openai.com")) {
489
+ return openAiKey;
490
+ }
491
+ return "";
492
+ }
493
+
494
+ function resolveLlmConfig(
495
+ serviceNode: TomlObject,
496
+ envPrefix: "LOCALBUDDY" | "REMOTEBUDDY" | "WORKERPALS",
497
+ defaults: { backend: string; endpoint: string; model: string; sessionId: string },
498
+ globalSessionId: string,
499
+ ): PushPalsLlmConfig {
500
+ const llmNode = getObject(serviceNode, "llm");
501
+ const backend = normalizeBackend(
502
+ firstNonEmpty(
503
+ process.env[`${envPrefix}_LLM_BACKEND`],
504
+ asString(llmNode.backend, defaults.backend),
505
+ defaults.backend,
506
+ ),
507
+ );
508
+ const endpoint = firstNonEmpty(
509
+ process.env[`${envPrefix}_LLM_ENDPOINT`],
510
+ asString(llmNode.endpoint, defaults.endpoint),
511
+ defaults.endpoint,
512
+ );
513
+ const model = firstNonEmpty(
514
+ process.env[`${envPrefix}_LLM_MODEL`],
515
+ asString(llmNode.model, defaults.model),
516
+ defaults.model,
517
+ );
518
+ const sessionId = firstNonEmpty(
519
+ process.env[`${envPrefix}_LLM_SESSION_ID`],
520
+ asString(llmNode.session_id, defaults.sessionId),
521
+ process.env.PUSHPALS_LLM_SESSION_ID,
522
+ globalSessionId,
523
+ );
524
+ const apiKey = firstNonEmpty(
525
+ process.env[`${envPrefix}_LLM_API_KEY`],
526
+ defaultApiKeyForBackend(backend, endpoint),
527
+ );
528
+ const reasoningEffort = firstNonEmpty(
529
+ process.env[`${envPrefix}_LLM_REASONING_EFFORT`],
530
+ asString(llmNode.reasoning_effort, ""),
531
+ );
532
+ const codexAuthMode = firstNonEmpty(
533
+ process.env[`${envPrefix}_LLM_CODEX_AUTH_MODE`],
534
+ asString(llmNode.codex_auth_mode, ""),
535
+ );
536
+ const codexBin = firstNonEmpty(
537
+ process.env[`${envPrefix}_LLM_CODEX_BIN`],
538
+ asString(llmNode.codex_bin, ""),
539
+ );
540
+ const codexTimeoutMs = Math.max(
541
+ 10_000,
542
+ asInt(
543
+ parseIntEnv(`${envPrefix}_LLM_CODEX_TIMEOUT_MS`) ?? llmNode.codex_timeout_ms,
544
+ 120_000,
545
+ ),
546
+ );
547
+ return {
548
+ backend,
549
+ endpoint,
550
+ model,
551
+ sessionId,
552
+ apiKey,
553
+ reasoningEffort,
554
+ codexAuthMode,
555
+ codexBin,
556
+ codexTimeoutMs,
557
+ };
558
+ }
559
+
560
+ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
561
+ const projectRootOverride = firstNonEmpty(
562
+ options.projectRoot,
563
+ process.env.PUSHPALS_PROJECT_ROOT_OVERRIDE,
564
+ PROJECT_ROOT,
565
+ );
566
+ const projectRoot = resolve(projectRootOverride);
567
+ const configDirOverride = firstNonEmpty(
568
+ options.configDir,
569
+ process.env.PUSHPALS_CONFIG_DIR_OVERRIDE,
570
+ "",
571
+ );
572
+ const configDir = resolveRuntimeConfigDir(projectRoot, configDirOverride);
573
+ const legacyConfigDir = resolvePathFromRoot(projectRoot, LEGACY_CONFIG_DIR);
574
+ const fallbackConfigDir =
575
+ !configDirOverride && configDir !== legacyConfigDir ? legacyConfigDir : "";
576
+ const cacheKey = `${projectRoot}::${configDir}::${process.env.PUSHPALS_PROFILE ?? ""}`;
577
+ if (!options.reload && cachedConfig && cachedConfigKey === cacheKey) {
578
+ return cachedConfig;
579
+ }
580
+
581
+ const defaultToml = parseTomlWithLegacyFallback(
582
+ join(configDir, "default.toml"),
583
+ fallbackConfigDir ? join(fallbackConfigDir, "default.toml") : undefined,
584
+ );
585
+ const preferredProfile = firstNonEmpty(
586
+ process.env.PUSHPALS_PROFILE,
587
+ asString(defaultToml.profile, "dev"),
588
+ "dev",
589
+ );
590
+ const profileToml = parseTomlWithLegacyFallback(
591
+ join(configDir, `${preferredProfile}.toml`),
592
+ fallbackConfigDir ? join(fallbackConfigDir, `${preferredProfile}.toml`) : undefined,
593
+ );
594
+ const localExampleToml = parseTomlWithLegacyFallback(
595
+ join(configDir, "local.example.toml"),
596
+ fallbackConfigDir ? join(fallbackConfigDir, "local.example.toml") : undefined,
597
+ );
598
+ const localToml = parseTomlWithLegacyFallback(
599
+ join(configDir, "local.toml"),
600
+ fallbackConfigDir ? join(fallbackConfigDir, "local.toml") : undefined,
601
+ );
602
+ const merged = mergeDeep(
603
+ mergeDeep(mergeDeep(defaultToml, profileToml), localExampleToml),
604
+ localToml,
605
+ );
606
+
607
+ const profile = firstNonEmpty(
608
+ process.env.PUSHPALS_PROFILE,
609
+ asString(merged.profile, preferredProfile),
610
+ preferredProfile,
611
+ );
612
+ const sessionId = firstNonEmpty(
613
+ process.env.PUSHPALS_SESSION_ID,
614
+ asString(merged.session_id, "dev"),
615
+ "dev",
616
+ );
617
+
618
+ const llmNode = getObject(merged, "llm");
619
+ const lmStudioNode = getObject(llmNode, "lmstudio");
620
+ const lmStudioContextWindow = Math.max(
621
+ 512,
622
+ asInt(parseIntEnv("PUSHPALS_LMSTUDIO_CONTEXT_WINDOW") ?? lmStudioNode.context_window, 4096),
623
+ );
624
+ const lmStudioMinOutputTokens = Math.max(
625
+ 64,
626
+ asInt(
627
+ parseIntEnv("PUSHPALS_LMSTUDIO_MIN_OUTPUT_TOKENS") ?? lmStudioNode.min_output_tokens,
628
+ 256,
629
+ ),
630
+ );
631
+ const lmStudioTokenSafetyMargin = Math.max(
632
+ 16,
633
+ asInt(
634
+ parseIntEnv("PUSHPALS_LMSTUDIO_TOKEN_SAFETY_MARGIN") ?? lmStudioNode.token_safety_margin,
635
+ 64,
636
+ ),
637
+ );
638
+ const lmStudioBatchTailMessages = Math.max(
639
+ 1,
640
+ asInt(
641
+ parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_TAIL_MESSAGES") ?? lmStudioNode.batch_tail_messages,
642
+ 3,
643
+ ),
644
+ );
645
+ const lmStudioBatchChunkTokens = Math.max(
646
+ 0,
647
+ asInt(
648
+ parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_CHUNK_TOKENS") ?? lmStudioNode.batch_chunk_tokens,
649
+ 0,
650
+ ),
651
+ );
652
+ const lmStudioBatchMemoryChars = Math.max(
653
+ 0,
654
+ asInt(
655
+ parseIntEnv("PUSHPALS_LMSTUDIO_BATCH_MEMORY_CHARS") ?? lmStudioNode.batch_memory_chars,
656
+ 0,
657
+ ),
658
+ );
659
+
660
+ const pathsNode = getObject(merged, "paths");
661
+ const dataDir = resolvePathFromRoot(
662
+ projectRoot,
663
+ firstNonEmpty(process.env.PUSHPALS_DATA_DIR, asString(pathsNode.data_dir, "outputs/data")),
664
+ );
665
+ const sharedDbPath = resolvePathFromRoot(
666
+ projectRoot,
667
+ firstNonEmpty(
668
+ process.env.PUSHPALS_DB_PATH,
669
+ asString(pathsNode.shared_db_path, join(dataDir, "pushpals.db")),
670
+ ),
671
+ );
672
+ const remotebuddyDbPath = resolvePathFromRoot(
673
+ projectRoot,
674
+ firstNonEmpty(
675
+ process.env.REMOTEBUDDY_DB_PATH,
676
+ asString(pathsNode.remotebuddy_db_path, join(dataDir, "remotebuddy-state.db")),
677
+ ),
678
+ );
679
+
680
+ const serverNode = getObject(merged, "server");
681
+ const serverPort = Math.max(1, asInt(parseIntEnv("PUSHPALS_PORT") ?? serverNode.port, 3001));
682
+ const serverUrl = normalizeLoopbackHttpUrl(
683
+ firstNonEmpty(
684
+ process.env.PUSHPALS_SERVER_URL,
685
+ asString(serverNode.url, `http://127.0.0.1:${serverPort}`),
686
+ `http://127.0.0.1:${serverPort}`,
687
+ ),
688
+ serverPort,
689
+ );
690
+ const serverHost = normalizeLoopbackHost(
691
+ firstNonEmpty(process.env.PUSHPALS_HOST, asString(serverNode.host, "127.0.0.1")),
692
+ );
693
+ const debugHttp = parseBoolEnv("PUSHPALS_DEBUG_HTTP") ?? asBoolean(serverNode.debug_http, false);
694
+ const staleClaimTtlMs = Math.max(
695
+ 5_000,
696
+ asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_TTL_MS") ?? serverNode.stale_claim_ttl_ms, 120_000),
697
+ );
698
+ const staleClaimSweepIntervalMs = Math.max(
699
+ 1_000,
700
+ asInt(
701
+ parseIntEnv("PUSHPALS_STALE_CLAIM_SWEEP_INTERVAL_MS") ??
702
+ serverNode.stale_claim_sweep_interval_ms,
703
+ 5_000,
704
+ ),
705
+ );
706
+
707
+ const globalStatusHeartbeatMs = parseIntEnv("PUSHPALS_STATUS_HEARTBEAT_MS");
708
+
709
+ const localNode = getObject(merged, "localbuddy");
710
+ const localEnabled = parseBoolEnv("LOCALBUDDY_ENABLED") ?? asBoolean(localNode.enabled, false);
711
+ const localPort = Math.max(1, asInt(parseIntEnv("LOCAL_AGENT_PORT") ?? localNode.port, 3003));
712
+ const localStatusHeartbeatMs = Math.max(
713
+ 0,
714
+ asInt(
715
+ parseIntEnv("LOCALBUDDY_STATUS_HEARTBEAT_MS") ??
716
+ globalStatusHeartbeatMs ??
717
+ localNode.status_heartbeat_ms,
718
+ 120_000,
719
+ ),
720
+ );
721
+ const localLlm = resolveLlmConfig(
722
+ localNode,
723
+ "LOCALBUDDY",
724
+ {
725
+ backend: "lmstudio",
726
+ endpoint: "http://127.0.0.1:1234",
727
+ model: "local-model",
728
+ sessionId: "localbuddy-dev",
729
+ },
730
+ sessionId,
731
+ );
732
+
733
+ const remoteNode = getObject(merged, "remotebuddy");
734
+ const remoteStatusHeartbeatMs = Math.max(
735
+ 0,
736
+ asInt(
737
+ parseIntEnv("REMOTEBUDDY_STATUS_HEARTBEAT_MS") ??
738
+ globalStatusHeartbeatMs ??
739
+ remoteNode.status_heartbeat_ms,
740
+ 120_000,
741
+ ),
742
+ );
743
+ const remotePollMs = Math.max(
744
+ 200,
745
+ asInt(parseIntEnv("REMOTEBUDDY_POLL_MS") ?? remoteNode.poll_ms, 2_000),
746
+ );
747
+ const remoteLlm = resolveLlmConfig(
748
+ remoteNode,
749
+ "REMOTEBUDDY",
750
+ {
751
+ backend: "lmstudio",
752
+ endpoint: "http://127.0.0.1:1234",
753
+ model: "local-model",
754
+ sessionId: "remotebuddy-dev",
755
+ },
756
+ sessionId,
757
+ );
758
+ const remoteMemoryNode = getObject(remoteNode, "memory");
759
+ const remoteMemoryEnabled =
760
+ parseBoolEnv("REMOTEBUDDY_MEMORY_ENABLED") ?? asBoolean(remoteMemoryNode.enabled, true);
761
+ const remoteMemoryIncludeCrossSession =
762
+ parseBoolEnv("REMOTEBUDDY_MEMORY_INCLUDE_CROSS_SESSION") ??
763
+ asBoolean(remoteMemoryNode.include_cross_session, true);
764
+ const remoteMemoryMaxRecallItems = Math.max(
765
+ 1,
766
+ Math.min(
767
+ 128,
768
+ asInt(
769
+ parseIntEnv("REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS") ?? remoteMemoryNode.max_recall_items,
770
+ DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_ITEMS,
771
+ ),
772
+ ),
773
+ );
774
+ const remoteMemoryMaxRecallChars = Math.max(
775
+ 120,
776
+ Math.min(
777
+ 64_000,
778
+ asInt(
779
+ parseIntEnv("REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS") ?? remoteMemoryNode.max_recall_chars,
780
+ DEFAULT_REMOTEBUDDY_MEMORY_MAX_RECALL_CHARS,
781
+ ),
782
+ ),
783
+ );
784
+ const remoteMemoryMaxSummaryChars = Math.max(
785
+ 64,
786
+ Math.min(
787
+ 16_000,
788
+ asInt(
789
+ parseIntEnv("REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS") ?? remoteMemoryNode.max_summary_chars,
790
+ DEFAULT_REMOTEBUDDY_MEMORY_MAX_SUMMARY_CHARS,
791
+ ),
792
+ ),
793
+ );
794
+ const remoteMemoryRetentionDays = Math.max(
795
+ 1,
796
+ Math.min(
797
+ 3650,
798
+ asInt(
799
+ parseIntEnv("REMOTEBUDDY_MEMORY_RETENTION_DAYS") ?? remoteMemoryNode.retention_days,
800
+ DEFAULT_REMOTEBUDDY_MEMORY_RETENTION_DAYS,
801
+ ),
802
+ ),
803
+ );
804
+ const remoteAutonomyNode = getObject(remoteNode, "autonomy");
805
+ const remoteAutonomyReplayNode = getObject(remoteAutonomyNode, "replay");
806
+ const remoteAutonomyDispatchByTypeCfg = {
807
+ flaky_test: 4,
808
+ lint_fix: 3,
809
+ type_fix: 3,
810
+ small_refactor: 2,
811
+ feature_small: 2,
812
+ feature_medium: 1,
813
+ feature_large: 0,
814
+ docs: 1,
815
+ dep_bump: 0,
816
+ };
817
+ const remoteAutonomyDispatchByType = {
818
+ ...remoteAutonomyDispatchByTypeCfg,
819
+ ...asStringNumberRecord(remoteAutonomyNode.max_dispatch_per_hour_by_type),
820
+ };
821
+ const remoteAutonomyDispatchByComponentCfg = {
822
+ "apps/server": 3,
823
+ "apps/remotebuddy": 2,
824
+ "apps/workerpals": 2,
825
+ "apps/client": 2,
826
+ "packages/protocol": 1,
827
+ "packages/shared": 2,
828
+ "tests/integration": 2,
829
+ "tests/unit": 2,
830
+ };
831
+ const remoteAutonomyDispatchByComponentRaw = asStringNumberRecord(
832
+ remoteAutonomyNode.max_dispatch_per_hour_by_component,
833
+ );
834
+ const legacyAutonomyComponentAliasMap = new Map<string, string>(
835
+ Object.keys(remoteAutonomyDispatchByComponentCfg).flatMap((key) => {
836
+ const direct = normalizeAutonomyComponentArea(key);
837
+ const legacyUnderscore = normalizeAutonomyComponentArea(key.replace(/\//g, "_"));
838
+ const legacyHyphen = normalizeAutonomyComponentArea(key.replace(/\//g, "-"));
839
+ return [direct, legacyUnderscore, legacyHyphen]
840
+ .filter((value): value is string => Boolean(value))
841
+ .map((value) => [value, key] as const);
842
+ }),
843
+ );
844
+ const coerceAutonomyComponentConfigKey = (value: string): string | null => {
845
+ const direct = normalizeAutonomyComponentArea(value);
846
+ const legacyAliasCandidate = normalizeAutonomyComponentArea(
847
+ value
848
+ .trim()
849
+ .toLowerCase()
850
+ .replace(/\\/g, "/")
851
+ .replace(/_+/g, "/")
852
+ .replace(/-+/g, "/")
853
+ .replace(/\/+/g, "/"),
854
+ );
855
+ if (legacyAliasCandidate && legacyAutonomyComponentAliasMap.has(legacyAliasCandidate)) {
856
+ return legacyAutonomyComponentAliasMap.get(legacyAliasCandidate) ?? legacyAliasCandidate;
857
+ }
858
+ return direct;
859
+ };
860
+ const remoteAutonomyDispatchByComponent = Object.fromEntries(
861
+ Object.entries(remoteAutonomyDispatchByComponentCfg).map(([key, value]) => [
862
+ coerceAutonomyComponentConfigKey(key) ?? key,
863
+ value,
864
+ ]),
865
+ ) as Record<string, number>;
866
+ for (const [rawKey, rawValue] of Object.entries(remoteAutonomyDispatchByComponentRaw)) {
867
+ const canonical = coerceAutonomyComponentConfigKey(rawKey);
868
+ if (!canonical) continue;
869
+ const parsed =
870
+ typeof rawValue === "number"
871
+ ? rawValue
872
+ : typeof rawValue === "string"
873
+ ? Number.parseInt(rawValue.trim(), 10)
874
+ : Number.NaN;
875
+ remoteAutonomyDispatchByComponent[canonical] = Number.isFinite(parsed)
876
+ ? Math.max(0, Math.floor(parsed))
877
+ : 0;
878
+ }
879
+
880
+ const workerNode = getObject(merged, "workerpals");
881
+ const workerOpenHandsNode = getObject(workerNode, "openhands");
882
+ const workerPollMs = Math.max(
883
+ 200,
884
+ asInt(parseIntEnv("WORKERPALS_POLL_MS") ?? workerNode.poll_ms, 2_000),
885
+ );
886
+ const workerHeartbeatMs = Math.max(
887
+ 200,
888
+ asInt(parseIntEnv("WORKERPALS_HEARTBEAT_MS") ?? workerNode.heartbeat_ms, 5_000),
889
+ );
890
+ const workerExecutor = firstNonEmpty(
891
+ process.env.WORKERPALS_EXECUTOR,
892
+ asString(workerNode.executor, "openhands"),
893
+ "openhands",
894
+ ).toLowerCase();
895
+ const workerOpenHandsPython = firstNonEmpty(
896
+ process.env.WORKERPALS_OPENHANDS_PYTHON,
897
+ asString(workerNode.openhands_python, "python"),
898
+ "python",
899
+ );
900
+ const workerOpenHandsTimeoutMs = Math.max(
901
+ 10_000,
902
+ asInt(
903
+ parseIntEnv("WORKERPALS_OPENHANDS_TIMEOUT_MS") ?? workerNode.openhands_timeout_ms,
904
+ 1_800_000,
905
+ ),
906
+ );
907
+ const workerMiniswePython = firstNonEmpty(
908
+ process.env.WORKERPALS_MINISWE_PYTHON,
909
+ asString(workerNode.miniswe_python, "python"),
910
+ "python",
911
+ );
912
+ const workerMinisweTimeoutMs = Math.max(
913
+ 10_000,
914
+ asInt(parseIntEnv("WORKERPALS_MINISWE_TIMEOUT_MS") ?? workerNode.miniswe_timeout_ms, 1_800_000),
915
+ );
916
+ const workerOpenAICodexPython = firstNonEmpty(
917
+ process.env.PUSHPALS_OPENAI_CODEX_PYTHON,
918
+ asString(workerNode.openai_codex_python, "python"),
919
+ "python",
920
+ );
921
+ const workerOpenAICodexTimeoutMs = Math.max(
922
+ 10_000,
923
+ asInt(workerNode.openai_codex_timeout_ms, 7_200_000),
924
+ );
925
+ const workerQualityMaxAutoRevisions = Math.max(
926
+ 0,
927
+ Math.min(
928
+ 10,
929
+ asInt(
930
+ parseIntEnv("WORKERPALS_QUALITY_MAX_AUTO_REVISIONS") ?? workerNode.quality_max_auto_revisions,
931
+ DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS,
932
+ ),
933
+ ),
934
+ );
935
+ const workerFileModifyingJobs = (() => {
936
+ const envRaw = firstNonEmpty(process.env.WORKERPALS_FILE_MODIFYING_JOBS);
937
+ const parsed = envRaw
938
+ ? envRaw
939
+ .split(",")
940
+ .map((entry) => entry.trim())
941
+ .filter(Boolean)
942
+ : asStringArray(workerNode.file_modifying_jobs);
943
+ const out = parsed.length > 0 ? parsed : DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS;
944
+ return [...new Set(out)];
945
+ })();
946
+ const workerOutputMaxChars = Math.max(
947
+ 8_192,
948
+ Math.min(
949
+ 4_194_304,
950
+ asInt(
951
+ parseIntEnv("WORKERPALS_OUTPUT_MAX_CHARS") ?? workerNode.output_max_chars,
952
+ DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS,
953
+ ),
954
+ ),
955
+ );
956
+ const workerOutputMaxLines = Math.max(
957
+ 50,
958
+ Math.min(
959
+ 20_000,
960
+ asInt(
961
+ parseIntEnv("WORKERPALS_OUTPUT_MAX_LINES") ?? workerNode.output_max_lines,
962
+ DEFAULT_WORKERPALS_OUTPUT_MAX_LINES,
963
+ ),
964
+ ),
965
+ );
966
+ const workerOutputMaxHeadLines = Math.max(
967
+ 1,
968
+ Math.min(
969
+ workerOutputMaxLines,
970
+ asInt(
971
+ parseIntEnv("WORKERPALS_OUTPUT_MAX_HEAD_LINES") ?? workerNode.output_max_head_lines,
972
+ DEFAULT_WORKERPALS_OUTPUT_MAX_HEAD_LINES,
973
+ ),
974
+ ),
975
+ );
976
+ const workerQualityValidationStepTimeoutMs = Math.max(
977
+ 1_000,
978
+ asInt(
979
+ parseIntEnv("WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS") ??
980
+ workerNode.quality_validation_step_timeout_ms,
981
+ DEFAULT_WORKERPALS_QUALITY_VALIDATION_STEP_TIMEOUT_MS,
982
+ ),
983
+ );
984
+ const workerQualityCriticTimeoutMs = Math.max(
985
+ 1_000,
986
+ asInt(
987
+ parseIntEnv("WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS") ??
988
+ workerNode.quality_critic_timeout_ms,
989
+ DEFAULT_WORKERPALS_QUALITY_CRITIC_TIMEOUT_MS,
990
+ ),
991
+ );
992
+ const workerQualitySoftPassOnExhausted =
993
+ parseBoolEnv("WORKERPALS_QUALITY_SOFT_PASS_ON_EXHAUSTED") ??
994
+ asBoolean(workerNode.quality_soft_pass_on_exhausted, true);
995
+ const workerQualityCriticMinScore = (() => {
996
+ const configThresholdRaw =
997
+ workerNode.quality_critic_min_score == null
998
+ ? ""
999
+ : String(workerNode.quality_critic_min_score);
1000
+ const raw = firstNonEmpty(
1001
+ process.env.WORKERPALS_QUALITY_CRITIC_MIN_SCORE,
1002
+ configThresholdRaw,
1003
+ String(DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE),
1004
+ );
1005
+ const parsed = Number.parseFloat(raw);
1006
+ if (!Number.isFinite(parsed)) return DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE;
1007
+ return Math.max(0, Math.min(10, parsed));
1008
+ })();
1009
+ const workerQualityCriticMaxDiffChars = Math.max(
1010
+ 256,
1011
+ Math.min(
1012
+ 524_288,
1013
+ asInt(
1014
+ parseIntEnv("WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS") ??
1015
+ workerNode.quality_critic_max_diff_chars,
1016
+ DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_DIFF_CHARS,
1017
+ ),
1018
+ ),
1019
+ );
1020
+ const workerQualityCriticMaxValidationOutputChars = Math.max(
1021
+ 256,
1022
+ Math.min(
1023
+ 524_288,
1024
+ asInt(
1025
+ parseIntEnv("WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS") ??
1026
+ workerNode.quality_critic_max_validation_output_chars,
1027
+ DEFAULT_WORKERPALS_QUALITY_CRITIC_MAX_VALIDATION_OUTPUT_CHARS,
1028
+ ),
1029
+ ),
1030
+ );
1031
+ const workerExecutorResultPrefix = (() => {
1032
+ if (process.env.WORKERPALS_EXECUTOR_RESULT_PREFIX !== undefined) {
1033
+ const raw = process.env.WORKERPALS_EXECUTOR_RESULT_PREFIX;
1034
+ if (typeof raw === "string" && raw.length > 0) return raw;
1035
+ }
1036
+ if (
1037
+ Object.prototype.hasOwnProperty.call(workerNode, "executor_result_prefix") &&
1038
+ typeof workerNode.executor_result_prefix === "string" &&
1039
+ workerNode.executor_result_prefix.length > 0
1040
+ ) {
1041
+ return workerNode.executor_result_prefix;
1042
+ }
1043
+ return DEFAULT_WORKERPALS_EXECUTOR_RESULT_PREFIX;
1044
+ })();
1045
+ const workerOpenHandsStuckGuardEnabled =
1046
+ parseBoolEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_ENABLED") ??
1047
+ asBoolean(workerNode.openhands_stuck_guard_enabled, true);
1048
+ const workerOpenHandsStuckGuardExploreLimit = Math.max(
1049
+ 6,
1050
+ asInt(
1051
+ parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_EXPLORE_LIMIT") ??
1052
+ workerNode.openhands_stuck_guard_explore_limit,
1053
+ 18,
1054
+ ),
1055
+ );
1056
+ const workerOpenHandsStuckGuardMinElapsedMs = Math.max(
1057
+ 60_000,
1058
+ asInt(
1059
+ parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_MIN_ELAPSED_MS") ??
1060
+ workerNode.openhands_stuck_guard_min_elapsed_ms,
1061
+ 180_000,
1062
+ ),
1063
+ );
1064
+ const workerOpenHandsStuckGuardBroadScanLimit = Math.max(
1065
+ 1,
1066
+ asInt(
1067
+ parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_BROAD_SCAN_LIMIT") ??
1068
+ workerNode.openhands_stuck_guard_broad_scan_limit,
1069
+ 2,
1070
+ ),
1071
+ );
1072
+ const workerOpenHandsStuckGuardNoProgressMaxMs = Math.max(
1073
+ 60_000,
1074
+ asInt(
1075
+ parseIntEnv("WORKERPALS_OPENHANDS_STUCK_GUARD_NO_PROGRESS_MAX_MS") ??
1076
+ workerNode.openhands_stuck_guard_no_progress_max_ms,
1077
+ 300_000,
1078
+ ),
1079
+ );
1080
+ const workerOpenHandsAutoSteerEnabled =
1081
+ parseBoolEnv("WORKERPALS_OPENHANDS_AUTO_STEER_ENABLED") ??
1082
+ asBoolean(workerOpenHandsNode.auto_steer_enabled, true);
1083
+ const workerOpenHandsAutoSteerInitialDelaySec = Math.max(
1084
+ 0,
1085
+ Math.min(
1086
+ 600,
1087
+ asInt(
1088
+ parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_INITIAL_DELAY_SEC") ??
1089
+ workerOpenHandsNode.auto_steer_initial_delay_sec,
1090
+ 90,
1091
+ ),
1092
+ ),
1093
+ );
1094
+ const workerOpenHandsAutoSteerIntervalSec = Math.max(
1095
+ 15,
1096
+ Math.min(
1097
+ 600,
1098
+ asInt(
1099
+ parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_INTERVAL_SEC") ??
1100
+ workerOpenHandsNode.auto_steer_interval_sec,
1101
+ 60,
1102
+ ),
1103
+ ),
1104
+ );
1105
+ const workerOpenHandsAutoSteerMaxNudges = Math.max(
1106
+ 0,
1107
+ Math.min(
1108
+ 120,
1109
+ asInt(
1110
+ parseIntEnv("WORKERPALS_OPENHANDS_AUTO_STEER_MAX_NUDGES") ??
1111
+ workerOpenHandsNode.auto_steer_max_nudges,
1112
+ 30,
1113
+ ),
1114
+ ),
1115
+ );
1116
+ const workerRequirePush =
1117
+ parseBoolEnv("WORKERPALS_REQUIRE_PUSH") ?? asBoolean(workerNode.require_push, false);
1118
+ const workerPushAgentBranchEnv = parseBoolEnv("WORKERPALS_PUSH_AGENT_BRANCH");
1119
+ const workerPushAgentBranch =
1120
+ workerRequirePush ||
1121
+ (workerPushAgentBranchEnv ?? asBoolean(workerNode.push_agent_branch, false));
1122
+ const workerSkipDockerSelfCheck =
1123
+ parseBoolEnv("WORKERPALS_SKIP_DOCKER_SELF_CHECK") ??
1124
+ asBoolean(workerNode.skip_docker_self_check, false);
1125
+ const workerDockerAgentStartupTimeoutMs = Math.max(
1126
+ 10_000,
1127
+ Math.min(
1128
+ 180_000,
1129
+ asInt(
1130
+ parseIntEnv("WORKERPALS_DOCKER_AGENT_STARTUP_TIMEOUT_MS") ??
1131
+ workerNode.docker_agent_startup_timeout_ms,
1132
+ 45_000,
1133
+ ),
1134
+ ),
1135
+ );
1136
+ const workerDockerWarmMaxAttempts = Math.max(
1137
+ 1,
1138
+ Math.min(
1139
+ 5,
1140
+ asInt(
1141
+ parseIntEnv("WORKERPALS_DOCKER_WARM_MAX_ATTEMPTS") ?? workerNode.docker_warm_max_attempts,
1142
+ 3,
1143
+ ),
1144
+ ),
1145
+ );
1146
+ const workerDockerWarmRetryBackoffMs = Math.max(
1147
+ 250,
1148
+ Math.min(
1149
+ 60_000,
1150
+ asInt(
1151
+ parseIntEnv("WORKERPALS_DOCKER_WARM_RETRY_BACKOFF_MS") ??
1152
+ workerNode.docker_warm_retry_backoff_ms,
1153
+ 2_000,
1154
+ ),
1155
+ ),
1156
+ );
1157
+ const workerDockerJobMaxAttempts = Math.max(
1158
+ 1,
1159
+ Math.min(
1160
+ 3,
1161
+ asInt(
1162
+ parseIntEnv("WORKERPALS_DOCKER_JOB_MAX_ATTEMPTS") ?? workerNode.docker_job_max_attempts,
1163
+ 2,
1164
+ ),
1165
+ ),
1166
+ );
1167
+ const workerDockerJobRetryBackoffMs = Math.max(
1168
+ 250,
1169
+ Math.min(
1170
+ 60_000,
1171
+ asInt(
1172
+ parseIntEnv("WORKERPALS_DOCKER_JOB_RETRY_BACKOFF_MS") ??
1173
+ workerNode.docker_job_retry_backoff_ms,
1174
+ 3_000,
1175
+ ),
1176
+ ),
1177
+ );
1178
+ const workerDockerWarmMemoryMb = Math.max(
1179
+ 512,
1180
+ Math.min(
1181
+ 32_768,
1182
+ asInt(
1183
+ parseIntEnv("WORKERPALS_DOCKER_WARM_MEMORY_MB") ?? workerNode.docker_warm_memory_mb,
1184
+ 2_048,
1185
+ ),
1186
+ ),
1187
+ );
1188
+ const workerDockerWarmCpus = Math.max(
1189
+ 1,
1190
+ Math.min(
1191
+ 16,
1192
+ asInt(parseIntEnv("WORKERPALS_DOCKER_WARM_CPUS") ?? workerNode.docker_warm_cpus, 2),
1193
+ ),
1194
+ );
1195
+ const workerLlm = resolveLlmConfig(
1196
+ workerNode,
1197
+ "WORKERPALS",
1198
+ {
1199
+ backend: "lmstudio",
1200
+ endpoint: "http://127.0.0.1:1234",
1201
+ model: "local-model",
1202
+ sessionId: "workerpals-dev",
1203
+ },
1204
+ sessionId,
1205
+ );
1206
+
1207
+ const scmNode = getObject(merged, "source_control_manager");
1208
+ const scmRepoPath = resolvePathFromRoot(
1209
+ projectRoot,
1210
+ firstNonEmpty(
1211
+ process.env.SOURCE_CONTROL_MANAGER_REPO_PATH,
1212
+ asString(scmNode.repo_path, ".worktrees/source_control_manager"),
1213
+ ".worktrees/source_control_manager",
1214
+ ),
1215
+ );
1216
+ const scmRemote = asString(process.env.SOURCE_CONTROL_MANAGER_REMOTE ?? scmNode.remote, "origin");
1217
+ const scmMainBranch = firstNonEmpty(
1218
+ process.env.SOURCE_CONTROL_MANAGER_MAIN_BRANCH,
1219
+ process.env.PUSHPALS_INTEGRATION_BRANCH,
1220
+ asString(scmNode.pushpals_branch, "main_agents"),
1221
+ "main_agents",
1222
+ );
1223
+ const scmBaseBranch = firstNonEmpty(
1224
+ process.env.PUSHPALS_INTEGRATION_BASE_BRANCH,
1225
+ asString(scmNode.base_branch, "main"),
1226
+ "main",
1227
+ );
1228
+ const scmBranchPrefix = asString(
1229
+ process.env.SOURCE_CONTROL_MANAGER_BRANCH_PREFIX ?? scmNode.branch_prefix,
1230
+ "agent/",
1231
+ );
1232
+ const scmPollIntervalSeconds = Math.max(
1233
+ 1,
1234
+ asInt(
1235
+ parseIntEnv("SOURCE_CONTROL_MANAGER_POLL_INTERVAL_SECONDS") ?? scmNode.poll_interval_seconds,
1236
+ 10,
1237
+ ),
1238
+ );
1239
+ const scmChecks = asCheckArray(scmNode.checks);
1240
+ const scmStateDir = resolvePathFromRoot(
1241
+ projectRoot,
1242
+ firstNonEmpty(
1243
+ process.env.SOURCE_CONTROL_MANAGER_STATE_DIR,
1244
+ asString(scmNode.state_dir, join(dataDir, "source_control_manager")),
1245
+ join(dataDir, "source_control_manager"),
1246
+ ),
1247
+ );
1248
+ const scmPort = Math.max(
1249
+ 1,
1250
+ Math.min(65_535, asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_PORT") ?? scmNode.port, 3002)),
1251
+ );
1252
+ const scmDeleteAfterMerge =
1253
+ parseBoolEnv("SOURCE_CONTROL_MANAGER_DELETE_AFTER_MERGE") ??
1254
+ asBoolean(scmNode.delete_after_merge, false);
1255
+ const scmMaxAttempts = Math.max(
1256
+ 1,
1257
+ asInt(parseIntEnv("SOURCE_CONTROL_MANAGER_MAX_ATTEMPTS") ?? scmNode.max_attempts, 3),
1258
+ );
1259
+ const scmMergeStrategyRaw = firstNonEmpty(
1260
+ process.env.SOURCE_CONTROL_MANAGER_MERGE_STRATEGY,
1261
+ asString(scmNode.merge_strategy, "cherry-pick"),
1262
+ "cherry-pick",
1263
+ );
1264
+ const scmMergeStrategy =
1265
+ scmMergeStrategyRaw === "no-ff" || scmMergeStrategyRaw === "ff-only"
1266
+ ? scmMergeStrategyRaw
1267
+ : "cherry-pick";
1268
+ let scmPushMainAfterMerge = asBoolean(scmNode.push_main_after_merge, true);
1269
+ const scmPushMainAfterMergeEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_PUSH_MAIN_AFTER_MERGE");
1270
+ if (scmPushMainAfterMergeEnv != null) scmPushMainAfterMerge = scmPushMainAfterMergeEnv;
1271
+ const scmNoPushEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_NO_PUSH");
1272
+ if (scmNoPushEnv != null) scmPushMainAfterMerge = !scmNoPushEnv;
1273
+ let scmOpenPrAfterPush = asBoolean(scmNode.open_pr_after_push, true);
1274
+ const scmOpenPrAfterPushEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_OPEN_PR_AFTER_PUSH");
1275
+ if (scmOpenPrAfterPushEnv != null) scmOpenPrAfterPush = scmOpenPrAfterPushEnv;
1276
+ const scmDisableAutoPrEnv = parseBoolEnv("SOURCE_CONTROL_MANAGER_DISABLE_AUTO_PR");
1277
+ if (scmDisableAutoPrEnv != null) scmOpenPrAfterPush = !scmDisableAutoPrEnv;
1278
+ const scmPrBaseBranch = firstNonEmpty(
1279
+ process.env.SOURCE_CONTROL_MANAGER_PR_BASE_BRANCH,
1280
+ asString(scmNode.pr_base_branch, scmBaseBranch),
1281
+ scmBaseBranch,
1282
+ );
1283
+ const scmPrTitle = firstNonEmpty(
1284
+ process.env.SOURCE_CONTROL_MANAGER_PR_TITLE,
1285
+ asString(scmNode.pr_title, ""),
1286
+ );
1287
+ const scmPrBody = firstNonEmpty(
1288
+ process.env.SOURCE_CONTROL_MANAGER_PR_BODY,
1289
+ asString(scmNode.pr_body, ""),
1290
+ );
1291
+ const scmPrDraft =
1292
+ parseBoolEnv("SOURCE_CONTROL_MANAGER_PR_DRAFT") ?? asBoolean(scmNode.pr_draft, false);
1293
+ const scmStatusHeartbeatMs = Math.max(
1294
+ 0,
1295
+ asInt(
1296
+ parseIntEnv("SOURCE_CONTROL_MANAGER_STATUS_HEARTBEAT_MS") ??
1297
+ globalStatusHeartbeatMs ??
1298
+ scmNode.status_heartbeat_ms,
1299
+ 120_000,
1300
+ ),
1301
+ );
1302
+ const scmSkipCleanCheck =
1303
+ parseBoolEnv("SOURCE_CONTROL_MANAGER_SKIP_CLEAN_CHECK") ??
1304
+ asBoolean(scmNode.skip_clean_check, false);
1305
+ const scmAutoCreateMainBranch =
1306
+ parseBoolEnv("SOURCE_CONTROL_MANAGER_AUTO_CREATE_MAIN_BRANCH") ??
1307
+ asBoolean(scmNode.auto_create_main_branch, false);
1308
+
1309
+ const scmReviewAgentNode = getObject(scmNode, "review_agent");
1310
+ const scmReviewAgentEnabled =
1311
+ parseBoolEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_ENABLED") ??
1312
+ asBoolean(scmReviewAgentNode.enabled, false);
1313
+ const scmReviewAgentPollIntervalMs = Math.max(
1314
+ 5_000,
1315
+ asInt(
1316
+ parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_POLL_INTERVAL_MS") ??
1317
+ scmReviewAgentNode.poll_interval_ms,
1318
+ 60_000,
1319
+ ),
1320
+ );
1321
+ const scmReviewAgentReviewerMdPath = firstNonEmpty(
1322
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_REVIEWER_MD_PATH,
1323
+ asString(scmReviewAgentNode.reviewer_md_path, "prompts/review_agent/reviewer.md"),
1324
+ "prompts/review_agent/reviewer.md",
1325
+ );
1326
+ const scmReviewAgentPassThreshold = (() => {
1327
+ const configThresholdRaw =
1328
+ scmReviewAgentNode.pass_threshold == null ? "" : String(scmReviewAgentNode.pass_threshold);
1329
+ const raw = firstNonEmpty(
1330
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_PASS_THRESHOLD,
1331
+ configThresholdRaw,
1332
+ "9.5",
1333
+ );
1334
+ const parsed = Number.parseFloat(raw);
1335
+ return Number.isFinite(parsed) ? Math.max(1, Math.min(10, parsed)) : 9.5;
1336
+ })();
1337
+ const scmReviewAgentMaxPrCommentsBeforeGiveUp = Math.max(
1338
+ 1,
1339
+ Math.min(
1340
+ 100,
1341
+ asInt(
1342
+ parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_MAX_PR_COMMENTS_BEFORE_GIVE_UP") ??
1343
+ scmReviewAgentNode.max_pr_comments_before_give_up,
1344
+ 10,
1345
+ ),
1346
+ ),
1347
+ );
1348
+ const scmReviewAgentMergeMethodRaw = firstNonEmpty(
1349
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_MERGE_METHOD,
1350
+ asString(scmReviewAgentNode.merge_method, "squash"),
1351
+ "squash",
1352
+ ).toLowerCase();
1353
+ const scmReviewAgentMergeMethod: "squash" | "merge" | "rebase" =
1354
+ scmReviewAgentMergeMethodRaw === "merge" || scmReviewAgentMergeMethodRaw === "rebase"
1355
+ ? scmReviewAgentMergeMethodRaw
1356
+ : "squash";
1357
+ const scmReviewAgentCodexBin = firstNonEmpty(
1358
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_BIN,
1359
+ asString(scmReviewAgentNode.codex_bin, "bun x --yes @openai/codex"),
1360
+ "bun x --yes @openai/codex",
1361
+ );
1362
+ const scmReviewAgentCodexAuthMode = firstNonEmpty(
1363
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_AUTH_MODE,
1364
+ asString(scmReviewAgentNode.codex_auth_mode, "chatgpt"),
1365
+ "chatgpt",
1366
+ );
1367
+ const scmReviewAgentCodexHomeDir = firstNonEmpty(
1368
+ process.env.SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_HOME_DIR,
1369
+ asString(scmReviewAgentNode.codex_home_dir, ""),
1370
+ );
1371
+ const scmReviewAgentCodexTimeoutMs = Math.max(
1372
+ 30_000,
1373
+ asInt(
1374
+ parseIntEnv("SOURCE_CONTROL_MANAGER_REVIEW_AGENT_CODEX_TIMEOUT_MS") ??
1375
+ scmReviewAgentNode.codex_timeout_ms,
1376
+ 300_000,
1377
+ ),
1378
+ );
1379
+
1380
+ const startupNode = getObject(merged, "startup");
1381
+ const startupWorkerImageRebuild = normalizeWorkerImageRebuildMode(
1382
+ firstNonEmpty(
1383
+ process.env.PUSHPALS_WORKER_IMAGE_REBUILD,
1384
+ asString(startupNode.worker_image_rebuild, "auto"),
1385
+ "auto",
1386
+ ),
1387
+ );
1388
+ const startupLogConfigOnStart =
1389
+ parseBoolEnv("PUSHPALS_LOG_CONFIG_ON_START") ??
1390
+ asBoolean(startupNode.log_config_on_start, true);
1391
+ const startupSyncIntegrationWithMain =
1392
+ parseBoolEnv("PUSHPALS_SYNC_INTEGRATION_WITH_MAIN") ??
1393
+ asBoolean(startupNode.sync_integration_with_main, true);
1394
+ const startupSkipLlmPreflight =
1395
+ parseBoolEnv("PUSHPALS_SKIP_LLM_PREFLIGHT") ?? asBoolean(startupNode.skip_llm_preflight, false);
1396
+ const startupAutoStartLmStudio =
1397
+ parseBoolEnv("PUSHPALS_AUTO_START_LMSTUDIO") ??
1398
+ asBoolean(startupNode.auto_start_lmstudio, true);
1399
+ const startupLmStudioReadyTimeoutMs = Math.max(
1400
+ 1_000,
1401
+ asInt(
1402
+ parseIntEnv("PUSHPALS_LMSTUDIO_READY_TIMEOUT_MS") ?? startupNode.lmstudio_ready_timeout_ms,
1403
+ 120_000,
1404
+ ),
1405
+ );
1406
+ const startupLmStudioCli = firstNonEmpty(
1407
+ process.env.PUSHPALS_LMSTUDIO_CLI,
1408
+ asString(startupNode.lmstudio_cli, "lms"),
1409
+ "lms",
1410
+ );
1411
+ const startupLmStudioPort = Math.max(
1412
+ 1,
1413
+ Math.min(
1414
+ 65_535,
1415
+ asInt(parseIntEnv("PUSHPALS_LMSTUDIO_PORT") ?? startupNode.lmstudio_port, 1234),
1416
+ ),
1417
+ );
1418
+ const startupLmStudioStartArgs = firstNonEmpty(
1419
+ process.env.PUSHPALS_LMSTUDIO_START_ARGS,
1420
+ asString(startupNode.lmstudio_start_args, ""),
1421
+ );
1422
+ const startupWarmup =
1423
+ parseBoolEnv("PUSHPALS_STARTUP_WARMUP") ?? asBoolean(startupNode.startup_warmup, true);
1424
+ const startupWarmupTimeoutMs = Math.max(
1425
+ 15_000,
1426
+ asInt(
1427
+ parseIntEnv("PUSHPALS_STARTUP_WARMUP_TIMEOUT_MS") ?? startupNode.startup_warmup_timeout_ms,
1428
+ 120_000,
1429
+ ),
1430
+ );
1431
+ const startupWarmupPollMs = Math.max(
1432
+ 250,
1433
+ Math.min(
1434
+ 5_000,
1435
+ asInt(
1436
+ parseIntEnv("PUSHPALS_STARTUP_WARMUP_POLL_MS") ?? startupNode.startup_warmup_poll_ms,
1437
+ 1_000,
1438
+ ),
1439
+ ),
1440
+ );
1441
+ const startupAllowExternalClean =
1442
+ parseBoolEnv("PUSHPALS_ALLOW_EXTERNAL_CLEAN") ??
1443
+ asBoolean(startupNode.allow_external_clean, false);
1444
+ const startupPortPreflight =
1445
+ parseBoolEnv("PUSHPALS_STARTUP_PORT_PREFLIGHT") ??
1446
+ asBoolean(startupNode.port_preflight, true);
1447
+ const startupPortConflictPolicy = normalizeStartupPortConflictPolicy(
1448
+ firstNonEmpty(
1449
+ process.env.PUSHPALS_STARTUP_PORT_CONFLICT_POLICY,
1450
+ asString(startupNode.port_conflict_policy, "terminate_pushpals"),
1451
+ "terminate_pushpals",
1452
+ ),
1453
+ );
1454
+
1455
+ const clientNode = getObject(merged, "client");
1456
+
1457
+ const authToken = firstNonEmpty(process.env.PUSHPALS_AUTH_TOKEN) || null;
1458
+ const gitToken =
1459
+ firstNonEmpty(process.env.PUSHPALS_GIT_TOKEN, process.env.GITHUB_TOKEN, process.env.GH_TOKEN) ||
1460
+ null;
1461
+
1462
+ const config: PushPalsConfig = {
1463
+ projectRoot,
1464
+ configDir,
1465
+ profile,
1466
+ sessionId,
1467
+ authToken,
1468
+ gitToken,
1469
+ llm: {
1470
+ lmstudio: {
1471
+ contextWindow: lmStudioContextWindow,
1472
+ minOutputTokens: lmStudioMinOutputTokens,
1473
+ tokenSafetyMargin: lmStudioTokenSafetyMargin,
1474
+ batchTailMessages: lmStudioBatchTailMessages,
1475
+ batchChunkTokens: lmStudioBatchChunkTokens,
1476
+ batchMemoryChars: lmStudioBatchMemoryChars,
1477
+ },
1478
+ },
1479
+ paths: {
1480
+ dataDir,
1481
+ sharedDbPath,
1482
+ remotebuddyDbPath,
1483
+ },
1484
+ server: {
1485
+ url: serverUrl,
1486
+ host: serverHost,
1487
+ port: serverPort,
1488
+ debugHttp,
1489
+ staleClaimTtlMs,
1490
+ staleClaimSweepIntervalMs,
1491
+ },
1492
+ localbuddy: {
1493
+ enabled: localEnabled,
1494
+ port: localPort,
1495
+ statusHeartbeatMs: localStatusHeartbeatMs,
1496
+ llm: localLlm,
1497
+ },
1498
+ remotebuddy: {
1499
+ pollMs: remotePollMs,
1500
+ statusHeartbeatMs: remoteStatusHeartbeatMs,
1501
+ workerpalOnlineTtlMs: Math.max(
1502
+ 1_000,
1503
+ asInt(
1504
+ parseIntEnv("REMOTEBUDDY_WORKERPAL_ONLINE_TTL_MS") ?? remoteNode.workerpal_online_ttl_ms,
1505
+ 15_000,
1506
+ ),
1507
+ ),
1508
+ waitForWorkerpalMs: Math.max(
1509
+ 0,
1510
+ asInt(
1511
+ parseIntEnv("REMOTEBUDDY_WAIT_FOR_WORKERPAL_MS") ?? remoteNode.wait_for_workerpal_ms,
1512
+ 15_000,
1513
+ ),
1514
+ ),
1515
+ autoSpawnWorkerpals:
1516
+ parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ??
1517
+ asBoolean(remoteNode.auto_spawn_workerpals, true),
1518
+ maxWorkerpals: Math.max(
1519
+ 1,
1520
+ asInt(remoteNode.max_workerpals, 20),
1521
+ ),
1522
+ workerpalStartupTimeoutMs: Math.max(
1523
+ 1_000,
1524
+ asInt(
1525
+ parseIntEnv("REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS") ??
1526
+ remoteNode.workerpal_startup_timeout_ms,
1527
+ 10_000,
1528
+ ),
1529
+ ),
1530
+ workerpalDocker:
1531
+ parseBoolEnv("REMOTEBUDDY_WORKERPAL_DOCKER") ??
1532
+ asBoolean(remoteNode.workerpal_docker, true),
1533
+ workerpalRequireDocker:
1534
+ parseBoolEnv("REMOTEBUDDY_WORKERPAL_REQUIRE_DOCKER") ??
1535
+ asBoolean(remoteNode.workerpal_require_docker, true),
1536
+ workerpalImage:
1537
+ firstNonEmpty(
1538
+ process.env.REMOTEBUDDY_WORKERPAL_IMAGE,
1539
+ asString(remoteNode.workerpal_image, ""),
1540
+ ) || null,
1541
+ workerpalPollMs:
1542
+ asIntOrNull(parseIntEnv("REMOTEBUDDY_WORKERPAL_POLL_MS")) ??
1543
+ asIntOrNull(remoteNode.workerpal_poll_ms),
1544
+ workerpalHeartbeatMs:
1545
+ asIntOrNull(parseIntEnv("REMOTEBUDDY_WORKERPAL_HEARTBEAT_MS")) ??
1546
+ asIntOrNull(remoteNode.workerpal_heartbeat_ms),
1547
+ workerpalLabels: firstNonEmpty(process.env.REMOTEBUDDY_WORKERPAL_LABELS)
1548
+ ? firstNonEmpty(process.env.REMOTEBUDDY_WORKERPAL_LABELS)
1549
+ .split(",")
1550
+ .map((value) => value.trim())
1551
+ .filter(Boolean)
1552
+ : asStringArray(remoteNode.workerpal_labels),
1553
+ executionBudgetInteractiveMs: Math.max(
1554
+ 60_000,
1555
+ asInt(
1556
+ parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_INTERACTIVE_MS") ??
1557
+ remoteNode.execution_budget_interactive_ms,
1558
+ 300_000,
1559
+ ),
1560
+ ),
1561
+ executionBudgetNormalMs: Math.max(
1562
+ 120_000,
1563
+ asInt(
1564
+ parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_NORMAL_MS") ??
1565
+ remoteNode.execution_budget_normal_ms,
1566
+ 900_000,
1567
+ ),
1568
+ ),
1569
+ executionBudgetBackgroundMs: Math.max(
1570
+ 180_000,
1571
+ asInt(
1572
+ parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_BACKGROUND_MS") ??
1573
+ remoteNode.execution_budget_background_ms,
1574
+ 1_800_000,
1575
+ ),
1576
+ ),
1577
+ finalizationBudgetMs: Math.max(
1578
+ 30_000,
1579
+ asInt(
1580
+ parseIntEnv("REMOTEBUDDY_FINALIZATION_BUDGET_MS") ?? remoteNode.finalization_budget_ms,
1581
+ 120_000,
1582
+ ),
1583
+ ),
1584
+ crashRestartEnabled:
1585
+ parseBoolEnv("REMOTEBUDDY_CRASH_RESTART_ENABLED") ??
1586
+ asBoolean(remoteNode.crash_restart_enabled, true),
1587
+ crashRestartMaxRestarts: Math.max(
1588
+ 0,
1589
+ asInt(
1590
+ parseIntEnv("REMOTEBUDDY_CRASH_RESTART_MAX_RESTARTS") ??
1591
+ remoteNode.crash_restart_max_restarts,
1592
+ 3,
1593
+ ),
1594
+ ),
1595
+ crashRestartBackoffMs: Math.max(
1596
+ 0,
1597
+ asInt(
1598
+ parseIntEnv("REMOTEBUDDY_CRASH_RESTART_BACKOFF_MS") ??
1599
+ remoteNode.crash_restart_backoff_ms,
1600
+ 3_000,
1601
+ ),
1602
+ ),
1603
+ memory: {
1604
+ enabled: remoteMemoryEnabled,
1605
+ includeCrossSession: remoteMemoryIncludeCrossSession,
1606
+ maxRecallItems: remoteMemoryMaxRecallItems,
1607
+ maxRecallChars: remoteMemoryMaxRecallChars,
1608
+ maxSummaryChars: remoteMemoryMaxSummaryChars,
1609
+ retentionDays: remoteMemoryRetentionDays,
1610
+ },
1611
+ autonomy: {
1612
+ enabled:
1613
+ parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ??
1614
+ asBoolean(remoteAutonomyNode.enabled, true),
1615
+ killSwitchEnabled:
1616
+ parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ??
1617
+ asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
1618
+ tickIntervalMs: Math.max(
1619
+ 5_000,
1620
+ asInt(
1621
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ??
1622
+ remoteAutonomyNode.tick_interval_ms,
1623
+ 120_000,
1624
+ ),
1625
+ ),
1626
+ heartbeatLogMs: Math.max(
1627
+ 1_000,
1628
+ asInt(
1629
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ??
1630
+ remoteAutonomyNode.heartbeat_log_ms,
1631
+ 30_000,
1632
+ ),
1633
+ ),
1634
+ visionContextMaxChars: Math.max(
1635
+ 1_000,
1636
+ Math.min(
1637
+ 1_000_000,
1638
+ asInt(
1639
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ??
1640
+ remoteAutonomyNode.vision_context_max_chars,
1641
+ 65_536,
1642
+ ),
1643
+ ),
1644
+ ),
1645
+ ideationBudgetMs: Math.max(
1646
+ 1_000,
1647
+ asInt(
1648
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ??
1649
+ remoteAutonomyNode.ideation_budget_ms,
1650
+ 20_000,
1651
+ ),
1652
+ ),
1653
+ llmTimeoutMs: Math.max(
1654
+ 1_000,
1655
+ asInt(
1656
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_LLM_TIMEOUT_MS") ??
1657
+ remoteAutonomyNode.llm_timeout_ms,
1658
+ 12_000,
1659
+ ),
1660
+ ),
1661
+ allowDirtyWorktree:
1662
+ parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_DIRTY_WORKTREE") ??
1663
+ asBoolean(remoteAutonomyNode.allow_dirty_worktree, false),
1664
+ ideationMaxCandidates: Math.max(
1665
+ 1,
1666
+ Math.min(
1667
+ 100,
1668
+ asInt(
1669
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_MAX_CANDIDATES") ??
1670
+ remoteAutonomyNode.ideation_max_candidates,
1671
+ 20,
1672
+ ),
1673
+ ),
1674
+ ),
1675
+ topK: Math.max(
1676
+ 1,
1677
+ Math.min(
1678
+ 20,
1679
+ asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TOP_K") ?? remoteAutonomyNode.top_k, 3),
1680
+ ),
1681
+ ),
1682
+ exploreRate: Math.max(
1683
+ 0,
1684
+ Math.min(
1685
+ 1,
1686
+ (() => {
1687
+ const parsed = Number.parseFloat(
1688
+ String(
1689
+ firstNonEmpty(
1690
+ process.env.REMOTEBUDDY_AUTONOMY_EXPLORE_RATE,
1691
+ asString(remoteAutonomyNode.explore_rate, "0.3"),
1692
+ "0.3",
1693
+ ),
1694
+ ),
1695
+ );
1696
+ return Number.isFinite(parsed) ? parsed : 0.3;
1697
+ })(),
1698
+ ),
1699
+ ),
1700
+ minConfidence: Math.max(
1701
+ 0,
1702
+ Math.min(
1703
+ 1,
1704
+ (() => {
1705
+ const parsed = Number.parseFloat(
1706
+ String(
1707
+ firstNonEmpty(
1708
+ process.env.REMOTEBUDDY_AUTONOMY_MIN_CONFIDENCE,
1709
+ asString(remoteAutonomyNode.min_confidence, "0.65"),
1710
+ "0.65",
1711
+ ),
1712
+ ),
1713
+ );
1714
+ return Number.isFinite(parsed) ? parsed : 0.65;
1715
+ })(),
1716
+ ),
1717
+ ),
1718
+ maxConcurrentObjectives: Math.max(
1719
+ 1,
1720
+ asInt(
1721
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_CONCURRENT_OBJECTIVES") ??
1722
+ remoteAutonomyNode.max_concurrent_objectives,
1723
+ 2,
1724
+ ),
1725
+ ),
1726
+ maxDispatchPerHour: Math.max(
1727
+ 1,
1728
+ asInt(
1729
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_DISPATCH_PER_HOUR") ??
1730
+ remoteAutonomyNode.max_dispatch_per_hour,
1731
+ 6,
1732
+ ),
1733
+ ),
1734
+ maxDispatchPerHourByType: remoteAutonomyDispatchByType,
1735
+ maxDispatchPerHourByComponent: remoteAutonomyDispatchByComponent,
1736
+ maxTokenUsagePerHour: Math.max(
1737
+ 0,
1738
+ asInt(
1739
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_TOKEN_USAGE_PER_HOUR") ??
1740
+ remoteAutonomyNode.max_token_usage_per_hour,
1741
+ 120_000,
1742
+ ),
1743
+ ),
1744
+ maxRuntimeMsPerHour: Math.max(
1745
+ 0,
1746
+ asInt(
1747
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_MAX_RUNTIME_MS_PER_HOUR") ??
1748
+ remoteAutonomyNode.max_runtime_ms_per_hour,
1749
+ 5_400_000,
1750
+ ),
1751
+ ),
1752
+ cooldownFailStreakThreshold: Math.max(
1753
+ 1,
1754
+ asInt(
1755
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_COOLDOWN_FAIL_STREAK_THRESHOLD") ??
1756
+ remoteAutonomyNode.cooldown_fail_streak_threshold,
1757
+ 2,
1758
+ ),
1759
+ ),
1760
+ cooldownMs: Math.max(
1761
+ 1_000,
1762
+ asInt(
1763
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_COOLDOWN_MS") ?? remoteAutonomyNode.cooldown_ms,
1764
+ 1_800_000,
1765
+ ),
1766
+ ),
1767
+ staleObjectiveTtlMs: Math.max(
1768
+ 60_000,
1769
+ asInt(
1770
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_STALE_OBJECTIVE_TTL_MS") ??
1771
+ remoteAutonomyNode.stale_objective_ttl_ms,
1772
+ 2_700_000,
1773
+ ),
1774
+ ),
1775
+ staleObjectiveSweepIntervalMs: Math.max(
1776
+ 5_000,
1777
+ asInt(
1778
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_STALE_OBJECTIVE_SWEEP_INTERVAL_MS") ??
1779
+ remoteAutonomyNode.stale_objective_sweep_interval_ms,
1780
+ 60_000,
1781
+ ),
1782
+ ),
1783
+ autoFreezeFailStreakThreshold: Math.max(
1784
+ 1,
1785
+ asInt(
1786
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_AUTO_FREEZE_FAIL_STREAK_THRESHOLD") ??
1787
+ remoteAutonomyNode.auto_freeze_fail_streak_threshold,
1788
+ 3,
1789
+ ),
1790
+ ),
1791
+ autoFreezeDurationMs: Math.max(
1792
+ 60_000,
1793
+ asInt(
1794
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_AUTO_FREEZE_DURATION_MS") ??
1795
+ remoteAutonomyNode.auto_freeze_duration_ms,
1796
+ 1_800_000,
1797
+ ),
1798
+ ),
1799
+ evaluatorWindowHours: Math.max(
1800
+ 1,
1801
+ Math.min(
1802
+ 168,
1803
+ asInt(
1804
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_WINDOW_HOURS") ??
1805
+ remoteAutonomyNode.evaluator_window_hours,
1806
+ 24,
1807
+ ),
1808
+ ),
1809
+ ),
1810
+ evaluatorMinSamples: Math.max(
1811
+ 1,
1812
+ asInt(
1813
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_MIN_SAMPLES") ??
1814
+ remoteAutonomyNode.evaluator_min_samples,
1815
+ 6,
1816
+ ),
1817
+ ),
1818
+ evaluatorMinSuccessRate: Math.max(
1819
+ 0,
1820
+ Math.min(
1821
+ 1,
1822
+ (() => {
1823
+ const parsed = Number.parseFloat(
1824
+ String(
1825
+ firstNonEmpty(
1826
+ process.env.REMOTEBUDDY_AUTONOMY_EVALUATOR_MIN_SUCCESS_RATE,
1827
+ asString(remoteAutonomyNode.evaluator_min_success_rate, "0.45"),
1828
+ "0.45",
1829
+ ),
1830
+ ),
1831
+ );
1832
+ return Number.isFinite(parsed) ? parsed : 0.45;
1833
+ })(),
1834
+ ),
1835
+ ),
1836
+ evaluatorMaxRegretRate: Math.max(
1837
+ 0,
1838
+ Math.min(
1839
+ 1,
1840
+ (() => {
1841
+ const parsed = Number.parseFloat(
1842
+ String(
1843
+ firstNonEmpty(
1844
+ process.env.REMOTEBUDDY_AUTONOMY_EVALUATOR_MAX_REGRET_RATE,
1845
+ asString(remoteAutonomyNode.evaluator_max_regret_rate, "0.35"),
1846
+ "0.35",
1847
+ ),
1848
+ ),
1849
+ );
1850
+ return Number.isFinite(parsed) ? parsed : 0.35;
1851
+ })(),
1852
+ ),
1853
+ ),
1854
+ evaluatorRunIntervalMs: Math.max(
1855
+ 10_000,
1856
+ asInt(
1857
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_EVALUATOR_RUN_INTERVAL_MS") ??
1858
+ remoteAutonomyNode.evaluator_run_interval_ms,
1859
+ 120_000,
1860
+ ),
1861
+ ),
1862
+ alertQueuePendingThreshold: Math.max(
1863
+ 1,
1864
+ asInt(
1865
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_ALERT_QUEUE_PENDING_THRESHOLD") ??
1866
+ remoteAutonomyNode.alert_queue_pending_threshold,
1867
+ 20,
1868
+ ),
1869
+ ),
1870
+ alertJobFailureRateThreshold: Math.max(
1871
+ 0,
1872
+ Math.min(
1873
+ 1,
1874
+ (() => {
1875
+ const parsed = Number.parseFloat(
1876
+ String(
1877
+ firstNonEmpty(
1878
+ process.env.REMOTEBUDDY_AUTONOMY_ALERT_JOB_FAILURE_RATE_THRESHOLD,
1879
+ asString(remoteAutonomyNode.alert_job_failure_rate_threshold, "0.3"),
1880
+ "0.3",
1881
+ ),
1882
+ ),
1883
+ );
1884
+ return Number.isFinite(parsed) ? parsed : 0.3;
1885
+ })(),
1886
+ ),
1887
+ ),
1888
+ alertAutonomyFailureRateThreshold: Math.max(
1889
+ 0,
1890
+ Math.min(
1891
+ 1,
1892
+ (() => {
1893
+ const parsed = Number.parseFloat(
1894
+ String(
1895
+ firstNonEmpty(
1896
+ process.env.REMOTEBUDDY_AUTONOMY_ALERT_AUTONOMY_FAILURE_RATE_THRESHOLD,
1897
+ asString(remoteAutonomyNode.alert_autonomy_failure_rate_threshold, "0.45"),
1898
+ "0.45",
1899
+ ),
1900
+ ),
1901
+ );
1902
+ return Number.isFinite(parsed) ? parsed : 0.45;
1903
+ })(),
1904
+ ),
1905
+ ),
1906
+ allowReadAnywhere:
1907
+ parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ??
1908
+ asBoolean(remoteAutonomyNode.allow_read_anywhere, false),
1909
+ prFeedbackCommentRows: Math.max(
1910
+ 1,
1911
+ Math.min(
1912
+ 200,
1913
+ asInt(
1914
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_ROWS") ??
1915
+ remoteAutonomyNode.pr_feedback_comment_rows,
1916
+ 16,
1917
+ ),
1918
+ ),
1919
+ ),
1920
+ prFeedbackCommentChars: Math.max(
1921
+ 32,
1922
+ Math.min(
1923
+ 20_000,
1924
+ asInt(
1925
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_CHARS") ??
1926
+ remoteAutonomyNode.pr_feedback_comment_chars,
1927
+ 600,
1928
+ ),
1929
+ ),
1930
+ ),
1931
+ prFeedbackSummaryChars: Math.max(
1932
+ 32,
1933
+ Math.min(
1934
+ 20_000,
1935
+ asInt(
1936
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_SUMMARY_CHARS") ??
1937
+ remoteAutonomyNode.pr_feedback_summary_chars,
1938
+ 600,
1939
+ ),
1940
+ ),
1941
+ ),
1942
+ questionTtlMs: Math.max(
1943
+ 60_000,
1944
+ asInt(
1945
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_QUESTION_TTL_MS") ??
1946
+ remoteAutonomyNode.question_ttl_ms,
1947
+ 259_200_000,
1948
+ ),
1949
+ ),
1950
+ policyVersion: firstNonEmpty(
1951
+ process.env.REMOTEBUDDY_AUTONOMY_POLICY_VERSION,
1952
+ asString(remoteAutonomyNode.policy_version, "policy-v3.3"),
1953
+ "policy-v3.3",
1954
+ ),
1955
+ impactModelVersion: firstNonEmpty(
1956
+ process.env.REMOTEBUDDY_AUTONOMY_IMPACT_MODEL_VERSION,
1957
+ asString(remoteAutonomyNode.impact_model_version, "impact-v1"),
1958
+ "impact-v1",
1959
+ ),
1960
+ replay: {
1961
+ storePromptPayloads:
1962
+ parseBoolEnv("REMOTEBUDDY_AUTONOMY_REPLAY_STORE_PROMPT_PAYLOADS") ??
1963
+ asBoolean(remoteAutonomyReplayNode.store_prompt_payloads, false),
1964
+ maxRunsWithPayloads: Math.max(
1965
+ 0,
1966
+ asInt(
1967
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_REPLAY_MAX_RUNS_WITH_PAYLOADS") ??
1968
+ remoteAutonomyReplayNode.max_runs_with_payloads,
1969
+ 50,
1970
+ ),
1971
+ ),
1972
+ maxPayloadBytes: Math.max(
1973
+ 1024,
1974
+ asInt(
1975
+ parseIntEnv("REMOTEBUDDY_AUTONOMY_REPLAY_MAX_PAYLOAD_BYTES") ??
1976
+ remoteAutonomyReplayNode.max_payload_bytes,
1977
+ 262_144,
1978
+ ),
1979
+ ),
1980
+ },
1981
+ },
1982
+ llm: remoteLlm,
1983
+ },
1984
+ workerpals: {
1985
+ pollMs: workerPollMs,
1986
+ heartbeatMs: workerHeartbeatMs,
1987
+ executor: workerExecutor,
1988
+ openhandsPython: workerOpenHandsPython,
1989
+ openhandsTimeoutMs: workerOpenHandsTimeoutMs,
1990
+ miniswePython: workerMiniswePython,
1991
+ minisweTimeoutMs: workerMinisweTimeoutMs,
1992
+ openaiCodexPython: workerOpenAICodexPython,
1993
+ openaiCodexTimeoutMs: workerOpenAICodexTimeoutMs,
1994
+ openhandsStuckGuardEnabled: workerOpenHandsStuckGuardEnabled,
1995
+ openhandsStuckGuardExploreLimit: workerOpenHandsStuckGuardExploreLimit,
1996
+ openhandsStuckGuardMinElapsedMs: workerOpenHandsStuckGuardMinElapsedMs,
1997
+ openhandsStuckGuardBroadScanLimit: workerOpenHandsStuckGuardBroadScanLimit,
1998
+ openhandsStuckGuardNoProgressMaxMs: workerOpenHandsStuckGuardNoProgressMaxMs,
1999
+ openhandsAutoSteerEnabled: workerOpenHandsAutoSteerEnabled,
2000
+ openhandsAutoSteerInitialDelaySec: workerOpenHandsAutoSteerInitialDelaySec,
2001
+ openhandsAutoSteerIntervalSec: workerOpenHandsAutoSteerIntervalSec,
2002
+ openhandsAutoSteerMaxNudges: workerOpenHandsAutoSteerMaxNudges,
2003
+ requirePush: workerRequirePush,
2004
+ pushAgentBranch: workerPushAgentBranch,
2005
+ requireDocker:
2006
+ parseBoolEnv("WORKERPALS_REQUIRE_DOCKER") ?? asBoolean(workerNode.require_docker, false),
2007
+ skipDockerSelfCheck: workerSkipDockerSelfCheck,
2008
+ dockerImage: firstNonEmpty(
2009
+ process.env.WORKERPALS_DOCKER_IMAGE,
2010
+ asString(workerNode.docker_image, "pushpals-worker-sandbox:latest"),
2011
+ "pushpals-worker-sandbox:latest",
2012
+ ),
2013
+ dockerTimeoutMs: Math.max(
2014
+ 10_000,
2015
+ asInt(
2016
+ parseIntEnv("WORKERPALS_DOCKER_TIMEOUT_MS") ?? workerNode.docker_timeout_ms,
2017
+ 7_260_000,
2018
+ ),
2019
+ ),
2020
+ dockerIdleTimeoutMs: Math.max(
2021
+ 0,
2022
+ asInt(
2023
+ parseIntEnv("WORKERPALS_DOCKER_IDLE_TIMEOUT_MS") ?? workerNode.docker_idle_timeout_ms,
2024
+ 600_000,
2025
+ ),
2026
+ ),
2027
+ dockerAgentStartupTimeoutMs: workerDockerAgentStartupTimeoutMs,
2028
+ dockerWarmMaxAttempts: workerDockerWarmMaxAttempts,
2029
+ dockerWarmRetryBackoffMs: workerDockerWarmRetryBackoffMs,
2030
+ dockerJobMaxAttempts: workerDockerJobMaxAttempts,
2031
+ dockerJobRetryBackoffMs: workerDockerJobRetryBackoffMs,
2032
+ dockerWarmMemoryMb: workerDockerWarmMemoryMb,
2033
+ dockerWarmCpus: workerDockerWarmCpus,
2034
+ fileModifyingJobs: workerFileModifyingJobs,
2035
+ outputMaxChars: workerOutputMaxChars,
2036
+ outputMaxLines: workerOutputMaxLines,
2037
+ outputMaxHeadLines: workerOutputMaxHeadLines,
2038
+ qualityMaxAutoRevisions: workerQualityMaxAutoRevisions,
2039
+ qualityValidationStepTimeoutMs: workerQualityValidationStepTimeoutMs,
2040
+ qualityCriticTimeoutMs: workerQualityCriticTimeoutMs,
2041
+ qualitySoftPassOnExhausted: workerQualitySoftPassOnExhausted,
2042
+ qualityCriticMinScore: workerQualityCriticMinScore,
2043
+ qualityCriticMaxDiffChars: workerQualityCriticMaxDiffChars,
2044
+ qualityCriticMaxValidationOutputChars: workerQualityCriticMaxValidationOutputChars,
2045
+ executorResultPrefix: workerExecutorResultPrefix,
2046
+ dockerNetworkMode: asString(
2047
+ process.env.WORKERPALS_DOCKER_NETWORK_MODE ?? workerNode.docker_network_mode,
2048
+ "bridge",
2049
+ ),
2050
+ baseRef: firstNonEmpty(
2051
+ process.env.WORKERPALS_BASE_REF,
2052
+ asString(workerNode.base_ref, "origin/main_agents"),
2053
+ "origin/main_agents",
2054
+ ),
2055
+ labels: firstNonEmpty(process.env.WORKERPALS_LABELS)
2056
+ ? firstNonEmpty(process.env.WORKERPALS_LABELS)
2057
+ .split(",")
2058
+ .map((value) => value.trim())
2059
+ .filter(Boolean)
2060
+ : asStringArray(workerNode.labels),
2061
+ failureCooldownMs: Math.max(
2062
+ 0,
2063
+ asInt(
2064
+ parseIntEnv("WORKERPALS_FAILURE_COOLDOWN_MS") ??
2065
+ parseIntEnv("WORKERPALS_DOCKER_FAILURE_COOLDOWN_MS") ??
2066
+ workerNode.failure_cooldown_ms,
2067
+ 20_000,
2068
+ ),
2069
+ ),
2070
+ llm: workerLlm,
2071
+ },
2072
+ sourceControlManager: {
2073
+ repoPath: scmRepoPath,
2074
+ remote: scmRemote,
2075
+ mainBranch: scmMainBranch,
2076
+ baseBranch: scmBaseBranch,
2077
+ branchPrefix: scmBranchPrefix,
2078
+ pollIntervalSeconds: scmPollIntervalSeconds,
2079
+ checks: scmChecks,
2080
+ stateDir: scmStateDir,
2081
+ port: scmPort,
2082
+ deleteAfterMerge: scmDeleteAfterMerge,
2083
+ maxAttempts: scmMaxAttempts,
2084
+ mergeStrategy: scmMergeStrategy,
2085
+ pushMainAfterMerge: scmPushMainAfterMerge,
2086
+ openPrAfterPush: scmOpenPrAfterPush,
2087
+ prBaseBranch: scmPrBaseBranch,
2088
+ prTitle: scmPrTitle || null,
2089
+ prBody: scmPrBody || null,
2090
+ prDraft: scmPrDraft,
2091
+ statusHeartbeatMs: scmStatusHeartbeatMs,
2092
+ skipCleanCheck: scmSkipCleanCheck,
2093
+ autoCreateMainBranch: scmAutoCreateMainBranch,
2094
+ reviewAgent: {
2095
+ enabled: scmReviewAgentEnabled,
2096
+ pollIntervalMs: scmReviewAgentPollIntervalMs,
2097
+ reviewerMdPath: scmReviewAgentReviewerMdPath,
2098
+ passThreshold: scmReviewAgentPassThreshold,
2099
+ maxPrCommentsBeforeGiveUp: scmReviewAgentMaxPrCommentsBeforeGiveUp,
2100
+ mergeMethod: scmReviewAgentMergeMethod,
2101
+ codexBin: scmReviewAgentCodexBin,
2102
+ codexAuthMode: scmReviewAgentCodexAuthMode,
2103
+ codexHomeDir: scmReviewAgentCodexHomeDir,
2104
+ codexTimeoutMs: scmReviewAgentCodexTimeoutMs,
2105
+ },
2106
+ },
2107
+ startup: {
2108
+ workerImageRebuild: startupWorkerImageRebuild,
2109
+ logConfigOnStart: startupLogConfigOnStart,
2110
+ syncIntegrationWithMain: startupSyncIntegrationWithMain,
2111
+ skipLlmPreflight: startupSkipLlmPreflight,
2112
+ autoStartLmStudio: startupAutoStartLmStudio,
2113
+ lmStudioReadyTimeoutMs: startupLmStudioReadyTimeoutMs,
2114
+ lmStudioCli: startupLmStudioCli,
2115
+ lmStudioPort: startupLmStudioPort,
2116
+ lmStudioStartArgs: startupLmStudioStartArgs,
2117
+ startupWarmup,
2118
+ startupWarmupTimeoutMs: startupWarmupTimeoutMs,
2119
+ startupWarmupPollMs: startupWarmupPollMs,
2120
+ allowExternalClean: startupAllowExternalClean,
2121
+ portPreflight: startupPortPreflight,
2122
+ portConflictPolicy: startupPortConflictPolicy,
2123
+ },
2124
+ client: {
2125
+ localAgentUrl: normalizeLoopbackHttpUrl(
2126
+ firstNonEmpty(
2127
+ process.env.EXPO_PUBLIC_LOCAL_AGENT_URL,
2128
+ asString(clientNode.local_agent_url, `http://127.0.0.1:${localPort}`),
2129
+ `http://127.0.0.1:${localPort}`,
2130
+ ),
2131
+ localPort,
2132
+ ),
2133
+ traceTailLines: Math.max(
2134
+ 10,
2135
+ asInt(
2136
+ parseIntEnv("EXPO_PUBLIC_PUSHPALS_TRACE_TAIL_LINES") ?? clientNode.trace_tail_lines,
2137
+ 100,
2138
+ ),
2139
+ ),
2140
+ },
2141
+ };
2142
+
2143
+ cachedConfig = config;
2144
+ cachedConfigKey = cacheKey;
2145
+ return config;
2146
+ }
2147
+
2148
+ function sanitizeConfigString(value: string): string {
2149
+ let out = String(value ?? "");
2150
+ if (!out) return out;
2151
+ // redact URL userinfo credentials: https://user:pass@host -> https://***@host
2152
+ out = out.replace(/(https?:\/\/)[^@\s/]+@/gi, "$1***@");
2153
+ // redact malformed/encoded scheme userinfo from legacy rewrite bugs: https%3A//user%3Apass@host
2154
+ out = out.replace(/https%3a\/\/[^@\s/]+@/gi, "https%3A//***@");
2155
+ // redact bearer tokens
2156
+ out = out.replace(/\b(Bearer\s+)[A-Za-z0-9._\-:+/=]+\b/gi, "$1***");
2157
+ // redact common token formats
2158
+ out = out.replace(/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, "gh***");
2159
+ out = out.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "github_pat_***");
2160
+ out = out.replace(/\bglpat-[A-Za-z0-9\-_]{20,}\b/gi, "glpat-***");
2161
+ out = out.replace(/\bsk-[A-Za-z0-9]{20,}\b/g, "sk-***");
2162
+ return out;
2163
+ }
2164
+
2165
+ function sanitizeConfigValueForLogging(
2166
+ value: unknown,
2167
+ parentKey = "",
2168
+ ): unknown {
2169
+ if (
2170
+ typeof value === "string" ||
2171
+ typeof value === "number" ||
2172
+ typeof value === "boolean" ||
2173
+ value == null
2174
+ ) {
2175
+ if (typeof value === "string") {
2176
+ if (SENSITIVE_CONFIG_KEY_PATTERN.test(parentKey)) {
2177
+ return value.trim() ? REDACTED_LOG_VALUE : "";
2178
+ }
2179
+ return sanitizeConfigString(value);
2180
+ }
2181
+ return value;
2182
+ }
2183
+
2184
+ if (Array.isArray(value)) {
2185
+ return value.map((entry) => sanitizeConfigValueForLogging(entry, parentKey));
2186
+ }
2187
+
2188
+ if (typeof value === "object") {
2189
+ const out: Record<string, unknown> = {};
2190
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
2191
+ out[key] = sanitizeConfigValueForLogging(entry, key);
2192
+ }
2193
+ return out;
2194
+ }
2195
+
2196
+ return String(value);
2197
+ }
2198
+
2199
+ export function sanitizePushPalsConfigForLogging<T>(value: T): T {
2200
+ return sanitizeConfigValueForLogging(value) as T;
2201
+ }