@pushpalsdev/cli 1.0.17 → 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.
- package/dist/pushpals-cli.js +542 -23
- package/package.json +1 -1
- package/runtime/sandbox/apps/workerpals/.python-version +1 -0
- package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
- package/runtime/sandbox/apps/workerpals/package.json +25 -0
- package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
- package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +111 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
- package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
- package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
- package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
- package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
- package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
- package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
- package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
- package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
- package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
- package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
- package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
- package/runtime/sandbox/bun.lock +2591 -0
- package/runtime/sandbox/configs/backend.toml +79 -0
- package/runtime/sandbox/configs/default.toml +260 -0
- package/runtime/sandbox/configs/dev.toml +2 -0
- package/runtime/sandbox/configs/local.example.toml +129 -0
- package/runtime/sandbox/package.json +65 -0
- package/runtime/sandbox/packages/protocol/README.md +168 -0
- package/runtime/sandbox/packages/protocol/package.json +37 -0
- package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
- package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
- package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
- package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
- package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
- package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
- package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
- package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
- package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
- package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
- package/runtime/sandbox/packages/shared/package.json +19 -0
- package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
- package/runtime/sandbox/packages/shared/src/client_preflight.ts +297 -0
- package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
- package/runtime/sandbox/packages/shared/src/config.ts +2201 -0
- package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
- package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
- package/runtime/sandbox/packages/shared/src/index.ts +100 -0
- package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
- package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +329 -0
- package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
- package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
- package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
- package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
- package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
- package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
- package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
- package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
- package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
- package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
- package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_HERE = Path(__file__).resolve().parent
|
|
6
|
+
if str(_HERE) not in sys.path:
|
|
7
|
+
sys.path.insert(0, str(_HERE))
|
|
8
|
+
|
|
9
|
+
from executor_base import SettingsResolver, build_settings_resolver
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SettingsResolverTests(unittest.TestCase):
|
|
13
|
+
def test_get_str_prefers_env_then_config(self) -> None:
|
|
14
|
+
resolver = SettingsResolver(
|
|
15
|
+
env={"A": " env-value "},
|
|
16
|
+
config_loader=lambda: {"root": {"value": "toml-value"}},
|
|
17
|
+
)
|
|
18
|
+
value = resolver.get_str(
|
|
19
|
+
env_names=("A",),
|
|
20
|
+
config_paths=("root.value",),
|
|
21
|
+
default="fallback",
|
|
22
|
+
)
|
|
23
|
+
self.assertEqual(value, "env-value")
|
|
24
|
+
|
|
25
|
+
def test_get_str_uses_first_present_config_path(self) -> None:
|
|
26
|
+
resolver = SettingsResolver(
|
|
27
|
+
env={},
|
|
28
|
+
config_loader=lambda: {"root": {"secondary": "value-2"}},
|
|
29
|
+
)
|
|
30
|
+
value = resolver.get_str(
|
|
31
|
+
config_paths=("root.primary", "root.secondary"),
|
|
32
|
+
default="fallback",
|
|
33
|
+
)
|
|
34
|
+
self.assertEqual(value, "value-2")
|
|
35
|
+
|
|
36
|
+
def test_numeric_and_boolean_parsing(self) -> None:
|
|
37
|
+
resolver = SettingsResolver(
|
|
38
|
+
env={"INT_ENV": "42", "BOOL_ENV": "true"},
|
|
39
|
+
config_loader=lambda: {"root": {"int": "9", "enabled": False}},
|
|
40
|
+
)
|
|
41
|
+
self.assertEqual(
|
|
42
|
+
resolver.get_int(env_names=("INT_ENV",), config_paths=("root.int",), default=0),
|
|
43
|
+
42,
|
|
44
|
+
)
|
|
45
|
+
self.assertTrue(
|
|
46
|
+
resolver.get_bool(env_names=("BOOL_ENV",), config_paths=("root.enabled",), default=False),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_build_settings_resolver_static_config(self) -> None:
|
|
50
|
+
resolver = build_settings_resolver(
|
|
51
|
+
env={"X": ""},
|
|
52
|
+
config={"root": {"flag": "on"}},
|
|
53
|
+
)
|
|
54
|
+
self.assertTrue(
|
|
55
|
+
resolver.get_bool(env_names=("X",), config_paths=("root.flag",), default=False),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
unittest.main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExecutorBackend } from "../common/types.js";
|
|
2
|
+
import type { BackendTaskExecutor } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type { BackendTaskExecutor };
|
|
5
|
+
|
|
6
|
+
const specializedTaskExecutors = new Map<ExecutorBackend, BackendTaskExecutor>();
|
|
7
|
+
|
|
8
|
+
export function registerBackendTaskExecutor(
|
|
9
|
+
backend: ExecutorBackend,
|
|
10
|
+
executor: BackendTaskExecutor,
|
|
11
|
+
): void {
|
|
12
|
+
specializedTaskExecutors.set(backend, executor);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function unregisterBackendTaskExecutor(backend: ExecutorBackend): boolean {
|
|
16
|
+
return specializedTaskExecutors.delete(backend);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getBackendTaskExecutor(backend: ExecutorBackend): BackendTaskExecutor | undefined {
|
|
20
|
+
return specializedTaskExecutors.get(backend);
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExecutorBackend, JobResult } from "../common/types.js";
|
|
2
|
+
import type { WorkerpalsRuntimeConfig } from "../common/executor_backend.js";
|
|
3
|
+
|
|
4
|
+
export interface DockerBackendRuntimeConfigType {
|
|
5
|
+
python: string;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type DockerBackendRuntimeConfig = Record<string, DockerBackendRuntimeConfigType>;
|
|
10
|
+
|
|
11
|
+
export interface DockerWarmShellResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DockerWarmStartupContext {
|
|
19
|
+
sharedVenvPython: string;
|
|
20
|
+
warmAgentPort: number;
|
|
21
|
+
startupAttempts: number;
|
|
22
|
+
sleepSeconds: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DockerWarmRuntimeContext extends DockerWarmStartupContext {
|
|
26
|
+
warmContainerName: string;
|
|
27
|
+
runWarmShell: (command: string) => Promise<DockerWarmShellResult>;
|
|
28
|
+
restartWarmContainer: () => Promise<void>;
|
|
29
|
+
collectWarmDiagnostics: () => Promise<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DockerBackendSpec {
|
|
33
|
+
name: ExecutorBackend;
|
|
34
|
+
configuredPython: (config: DockerBackendRuntimeConfig) => string;
|
|
35
|
+
timeoutMs: (config: DockerBackendRuntimeConfig) => number;
|
|
36
|
+
normalizeContainerPython: (configuredPython: string, sharedVenvPython: string) => string;
|
|
37
|
+
warmContainerStartupCommand: (context: DockerWarmStartupContext) => string;
|
|
38
|
+
warmContainerEnv: (context: DockerWarmStartupContext) => Record<string, string>;
|
|
39
|
+
ensureWarmRuntime: ((context: DockerWarmRuntimeContext) => Promise<void>) | null;
|
|
40
|
+
diagnosticChecks: (sharedVenvPython: string) => Array<{ label: string; command: string }>;
|
|
41
|
+
warmupProbeCommand: ((sharedVenvPython: string) => string) | null;
|
|
42
|
+
taskExecute: BackendTaskExecutor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BackendTaskExecutor = (
|
|
46
|
+
kind: string,
|
|
47
|
+
params: Record<string, unknown>,
|
|
48
|
+
repo: string,
|
|
49
|
+
runtimeConfig: WorkerpalsRuntimeConfig,
|
|
50
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
51
|
+
budgets?: { executionBudgetMs?: number; finalizationBudgetMs?: number },
|
|
52
|
+
) => Promise<JobResult>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { loadPushPalsConfig } from "shared";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CONFIG = loadPushPalsConfig();
|
|
4
|
+
|
|
5
|
+
export interface OutputCompactionPolicy {
|
|
6
|
+
maxOutputChars: number;
|
|
7
|
+
maxOutputLines: number;
|
|
8
|
+
maxOutputHeadLines: number;
|
|
9
|
+
executorResultPrefix: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveOutputCompactionPolicy(
|
|
13
|
+
overrides: Partial<OutputCompactionPolicy> = {},
|
|
14
|
+
): OutputCompactionPolicy {
|
|
15
|
+
const worker = DEFAULT_CONFIG.workerpals;
|
|
16
|
+
const maxOutputChars = Number(overrides.maxOutputChars ?? worker.outputMaxChars);
|
|
17
|
+
const maxOutputLines = Number(overrides.maxOutputLines ?? worker.outputMaxLines);
|
|
18
|
+
const maxOutputHeadLines = Number(overrides.maxOutputHeadLines ?? worker.outputMaxHeadLines);
|
|
19
|
+
const executorResultPrefixRaw =
|
|
20
|
+
overrides.executorResultPrefix ?? worker.executorResultPrefix;
|
|
21
|
+
const executorResultPrefix =
|
|
22
|
+
typeof executorResultPrefixRaw === "string" && executorResultPrefixRaw.length > 0
|
|
23
|
+
? executorResultPrefixRaw
|
|
24
|
+
: "__PUSHPALS_OH_RESULT__ ";
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
maxOutputChars:
|
|
28
|
+
Number.isFinite(maxOutputChars) && maxOutputChars >= 8_192
|
|
29
|
+
? Math.min(Math.floor(maxOutputChars), 4_194_304)
|
|
30
|
+
: 192 * 1024,
|
|
31
|
+
maxOutputLines:
|
|
32
|
+
Number.isFinite(maxOutputLines) && maxOutputLines >= 50
|
|
33
|
+
? Math.min(Math.floor(maxOutputLines), 20_000)
|
|
34
|
+
: 600,
|
|
35
|
+
maxOutputHeadLines:
|
|
36
|
+
Number.isFinite(maxOutputHeadLines) && maxOutputHeadLines >= 1
|
|
37
|
+
? Math.max(1, Math.min(Math.floor(maxOutputHeadLines), Math.floor(maxOutputLines) || 600))
|
|
38
|
+
: 120,
|
|
39
|
+
executorResultPrefix,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Output truncation -------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export function compactJobOutput(text: string, policyOverrides: Partial<OutputCompactionPolicy> = {}): string {
|
|
46
|
+
if (!text) return "";
|
|
47
|
+
const policy = resolveOutputCompactionPolicy(policyOverrides);
|
|
48
|
+
const maxOutputChars = policy.maxOutputChars;
|
|
49
|
+
const maxOutputLines = policy.maxOutputLines;
|
|
50
|
+
const maxOutputHeadLines = Math.min(policy.maxOutputHeadLines, maxOutputLines);
|
|
51
|
+
let compact = text;
|
|
52
|
+
const lines = compact.split(/\r?\n/);
|
|
53
|
+
if (lines.length > maxOutputLines) {
|
|
54
|
+
const headCount = Math.min(maxOutputHeadLines, maxOutputLines, lines.length);
|
|
55
|
+
const tailBudget = Math.max(0, maxOutputLines - headCount);
|
|
56
|
+
const tailCount = Math.max(0, Math.min(lines.length - headCount, tailBudget));
|
|
57
|
+
const omitted = Math.max(0, lines.length - headCount - tailCount);
|
|
58
|
+
const marker = omitted > 0 ? [`... (${omitted} lines omitted) ...`] : [];
|
|
59
|
+
const tail = tailCount > 0 ? lines.slice(lines.length - tailCount) : [];
|
|
60
|
+
compact = [...lines.slice(0, headCount), ...marker, ...tail].join("\n");
|
|
61
|
+
}
|
|
62
|
+
if (compact.length > maxOutputChars) {
|
|
63
|
+
const markerPrefix = "... (";
|
|
64
|
+
const markerSuffix = " chars omitted) ...\n";
|
|
65
|
+
const markerBudget = markerPrefix.length + markerSuffix.length + 20;
|
|
66
|
+
if (markerBudget >= maxOutputChars) {
|
|
67
|
+
compact = compact.slice(-maxOutputChars);
|
|
68
|
+
} else {
|
|
69
|
+
const keepChars = Math.max(0, maxOutputChars - markerBudget);
|
|
70
|
+
const omittedChars = Math.max(0, compact.length - keepChars);
|
|
71
|
+
const marker = `${markerPrefix}${omittedChars}${markerSuffix}`;
|
|
72
|
+
const tail = keepChars > 0 ? compact.slice(-keepChars) : "";
|
|
73
|
+
compact = `${marker}${tail}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return compact;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function truncate(s: string, policyOverrides: Partial<OutputCompactionPolicy> = {}): string {
|
|
80
|
+
return compactJobOutput(s, policyOverrides);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- Stream helper -----------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
export async function streamLines(
|
|
86
|
+
readable: ReadableStream<Uint8Array>,
|
|
87
|
+
streamName: "stdout" | "stderr",
|
|
88
|
+
onLine: (stream: "stdout" | "stderr", line: string) => void,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
const reader = readable.getReader();
|
|
92
|
+
let full = "";
|
|
93
|
+
let buffer = "";
|
|
94
|
+
|
|
95
|
+
while (true) {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (done) break;
|
|
98
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
99
|
+
full += chunk;
|
|
100
|
+
buffer += chunk;
|
|
101
|
+
|
|
102
|
+
const lines = buffer.split("\n");
|
|
103
|
+
buffer = lines.pop() ?? "";
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
const clean = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
107
|
+
onLine(streamName, clean);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (buffer.length > 0) {
|
|
112
|
+
const clean = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
113
|
+
onLine(streamName, clean);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return full;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- Structured result parsing -----------------------------------------------
|
|
120
|
+
|
|
121
|
+
export function parseStructuredResult(
|
|
122
|
+
stdout: string,
|
|
123
|
+
executorResultPrefix = resolveOutputCompactionPolicy().executorResultPrefix,
|
|
124
|
+
): Record<string, unknown> | null {
|
|
125
|
+
const lines = stdout.split(/\r?\n/);
|
|
126
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
127
|
+
const line = lines[i].trim();
|
|
128
|
+
if (!line.startsWith(executorResultPrefix)) continue;
|
|
129
|
+
const raw = line.slice(executorResultPrefix.length).trim();
|
|
130
|
+
if (!raw) continue;
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function filterResultLines(
|
|
141
|
+
stdout: string,
|
|
142
|
+
executorResultPrefix = resolveOutputCompactionPolicy().executorResultPrefix,
|
|
143
|
+
): string {
|
|
144
|
+
return stdout
|
|
145
|
+
.split(/\r?\n/)
|
|
146
|
+
.filter((line) => !line.trim().startsWith(executorResultPrefix))
|
|
147
|
+
.join("\n")
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { loadPushPalsConfig } from "shared";
|
|
2
|
+
import type { ExecutorBackend } from "./types.js";
|
|
3
|
+
import { BACKEND_EXECUTOR_SCRIPT_SEGMENTS, DEFAULT_EXECUTOR } from "../backends/backend_config.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = loadPushPalsConfig();
|
|
6
|
+
export type WorkerpalsRuntimeConfig = ReturnType<typeof loadPushPalsConfig>;
|
|
7
|
+
|
|
8
|
+
export function resolveExecutor(config: WorkerpalsRuntimeConfig = DEFAULT_CONFIG): ExecutorBackend {
|
|
9
|
+
const raw = config.workerpals.executor.trim().toLowerCase();
|
|
10
|
+
if (raw in BACKEND_EXECUTOR_SCRIPT_SEGMENTS) return raw as ExecutorBackend;
|
|
11
|
+
console.warn(
|
|
12
|
+
`[WorkerPals] Unknown workerpals.executor="${raw}", falling back to "${DEFAULT_EXECUTOR}".`,
|
|
13
|
+
);
|
|
14
|
+
return DEFAULT_EXECUTOR;
|
|
15
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for a generic Python-wrapper backend task executor.
|
|
3
|
+
*
|
|
4
|
+
* Backends that don't need specialized streaming/stuck-guard logic can use
|
|
5
|
+
* this as their taskExecute implementation. It spawns the backend's Python
|
|
6
|
+
* script, applies timeout + budget capping, and parses the structured
|
|
7
|
+
* sentinel result.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
import type { JobResult } from "./types.js";
|
|
13
|
+
import type { WorkerpalsRuntimeConfig } from "./executor_backend.js";
|
|
14
|
+
import type { BackendTaskExecutor } from "../backends/types.js";
|
|
15
|
+
import {
|
|
16
|
+
truncate,
|
|
17
|
+
parseStructuredResult,
|
|
18
|
+
filterResultLines,
|
|
19
|
+
streamLines,
|
|
20
|
+
} from "./execution_utils.js";
|
|
21
|
+
|
|
22
|
+
interface GenericPythonExecutorConfig {
|
|
23
|
+
backendName: string;
|
|
24
|
+
scriptPath: string;
|
|
25
|
+
pythonConfigKey: string;
|
|
26
|
+
timeoutConfigKey: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveRuntimeSettings(
|
|
30
|
+
config: GenericPythonExecutorConfig,
|
|
31
|
+
runtimeConfig: WorkerpalsRuntimeConfig,
|
|
32
|
+
): { pythonBin: string; timeoutMs: number } {
|
|
33
|
+
const workerCfg = runtimeConfig.workerpals as Record<string, unknown>;
|
|
34
|
+
const rawPython = String(workerCfg[config.pythonConfigKey] ?? "python");
|
|
35
|
+
const pythonBin =
|
|
36
|
+
rawPython.includes("/") || rawPython.includes("\\")
|
|
37
|
+
? resolve(runtimeConfig.projectRoot, rawPython)
|
|
38
|
+
: rawPython;
|
|
39
|
+
const rawTimeout = Number(workerCfg[config.timeoutConfigKey]);
|
|
40
|
+
const timeoutMs = Number.isFinite(rawTimeout)
|
|
41
|
+
? Math.max(10_000, Math.floor(rawTimeout))
|
|
42
|
+
: 300_000;
|
|
43
|
+
return { pythonBin, timeoutMs };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createGenericPythonExecutor(
|
|
47
|
+
config: GenericPythonExecutorConfig,
|
|
48
|
+
): BackendTaskExecutor {
|
|
49
|
+
const { backendName, scriptPath } = config;
|
|
50
|
+
const backendLabel = backendName[0].toUpperCase() + backendName.slice(1);
|
|
51
|
+
|
|
52
|
+
return async (
|
|
53
|
+
kind: string,
|
|
54
|
+
params: Record<string, unknown>,
|
|
55
|
+
repo: string,
|
|
56
|
+
runtimeConfig: WorkerpalsRuntimeConfig,
|
|
57
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
58
|
+
budgets?: { executionBudgetMs?: number; finalizationBudgetMs?: number },
|
|
59
|
+
): Promise<JobResult> => {
|
|
60
|
+
if (!existsSync(scriptPath)) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
summary: `${backendName} wrapper script not found: ${scriptPath}`,
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { pythonBin, timeoutMs: configuredTimeoutMs } = resolveRuntimeSettings(
|
|
69
|
+
config,
|
|
70
|
+
runtimeConfig,
|
|
71
|
+
);
|
|
72
|
+
const executionBudgetMs =
|
|
73
|
+
typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
|
|
74
|
+
? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
|
|
75
|
+
: null;
|
|
76
|
+
const timeoutMs =
|
|
77
|
+
executionBudgetMs != null
|
|
78
|
+
? Math.min(configuredTimeoutMs, executionBudgetMs)
|
|
79
|
+
: configuredTimeoutMs;
|
|
80
|
+
const payloadBase64 = Buffer.from(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
kind,
|
|
83
|
+
params,
|
|
84
|
+
repo,
|
|
85
|
+
}),
|
|
86
|
+
"utf-8",
|
|
87
|
+
).toString("base64");
|
|
88
|
+
const args = [pythonBin, scriptPath, payloadBase64];
|
|
89
|
+
|
|
90
|
+
onLog?.(
|
|
91
|
+
"stdout",
|
|
92
|
+
`[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms)`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const outputPolicy = {
|
|
97
|
+
maxOutputChars: runtimeConfig.workerpals.outputMaxChars,
|
|
98
|
+
maxOutputLines: runtimeConfig.workerpals.outputMaxLines,
|
|
99
|
+
maxOutputHeadLines: runtimeConfig.workerpals.outputMaxHeadLines,
|
|
100
|
+
executorResultPrefix: runtimeConfig.workerpals.executorResultPrefix,
|
|
101
|
+
};
|
|
102
|
+
const proc = Bun.spawn(args, {
|
|
103
|
+
cwd: repo,
|
|
104
|
+
stdout: "pipe",
|
|
105
|
+
stderr: "pipe",
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
PUSHPALS_REPO_PATH: repo,
|
|
109
|
+
PUSHPALS_ASSIGNED_REPO_ROOT: repo,
|
|
110
|
+
PYTHONIOENCODING: "utf-8",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
const timeoutTimer = setTimeout(() => {
|
|
116
|
+
timedOut = true;
|
|
117
|
+
onLog?.(
|
|
118
|
+
"stdout",
|
|
119
|
+
`[${backendLabel}Executor] Timeout reached after ${timeoutMs}ms; terminating process.`,
|
|
120
|
+
);
|
|
121
|
+
proc.kill();
|
|
122
|
+
}, timeoutMs);
|
|
123
|
+
|
|
124
|
+
const progressIntervalMs = 15_000;
|
|
125
|
+
const startedAt = Date.now();
|
|
126
|
+
let sawProcessOutput = false;
|
|
127
|
+
const progressTimer = setInterval(() => {
|
|
128
|
+
if (timedOut || sawProcessOutput) return;
|
|
129
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
130
|
+
onLog?.(
|
|
131
|
+
"stdout",
|
|
132
|
+
`[${backendLabel}Executor] Still running (${Math.floor(
|
|
133
|
+
elapsedMs / 1000,
|
|
134
|
+
)}s elapsed); waiting for executor output...`,
|
|
135
|
+
);
|
|
136
|
+
}, progressIntervalMs);
|
|
137
|
+
|
|
138
|
+
const onProcessLine = (stream: "stdout" | "stderr", line: string) => {
|
|
139
|
+
if (!line.trim()) return;
|
|
140
|
+
sawProcessOutput = true;
|
|
141
|
+
if (stream === "stdout" && line.startsWith(outputPolicy.executorResultPrefix)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
onLog?.(stream, line);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const [rawStdout, rawStderr, exitCode] = await Promise.all([
|
|
148
|
+
proc.stdout ? streamLines(proc.stdout, "stdout", onProcessLine) : Promise.resolve(""),
|
|
149
|
+
proc.stderr ? streamLines(proc.stderr, "stderr", onProcessLine) : Promise.resolve(""),
|
|
150
|
+
proc.exited,
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
clearTimeout(timeoutTimer);
|
|
154
|
+
clearInterval(progressTimer);
|
|
155
|
+
|
|
156
|
+
const stdout = rawStdout ?? "";
|
|
157
|
+
const stderr = rawStderr ?? "";
|
|
158
|
+
|
|
159
|
+
const parsed = parseStructuredResult(stdout, outputPolicy.executorResultPrefix);
|
|
160
|
+
const filteredStdout = filterResultLines(stdout, outputPolicy.executorResultPrefix);
|
|
161
|
+
|
|
162
|
+
if (!parsed) {
|
|
163
|
+
if (timedOut) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
summary: `${backendName} wrapper timed out after ${timeoutMs}ms for ${kind}`,
|
|
167
|
+
stdout: truncate(filteredStdout, outputPolicy),
|
|
168
|
+
stderr: truncate(stderr, outputPolicy),
|
|
169
|
+
exitCode: exitCode === 0 ? 124 : exitCode,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
summary: `${backendName} wrapper did not return a structured result for ${kind}`,
|
|
175
|
+
stdout: truncate(filteredStdout, outputPolicy),
|
|
176
|
+
stderr: truncate(stderr, outputPolicy),
|
|
177
|
+
exitCode,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: typeof parsed.ok === "boolean" ? parsed.ok : exitCode === 0,
|
|
183
|
+
summary:
|
|
184
|
+
typeof parsed.summary === "string"
|
|
185
|
+
? parsed.summary
|
|
186
|
+
: exitCode === 0
|
|
187
|
+
? `${kind} passed via ${backendName}`
|
|
188
|
+
: `${kind} failed via ${backendName} (exit ${exitCode})`,
|
|
189
|
+
stdout: truncate(
|
|
190
|
+
typeof parsed.stdout === "string" ? parsed.stdout : filteredStdout,
|
|
191
|
+
outputPolicy,
|
|
192
|
+
),
|
|
193
|
+
stderr: truncate(
|
|
194
|
+
typeof parsed.stderr === "string" ? parsed.stderr : stderr,
|
|
195
|
+
outputPolicy,
|
|
196
|
+
),
|
|
197
|
+
exitCode:
|
|
198
|
+
typeof parsed.exitCode === "number" && Number.isFinite(parsed.exitCode)
|
|
199
|
+
? parsed.exitCode
|
|
200
|
+
: exitCode,
|
|
201
|
+
};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
summary: `${backendName} wrapper execution error for ${kind}: ${String(err)}`,
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
4
|
+
debug: 10,
|
|
5
|
+
info: 20,
|
|
6
|
+
warn: 30,
|
|
7
|
+
error: 40,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function normalizeLevel(raw: string): LogLevel | null {
|
|
11
|
+
const value = raw.trim().toLowerCase();
|
|
12
|
+
if (value === "debug" || value === "info" || value === "warn" || value === "error") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveMinLevel(): LogLevel {
|
|
19
|
+
const explicit = normalizeLevel(process.env.WORKERPALS_LOG_LEVEL ?? "");
|
|
20
|
+
if (explicit) return explicit;
|
|
21
|
+
const debugFlag = (process.env.WORKERPALS_DEBUG ?? "").trim().toLowerCase();
|
|
22
|
+
return debugFlag === "1" || debugFlag === "true" || debugFlag === "yes" ? "debug" : "info";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Logger {
|
|
26
|
+
private readonly minLevel: LogLevel;
|
|
27
|
+
private readonly prefix: string;
|
|
28
|
+
|
|
29
|
+
constructor(prefix: string, minLevel: LogLevel = resolveMinLevel()) {
|
|
30
|
+
this.prefix = prefix.trim();
|
|
31
|
+
this.minLevel = minLevel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isDebugEnabled(): boolean {
|
|
35
|
+
return this.canLog("debug");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
debug(message: string): void {
|
|
39
|
+
if (!this.canLog("debug")) return;
|
|
40
|
+
console.log(this.format(message));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
info(message: string): void {
|
|
44
|
+
if (!this.canLog("info")) return;
|
|
45
|
+
console.log(this.format(message));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warn(message: string): void {
|
|
49
|
+
if (!this.canLog("warn")) return;
|
|
50
|
+
console.warn(this.format(message));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
error(message: string): void {
|
|
54
|
+
if (!this.canLog("error")) return;
|
|
55
|
+
console.error(this.format(message));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private canLog(level: LogLevel): boolean {
|
|
59
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[this.minLevel];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private format(message: string): string {
|
|
63
|
+
return this.prefix ? `[${this.prefix}] ${message}` : message;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, rmSync } from "fs";
|
|
2
|
+
|
|
3
|
+
export type WorktreeCleanupOptions = {
|
|
4
|
+
retries?: number;
|
|
5
|
+
delayMs?: number;
|
|
6
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
7
|
+
removeFn?: (targetPath: string) => void;
|
|
8
|
+
existsFn?: (targetPath: string) => boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function windowsDeletionCandidates(worktreePath: string): string[] {
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
const out: string[] = [];
|
|
18
|
+
|
|
19
|
+
const add = (value: string) => {
|
|
20
|
+
if (!value || seen.has(value)) return;
|
|
21
|
+
seen.add(value);
|
|
22
|
+
out.push(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
add(worktreePath);
|
|
26
|
+
|
|
27
|
+
if (process.platform === "win32" && /^[A-Za-z]:[\\/]/.test(worktreePath)) {
|
|
28
|
+
// Long-path literal to avoid MAX_PATH cleanup failures.
|
|
29
|
+
add(`\\\\?\\${worktreePath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function forceDeleteWorktreePath(
|
|
36
|
+
worktreePath: string,
|
|
37
|
+
options: WorktreeCleanupOptions = {},
|
|
38
|
+
): Promise<{ removed: boolean; lastError?: string }> {
|
|
39
|
+
const retries = Math.max(1, Math.floor(options.retries ?? 5));
|
|
40
|
+
const delayMs = Math.max(0, Math.floor(options.delayMs ?? 120));
|
|
41
|
+
const sleep = options.sleepFn ?? defaultSleep;
|
|
42
|
+
const removePath = options.removeFn ?? ((targetPath: string) => rmSync(targetPath, { recursive: true, force: true }));
|
|
43
|
+
const pathExists = options.existsFn ?? ((targetPath: string) => existsSync(targetPath));
|
|
44
|
+
let lastError = "";
|
|
45
|
+
|
|
46
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
47
|
+
if (!pathExists(worktreePath)) return { removed: true };
|
|
48
|
+
|
|
49
|
+
for (const candidate of windowsDeletionCandidates(worktreePath)) {
|
|
50
|
+
try {
|
|
51
|
+
removePath(candidate);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
lastError = String(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!pathExists(worktreePath)) return { removed: true };
|
|
58
|
+
if (attempt < retries) await sleep(delayMs * attempt);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
removed: !pathExists(worktreePath),
|
|
63
|
+
...(lastError ? { lastError } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|