@pushpalsdev/cli 1.0.86 → 1.0.94
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 +1 -1
- package/package.json +2 -2
- package/runtime/prompts/remotebuddy/autonomy_ideation_system_prompt.md +2 -1
- package/runtime/prompts/remotebuddy/autonomy_planning_system_prompt.md +1 -1
- package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +4 -4
- package/runtime/prompts/workerpals/miniswe_completion_requirement.md +1 -1
- package/runtime/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
- package/runtime/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
- package/runtime/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
- package/runtime/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
- package/runtime/prompts/workerpals/workerpals_system_prompt.md +2 -2
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +248 -98
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +5 -34
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +219 -130
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +57 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +3 -2
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +142 -134
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +70 -25
- package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +14 -8
- package/runtime/sandbox/packages/shared/src/communication.ts +4 -1
- package/runtime/sandbox/packages/shared/src/config.ts +1 -1
- package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -1
- package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
- package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +2 -2
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* Used by both the host Worker (direct mode) and the Docker job runner.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
|
|
6
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
|
|
7
7
|
import { resolve } from "path";
|
|
8
8
|
import {
|
|
9
|
-
deriveAutonomyComponentArea,
|
|
10
9
|
buildGitCommitArgs as buildSourceControlGitCommitArgs,
|
|
11
10
|
explicitSourceControlCommitIdentityFromEnv,
|
|
12
11
|
loadPromptTemplate,
|
|
@@ -15,12 +14,9 @@ import {
|
|
|
15
14
|
extractVisionKeyItems,
|
|
16
15
|
formatToolRequirement,
|
|
17
16
|
matchesGlob,
|
|
18
|
-
normalizeAutonomyComponentArea,
|
|
19
17
|
normalizeTargetPath,
|
|
20
18
|
requirementsForValidationCommand,
|
|
21
19
|
sanitizeSourceControlIdentityField,
|
|
22
|
-
validateScopeInvariants,
|
|
23
|
-
type AutonomyComponentArea,
|
|
24
20
|
type SourceControlCommitIdentity,
|
|
25
21
|
type ToolRequirement,
|
|
26
22
|
} from "shared";
|
|
@@ -389,13 +385,6 @@ export function shouldEnqueueNoChangeReviewCompletion(
|
|
|
389
385
|
return extractReviewFixContext(params) == null;
|
|
390
386
|
}
|
|
391
387
|
|
|
392
|
-
function reviewAgentAllowsMultiRootScope(value: unknown): boolean {
|
|
393
|
-
const normalized = String(value ?? "")
|
|
394
|
-
.trim()
|
|
395
|
-
.toLowerCase();
|
|
396
|
-
return normalized === "review_fix" || normalized === "merge_conflict";
|
|
397
|
-
}
|
|
398
|
-
|
|
399
388
|
export function deriveQualityGatePolicy(
|
|
400
389
|
params: Record<string, unknown> | null | undefined,
|
|
401
390
|
runtimeConfig: WorkerpalsRuntimeConfig = DEFAULT_CONFIG,
|
|
@@ -1656,7 +1645,7 @@ async function runDeterministicQualityGate(
|
|
|
1656
1645
|
if (scopedValidationFailure === "outside_task_scope") {
|
|
1657
1646
|
onLog?.(
|
|
1658
1647
|
"stderr",
|
|
1659
|
-
"[ValidationGate] Required validation failures appear outside the task
|
|
1648
|
+
"[ValidationGate] Required validation failures appear outside the task target/relevance hints; treating them as publish blockers, not repair instructions.",
|
|
1660
1649
|
);
|
|
1661
1650
|
}
|
|
1662
1651
|
|
|
@@ -2177,6 +2166,12 @@ export type WorkerGitCommitIdentity = SourceControlCommitIdentity;
|
|
|
2177
2166
|
|
|
2178
2167
|
export const explicitWorkerCommitIdentityFromEnv = explicitSourceControlCommitIdentityFromEnv;
|
|
2179
2168
|
|
|
2169
|
+
async function unstageSandboxArtifactPaths(
|
|
2170
|
+
repo: string,
|
|
2171
|
+
): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
2172
|
+
return git(repo, ["reset", "-q", "--", ...SANDBOX_STAGE_ARTIFACT_PATHS]);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2180
2175
|
async function resolveGitConfigValue(repo: string, key: string): Promise<string> {
|
|
2181
2176
|
const value = await git(repo, ["config", "--get", key]);
|
|
2182
2177
|
return value.ok ? sanitizeSourceControlIdentityField(value.stdout) : "";
|
|
@@ -2293,19 +2288,21 @@ export async function createJobCommit(
|
|
|
2293
2288
|
console.warn(
|
|
2294
2289
|
`[WorkerPals] Stage target invalid/missing for ${job.kind}; retrying with fallback "git add -A".`,
|
|
2295
2290
|
);
|
|
2296
|
-
result = await git(repo, [
|
|
2297
|
-
"add",
|
|
2298
|
-
"-A",
|
|
2299
|
-
"--",
|
|
2300
|
-
".",
|
|
2301
|
-
":(exclude)workspace/**",
|
|
2302
|
-
":(exclude)outputs/**",
|
|
2303
|
-
]);
|
|
2291
|
+
result = await git(repo, ["add", "-A"]);
|
|
2304
2292
|
}
|
|
2305
2293
|
if (!result.ok) {
|
|
2306
2294
|
return { ok: false, error: `Failed to stage changes: ${result.stderr || result.stdout}` };
|
|
2307
2295
|
}
|
|
2308
2296
|
}
|
|
2297
|
+
if (job.kind === "task.execute") {
|
|
2298
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
2299
|
+
if (!unstageArtifacts.ok) {
|
|
2300
|
+
return {
|
|
2301
|
+
ok: false,
|
|
2302
|
+
error: `Failed to unstage sandbox artifact paths: ${unstageArtifacts.stderr || unstageArtifacts.stdout}`,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2309
2306
|
|
|
2310
2307
|
// Check if there are changes to commit
|
|
2311
2308
|
result = await git(repo, ["diff", "--cached", "--quiet"]);
|
|
@@ -2503,12 +2500,12 @@ function buildStageTargets(kind: string, params?: Record<string, unknown>): stri
|
|
|
2503
2500
|
}
|
|
2504
2501
|
}
|
|
2505
2502
|
|
|
2506
|
-
function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
|
|
2503
|
+
export function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
|
|
2504
|
+
if (kind === "task.execute") {
|
|
2505
|
+
return ["add", "-A"];
|
|
2506
|
+
}
|
|
2507
2507
|
const targets = buildStageTargets(kind, params);
|
|
2508
2508
|
if (targets.length === 0) {
|
|
2509
|
-
if (kind === "task.execute") {
|
|
2510
|
-
return ["add", "-A", "--", ".", ":(exclude)workspace/**", ":(exclude)outputs/**"];
|
|
2511
|
-
}
|
|
2512
2509
|
return null;
|
|
2513
2510
|
}
|
|
2514
2511
|
return ["add", "-A", "--", ...targets];
|
|
@@ -3050,25 +3047,11 @@ export async function resumePreparedMergeConflictRebase(
|
|
|
3050
3047
|
"stdout",
|
|
3051
3048
|
`[MergeConflict] Stage target invalid/missing for ${kind}; retrying with fallback "git add -A".`,
|
|
3052
3049
|
);
|
|
3053
|
-
stageResult = await git(repo, [
|
|
3054
|
-
"add",
|
|
3055
|
-
"-A",
|
|
3056
|
-
"--",
|
|
3057
|
-
".",
|
|
3058
|
-
":(exclude)workspace/**",
|
|
3059
|
-
":(exclude)outputs/**",
|
|
3060
|
-
]);
|
|
3050
|
+
stageResult = await git(repo, ["add", "-A"]);
|
|
3061
3051
|
}
|
|
3062
3052
|
}
|
|
3063
3053
|
} else {
|
|
3064
|
-
stageResult = await git(repo, [
|
|
3065
|
-
"add",
|
|
3066
|
-
"-A",
|
|
3067
|
-
"--",
|
|
3068
|
-
".",
|
|
3069
|
-
":(exclude)workspace/**",
|
|
3070
|
-
":(exclude)outputs/**",
|
|
3071
|
-
]);
|
|
3054
|
+
stageResult = await git(repo, ["add", "-A"]);
|
|
3072
3055
|
}
|
|
3073
3056
|
if (!stageResult.ok) {
|
|
3074
3057
|
return {
|
|
@@ -3078,6 +3061,15 @@ export async function resumePreparedMergeConflictRebase(
|
|
|
3078
3061
|
combinedGitOutput(stageResult),
|
|
3079
3062
|
};
|
|
3080
3063
|
}
|
|
3064
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
3065
|
+
if (!unstageArtifacts.ok) {
|
|
3066
|
+
return {
|
|
3067
|
+
ok: false,
|
|
3068
|
+
error:
|
|
3069
|
+
"Failed to unstage sandbox artifact paths before continuing rebase: " +
|
|
3070
|
+
combinedGitOutput(unstageArtifacts),
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3081
3073
|
|
|
3082
3074
|
let rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
|
|
3083
3075
|
let continueOutput = combinedGitOutput(rebaseContinue);
|
|
@@ -3227,19 +3219,19 @@ async function createMergeConflictJobCommit(
|
|
|
3227
3219
|
console.warn(
|
|
3228
3220
|
`[WorkerPals] Stage target invalid/missing for merge-conflict job ${job.id}; retrying with fallback "git add -A".`,
|
|
3229
3221
|
);
|
|
3230
|
-
result = await git(repo, [
|
|
3231
|
-
"add",
|
|
3232
|
-
"-A",
|
|
3233
|
-
"--",
|
|
3234
|
-
".",
|
|
3235
|
-
":(exclude)workspace/**",
|
|
3236
|
-
":(exclude)outputs/**",
|
|
3237
|
-
]);
|
|
3222
|
+
result = await git(repo, ["add", "-A"]);
|
|
3238
3223
|
}
|
|
3239
3224
|
if (!result.ok) {
|
|
3240
3225
|
return { ok: false, error: `Failed to stage merge-conflict changes: ${result.stderr || result.stdout}` };
|
|
3241
3226
|
}
|
|
3242
3227
|
}
|
|
3228
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
3229
|
+
if (!unstageArtifacts.ok) {
|
|
3230
|
+
return {
|
|
3231
|
+
ok: false,
|
|
3232
|
+
error: `Failed to unstage sandbox artifact paths: ${unstageArtifacts.stderr || unstageArtifacts.stdout}`,
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3243
3235
|
|
|
3244
3236
|
const cachedDiffQuiet = await git(repo, ["diff", "--cached", "--quiet"]);
|
|
3245
3237
|
let headSha = await currentRefSha(repo, "HEAD");
|
|
@@ -3602,6 +3594,85 @@ export function shouldUseCodexCliForExecutor(executor: string): boolean {
|
|
|
3602
3594
|
return executor.trim().toLowerCase() === "openai_codex";
|
|
3603
3595
|
}
|
|
3604
3596
|
|
|
3597
|
+
type MaskedRepoLocalCodexFile = {
|
|
3598
|
+
codexPath: string;
|
|
3599
|
+
backupPath: string;
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
function codexProjectConfigRoots(repo: string, env: Record<string, string>): string[] {
|
|
3603
|
+
const roots: string[] = [];
|
|
3604
|
+
const seen = new Set<string>();
|
|
3605
|
+
const add = (raw: unknown) => {
|
|
3606
|
+
const text = String(raw ?? "").trim();
|
|
3607
|
+
if (!text) return;
|
|
3608
|
+
const root = resolve(text);
|
|
3609
|
+
const key = root.toLowerCase();
|
|
3610
|
+
if (seen.has(key)) return;
|
|
3611
|
+
seen.add(key);
|
|
3612
|
+
roots.push(root);
|
|
3613
|
+
};
|
|
3614
|
+
add(repo);
|
|
3615
|
+
for (const key of [
|
|
3616
|
+
"PUSHPALS_REPO_ROOT_OVERRIDE",
|
|
3617
|
+
"PUSHPALS_PROJECT_ROOT_OVERRIDE",
|
|
3618
|
+
"PUSHPALS_ASSIGNED_REPO_ROOT",
|
|
3619
|
+
"PUSHPALS_REPO_PATH",
|
|
3620
|
+
]) {
|
|
3621
|
+
add(env[key]);
|
|
3622
|
+
}
|
|
3623
|
+
return roots;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
function maskRepoLocalCodexFilesForCodexCli(
|
|
3627
|
+
repo: string,
|
|
3628
|
+
env: Record<string, string>,
|
|
3629
|
+
): MaskedRepoLocalCodexFile[] {
|
|
3630
|
+
const masked: MaskedRepoLocalCodexFile[] = [];
|
|
3631
|
+
for (const root of codexProjectConfigRoots(repo, env)) {
|
|
3632
|
+
const codexPath = resolve(root, ".codex");
|
|
3633
|
+
if (!existsSync(codexPath)) continue;
|
|
3634
|
+
try {
|
|
3635
|
+
if (lstatSync(codexPath).isDirectory()) continue;
|
|
3636
|
+
let backupPath = resolve(root, `.codex.pushpals-masked-${process.pid}-${masked.length}`);
|
|
3637
|
+
let suffix = 0;
|
|
3638
|
+
while (existsSync(backupPath)) {
|
|
3639
|
+
suffix += 1;
|
|
3640
|
+
backupPath = resolve(
|
|
3641
|
+
root,
|
|
3642
|
+
`.codex.pushpals-masked-${process.pid}-${masked.length}-${suffix}`,
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
renameSync(codexPath, backupPath);
|
|
3646
|
+
masked.push({ codexPath, backupPath });
|
|
3647
|
+
console.warn(
|
|
3648
|
+
`[WorkerPals] Temporarily masked repo-local .codex file so Codex CLI can use CODEX_HOME: ${codexPath}`,
|
|
3649
|
+
);
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
console.warn(
|
|
3652
|
+
`[WorkerPals] Failed to mask repo-local .codex file ${codexPath}: ${String(error)}`,
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
return masked;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function restoreRepoLocalCodexFilesForCodexCli(masked: MaskedRepoLocalCodexFile[]): void {
|
|
3660
|
+
for (const entry of [...masked].reverse()) {
|
|
3661
|
+
try {
|
|
3662
|
+
if (existsSync(entry.codexPath)) {
|
|
3663
|
+
rmSync(entry.codexPath, { recursive: true, force: true });
|
|
3664
|
+
}
|
|
3665
|
+
if (existsSync(entry.backupPath)) {
|
|
3666
|
+
renameSync(entry.backupPath, entry.codexPath);
|
|
3667
|
+
}
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
console.warn(
|
|
3670
|
+
`[WorkerPals] Failed to restore repo-local .codex file ${entry.codexPath}: ${String(error)}`,
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3605
3676
|
function normalizeCodexReasoningEffort(
|
|
3606
3677
|
value: unknown,
|
|
3607
3678
|
model = "",
|
|
@@ -3712,11 +3783,13 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
3712
3783
|
if (model) cmd.push("-m", model);
|
|
3713
3784
|
cmd.push("-");
|
|
3714
3785
|
|
|
3786
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
3787
|
+
const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
|
|
3715
3788
|
try {
|
|
3716
3789
|
const stdinText = `${prompt.systemPrompt}\n\n${prompt.userMessage}`;
|
|
3717
3790
|
const proc = Bun.spawn(cmd, {
|
|
3718
3791
|
cwd: repo,
|
|
3719
|
-
env
|
|
3792
|
+
env,
|
|
3720
3793
|
stdout: "pipe",
|
|
3721
3794
|
stderr: "pipe",
|
|
3722
3795
|
stdin: new Blob([stdinText]),
|
|
@@ -3751,6 +3824,7 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
3751
3824
|
} catch {
|
|
3752
3825
|
return null;
|
|
3753
3826
|
} finally {
|
|
3827
|
+
restoreRepoLocalCodexFilesForCodexCli(codexMask);
|
|
3754
3828
|
try {
|
|
3755
3829
|
unlinkSync(tmpOutputPath);
|
|
3756
3830
|
} catch {
|
|
@@ -3930,9 +4004,7 @@ function hasInvalidRepoPathHint(values: string[]): boolean {
|
|
|
3930
4004
|
return values.some((entry) => normalizeStagePath(entry) === null);
|
|
3931
4005
|
}
|
|
3932
4006
|
|
|
3933
|
-
|
|
3934
|
-
return normalizeAutonomyComponentArea(value);
|
|
3935
|
-
}
|
|
4007
|
+
const SANDBOX_STAGE_ARTIFACT_PATHS = ["workspace", "outputs", ".codex"];
|
|
3936
4008
|
|
|
3937
4009
|
function taskExecuteOrigin(params: Record<string, unknown>): "autonomy" | "user" {
|
|
3938
4010
|
const explicit = String(params.origin ?? "")
|
|
@@ -3949,33 +4021,15 @@ function taskExecuteOrigin(params: Record<string, unknown>): "autonomy" | "user"
|
|
|
3949
4021
|
return "user";
|
|
3950
4022
|
}
|
|
3951
4023
|
|
|
3952
|
-
function collectWriteScopeIssuesFromChangedPaths(
|
|
4024
|
+
export function collectWriteScopeIssuesFromChangedPaths(
|
|
3953
4025
|
changedPaths: string[],
|
|
3954
4026
|
planning: TaskExecutePlanning,
|
|
3955
4027
|
): string[] {
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
.filter((entry): entry is string => Boolean(entry) && entry !== ".");
|
|
3962
|
-
if (normalizedChangedPaths.length === 0) return [];
|
|
3963
|
-
|
|
3964
|
-
const forbidden = toStringArray(planning.scope.forbiddenGlobs ?? []);
|
|
3965
|
-
const issues: string[] = [];
|
|
3966
|
-
const outOfScope = normalizedChangedPaths.filter(
|
|
3967
|
-
(path) => !writeGlobs.some((glob) => matchesGlob(path, glob)),
|
|
3968
|
-
);
|
|
3969
|
-
if (outOfScope.length > 0) {
|
|
3970
|
-
issues.push(`modified paths outside writeGlobs: ${outOfScope.join(", ")}`);
|
|
3971
|
-
}
|
|
3972
|
-
const forbiddenTouched = normalizedChangedPaths.filter((path) =>
|
|
3973
|
-
forbidden.some((glob) => matchesGlob(path, glob)),
|
|
3974
|
-
);
|
|
3975
|
-
if (forbiddenTouched.length > 0) {
|
|
3976
|
-
issues.push(`modified paths matching forbiddenGlobs: ${forbiddenTouched.join(", ")}`);
|
|
3977
|
-
}
|
|
3978
|
-
return issues;
|
|
4028
|
+
void changedPaths;
|
|
4029
|
+
void planning;
|
|
4030
|
+
// WorkerPals run in isolated worktrees and may write anywhere in that repo sandbox.
|
|
4031
|
+
// Scope hints guide planning/review, but they are not hard write privileges.
|
|
4032
|
+
return [];
|
|
3979
4033
|
}
|
|
3980
4034
|
|
|
3981
4035
|
function sanitizeTaskExecutePlanningPathHints(value: unknown): unknown {
|
|
@@ -4097,61 +4151,6 @@ function validateTaskExecutePlanning(
|
|
|
4097
4151
|
message: "task.execute planning.targetPaths must contain literal repo-relative paths",
|
|
4098
4152
|
};
|
|
4099
4153
|
}
|
|
4100
|
-
const normalizedWriteGlobs = isStringArray(scope.writeGlobs)
|
|
4101
|
-
? toStringArray(scope.writeGlobs)
|
|
4102
|
-
: [];
|
|
4103
|
-
const allowMultiRootAutonomyScope =
|
|
4104
|
-
origin === "autonomy" &&
|
|
4105
|
-
reviewAgentAllowsMultiRootScope(options?.reviewAgentResolutionType);
|
|
4106
|
-
if (origin === "autonomy") {
|
|
4107
|
-
const declaredComponentArea = asAutonomyComponentArea(options?.autonomyComponentArea);
|
|
4108
|
-
const inferredComponentArea = allowMultiRootAutonomyScope
|
|
4109
|
-
? null
|
|
4110
|
-
: deriveAutonomyComponentArea(normalizedTargetPaths, normalizedWriteGlobs);
|
|
4111
|
-
const componentArea = allowMultiRootAutonomyScope
|
|
4112
|
-
? declaredComponentArea
|
|
4113
|
-
: declaredComponentArea ?? inferredComponentArea;
|
|
4114
|
-
if (!allowMultiRootAutonomyScope && !componentArea) {
|
|
4115
|
-
return {
|
|
4116
|
-
ok: false,
|
|
4117
|
-
message:
|
|
4118
|
-
"task.execute planning.targetPaths must resolve to a repo-relative componentArea",
|
|
4119
|
-
};
|
|
4120
|
-
}
|
|
4121
|
-
if (
|
|
4122
|
-
!allowMultiRootAutonomyScope &&
|
|
4123
|
-
declaredComponentArea &&
|
|
4124
|
-
inferredComponentArea &&
|
|
4125
|
-
declaredComponentArea !== inferredComponentArea
|
|
4126
|
-
) {
|
|
4127
|
-
return {
|
|
4128
|
-
ok: false,
|
|
4129
|
-
message: "task.execute planning.targetPaths do not match autonomy componentArea",
|
|
4130
|
-
};
|
|
4131
|
-
}
|
|
4132
|
-
const validatedScope = validateScopeInvariants(
|
|
4133
|
-
componentArea,
|
|
4134
|
-
normalizedTargetPaths,
|
|
4135
|
-
normalizedWriteGlobs,
|
|
4136
|
-
{ requireWriteGlobs: false, allowMultipleComponentRoots: allowMultiRootAutonomyScope },
|
|
4137
|
-
);
|
|
4138
|
-
if (!validatedScope.ok) {
|
|
4139
|
-
return {
|
|
4140
|
-
ok: false,
|
|
4141
|
-
message: `task.execute scope invariants failed: ${validatedScope.errors.join("; ")}`,
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
} else if (normalizedWriteGlobs.length > 0) {
|
|
4145
|
-
const uncoveredPaths = normalizedTargetPaths.filter(
|
|
4146
|
-
(targetPath) => !normalizedWriteGlobs.some((glob) => matchesGlob(targetPath, glob)),
|
|
4147
|
-
);
|
|
4148
|
-
if (uncoveredPaths.length > 0) {
|
|
4149
|
-
return {
|
|
4150
|
-
ok: false,
|
|
4151
|
-
message: `task.execute planning.targetPaths must be covered by planning.scope.writeGlobs: ${uncoveredPaths.join(", ")}`,
|
|
4152
|
-
};
|
|
4153
|
-
}
|
|
4154
|
-
}
|
|
4155
4154
|
}
|
|
4156
4155
|
|
|
4157
4156
|
if (planning.discovery !== undefined) {
|
|
@@ -4378,10 +4377,12 @@ async function runCodexCriticReview(
|
|
|
4378
4377
|
"-",
|
|
4379
4378
|
];
|
|
4380
4379
|
|
|
4380
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
4381
|
+
const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
|
|
4381
4382
|
try {
|
|
4382
4383
|
const proc = Bun.spawn(cmd, {
|
|
4383
4384
|
cwd: repo,
|
|
4384
|
-
env
|
|
4385
|
+
env,
|
|
4385
4386
|
stdout: "pipe",
|
|
4386
4387
|
stderr: "pipe",
|
|
4387
4388
|
stdin: new Blob([criticInstruction]),
|
|
@@ -4461,6 +4462,13 @@ async function runCodexCriticReview(
|
|
|
4461
4462
|
} catch (err) {
|
|
4462
4463
|
onLog?.("stderr", `[CriticGate] Codex error: ${toSingleLine(err, 220)} (skipping).`);
|
|
4463
4464
|
return null;
|
|
4465
|
+
} finally {
|
|
4466
|
+
restoreRepoLocalCodexFilesForCodexCli(codexMask);
|
|
4467
|
+
try {
|
|
4468
|
+
unlinkSync(tmpOutputPath);
|
|
4469
|
+
} catch {
|
|
4470
|
+
/* ignore */
|
|
4471
|
+
}
|
|
4464
4472
|
}
|
|
4465
4473
|
}
|
|
4466
4474
|
|
|
@@ -4775,7 +4783,7 @@ export async function executeJob(
|
|
|
4775
4783
|
[
|
|
4776
4784
|
result.stderr ?? "",
|
|
4777
4785
|
validationOutsideTaskScope
|
|
4778
|
-
? "Validation failures appear outside the task
|
|
4786
|
+
? "Validation failures appear outside the task target/relevance hints and are treated as pre-existing repo blockers."
|
|
4779
4787
|
: "",
|
|
4780
4788
|
...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
|
|
4781
4789
|
]
|
|
@@ -65,6 +65,8 @@ type WorkerJobResult = JobResult & {
|
|
|
65
65
|
|
|
66
66
|
const DEFAULT_LLM_MODEL = "local-model";
|
|
67
67
|
const CODEX_UNAVAILABLE_WORKER_EXIT_CODE = 86;
|
|
68
|
+
const CODEX_UNAVAILABLE_DOCKER_SHUTDOWN_GRACE_MS = 5_000;
|
|
69
|
+
const CODEX_UNAVAILABLE_WORKER_FORCE_EXIT_MS = 4_000;
|
|
68
70
|
const CONFIG = loadPushPalsConfig();
|
|
69
71
|
const LOG = new Logger("WorkerPals");
|
|
70
72
|
|
|
@@ -360,6 +362,36 @@ function shouldRecycleWorkerForCodexUnavailableFailure(
|
|
|
360
362
|
].some((needle) => text.includes(needle));
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
async function shutdownDockerExecutorBeforeCodexRecycle(
|
|
366
|
+
dockerExecutor: DockerExecutor | null,
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
if (!dockerExecutor) return;
|
|
369
|
+
|
|
370
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
371
|
+
let timedOut = false;
|
|
372
|
+
try {
|
|
373
|
+
await Promise.race([
|
|
374
|
+
dockerExecutor.shutdown(),
|
|
375
|
+
new Promise<void>((resolvePromise) => {
|
|
376
|
+
timeout = setTimeout(() => {
|
|
377
|
+
timedOut = true;
|
|
378
|
+
resolvePromise();
|
|
379
|
+
}, CODEX_UNAVAILABLE_DOCKER_SHUTDOWN_GRACE_MS);
|
|
380
|
+
}),
|
|
381
|
+
]);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.error(`[WorkerPals] Docker shutdown cleanup failed: ${String(err)}`);
|
|
384
|
+
} finally {
|
|
385
|
+
if (timeout) clearTimeout(timeout);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (timedOut) {
|
|
389
|
+
console.warn(
|
|
390
|
+
`[WorkerPals] Docker shutdown cleanup exceeded ${CODEX_UNAVAILABLE_DOCKER_SHUTDOWN_GRACE_MS}ms; exiting worker for Codex recycle anyway.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
363
395
|
function parseArgs(): {
|
|
364
396
|
server: string;
|
|
365
397
|
pollMs: number;
|
|
@@ -1667,21 +1699,46 @@ async function workerLoop(
|
|
|
1667
1699
|
}
|
|
1668
1700
|
} finally {
|
|
1669
1701
|
clearInterval(busyHeartbeat);
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1702
|
+
if (recycleWorkerAfterJob) {
|
|
1703
|
+
runtimeState.shutdownRequested = true;
|
|
1704
|
+
const forceExitTimer = setTimeout(() => {
|
|
1705
|
+
console.warn(
|
|
1706
|
+
`[WorkerPals] Forcing worker recycle ${CODEX_UNAVAILABLE_WORKER_FORCE_EXIT_MS}ms after Codex backend failure.`,
|
|
1707
|
+
);
|
|
1708
|
+
process.exit(CODEX_UNAVAILABLE_WORKER_EXIT_CODE);
|
|
1709
|
+
}, CODEX_UNAVAILABLE_WORKER_FORCE_EXIT_MS);
|
|
1710
|
+
try {
|
|
1711
|
+
await maybeHeartbeat("offline", null, true);
|
|
1712
|
+
if (directWorktreePath) {
|
|
1713
|
+
await removeIsolatedWorktree(opts.repo, directWorktreePath).catch((err) => {
|
|
1714
|
+
console.error(
|
|
1715
|
+
`[WorkerPals] Failed to remove isolated worktree before Codex recycle: ${String(
|
|
1716
|
+
err,
|
|
1717
|
+
)}`,
|
|
1718
|
+
);
|
|
1719
|
+
});
|
|
1720
|
+
directWorktreePath = null;
|
|
1721
|
+
}
|
|
1722
|
+
await shutdownDockerExecutorBeforeCodexRecycle(dockerExecutor);
|
|
1723
|
+
} finally {
|
|
1724
|
+
clearTimeout(forceExitTimer);
|
|
1725
|
+
process.exit(CODEX_UNAVAILABLE_WORKER_EXIT_CODE);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (job.sessionId && result?.cooldownMs && result.cooldownMs > 0) {
|
|
1729
|
+
await transport.queueSessionCommand(
|
|
1730
|
+
job.sessionId,
|
|
1731
|
+
{
|
|
1732
|
+
type: "assistant_message",
|
|
1733
|
+
payload: {
|
|
1734
|
+
text: `WorkerPal is cooling down for ${formatDurationMs(result.cooldownMs)} after transient infrastructure failures.`,
|
|
1735
|
+
},
|
|
1736
|
+
from: `worker:${opts.workerId}`,
|
|
1680
1737
|
},
|
|
1681
|
-
|
|
1682
|
-
|
|
1738
|
+
{ priority: "high" },
|
|
1739
|
+
);
|
|
1683
1740
|
}
|
|
1684
|
-
if (
|
|
1741
|
+
if (result?.cooldownMs && result.cooldownMs > 0) {
|
|
1685
1742
|
const cooldownMs = Math.max(0, Math.floor(result.cooldownMs));
|
|
1686
1743
|
console.warn(
|
|
1687
1744
|
`[WorkerPals] Entering cooldown for ${formatDurationMs(cooldownMs)} after retry exhaustion.`,
|
|
@@ -1697,18 +1754,6 @@ async function workerLoop(
|
|
|
1697
1754
|
console.error(`[WorkerPals] Failed to remove isolated worktree: ${String(err)}`);
|
|
1698
1755
|
});
|
|
1699
1756
|
}
|
|
1700
|
-
if (recycleWorkerAfterJob) {
|
|
1701
|
-
runtimeState.shutdownRequested = true;
|
|
1702
|
-
await maybeHeartbeat("offline", null, true);
|
|
1703
|
-
if (dockerExecutor) {
|
|
1704
|
-
try {
|
|
1705
|
-
await dockerExecutor.shutdown();
|
|
1706
|
-
} catch (err) {
|
|
1707
|
-
console.error(`[WorkerPals] Docker shutdown cleanup failed: ${String(err)}`);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
process.exit(CODEX_UNAVAILABLE_WORKER_EXIT_CODE);
|
|
1711
|
-
}
|
|
1712
1757
|
}
|
|
1713
1758
|
}
|
|
1714
1759
|
}
|
|
@@ -319,7 +319,11 @@ export function validateScopeInvariants(
|
|
|
319
319
|
componentArea: AutonomyComponentArea | null | undefined,
|
|
320
320
|
targetPathsInput: unknown[],
|
|
321
321
|
writeGlobsInput: unknown[],
|
|
322
|
-
options?: {
|
|
322
|
+
options?: {
|
|
323
|
+
requireWriteGlobs?: boolean;
|
|
324
|
+
allowMultipleComponentRoots?: boolean;
|
|
325
|
+
hintsOnly?: boolean;
|
|
326
|
+
},
|
|
323
327
|
): ScopeValidationResult {
|
|
324
328
|
const errors: string[] = [];
|
|
325
329
|
const scopeSeeds = collectScopeSeedPaths(targetPathsInput, writeGlobsInput);
|
|
@@ -327,7 +331,8 @@ export function validateScopeInvariants(
|
|
|
327
331
|
normalizeAutonomyComponentArea(componentArea) ??
|
|
328
332
|
deriveAutonomyComponentArea(targetPathsInput, writeGlobsInput);
|
|
329
333
|
const allowMultipleComponentRoots = options?.allowMultipleComponentRoots === true;
|
|
330
|
-
|
|
334
|
+
const hintsOnly = options?.hintsOnly === true;
|
|
335
|
+
if (!hintsOnly && !normalizedComponentArea && scopeSeeds.length > 1 && !allowMultipleComponentRoots) {
|
|
331
336
|
errors.push(
|
|
332
337
|
`scope spans multiple component roots: ${scopeSeeds.slice(0, 6).join(", ")}`,
|
|
333
338
|
);
|
|
@@ -341,7 +346,7 @@ export function validateScopeInvariants(
|
|
|
341
346
|
errors.push(`invalid target_path: ${String(raw ?? "")}`);
|
|
342
347
|
continue;
|
|
343
348
|
}
|
|
344
|
-
if (rootPrefix && !underRoot(normalized, rootPrefix)) {
|
|
349
|
+
if (!hintsOnly && rootPrefix && !underRoot(normalized, rootPrefix)) {
|
|
345
350
|
errors.push(`target_path outside component root: ${normalized}`);
|
|
346
351
|
continue;
|
|
347
352
|
}
|
|
@@ -362,20 +367,21 @@ export function validateScopeInvariants(
|
|
|
362
367
|
errors.push(`invalid write_glob: ${String(raw ?? "")}`);
|
|
363
368
|
continue;
|
|
364
369
|
}
|
|
365
|
-
if (hasForbiddenBroadGlob(normalized)) {
|
|
370
|
+
if (!hintsOnly && hasForbiddenBroadGlob(normalized)) {
|
|
366
371
|
errors.push(`forbidden broad write_glob: ${normalized}`);
|
|
367
372
|
continue;
|
|
368
373
|
}
|
|
369
374
|
const prefix = literalPrefix(normalized);
|
|
370
|
-
if (!prefix) {
|
|
375
|
+
if (!hintsOnly && !prefix) {
|
|
371
376
|
errors.push(`write_glob literal prefix cannot be empty: ${normalized}`);
|
|
372
377
|
continue;
|
|
373
378
|
}
|
|
374
|
-
if (rootPrefix && !underRoot(prefix, rootPrefix)) {
|
|
379
|
+
if (!hintsOnly && rootPrefix && !underRoot(prefix, rootPrefix)) {
|
|
375
380
|
errors.push(`write_glob outside component root: ${normalized}`);
|
|
376
381
|
continue;
|
|
377
382
|
}
|
|
378
383
|
if (
|
|
384
|
+
!hintsOnly &&
|
|
379
385
|
!normalizedTargetPaths.some(
|
|
380
386
|
(targetPath) => targetPath === prefix || targetPath.startsWith(`${prefix}/`),
|
|
381
387
|
)
|
|
@@ -393,13 +399,13 @@ export function validateScopeInvariants(
|
|
|
393
399
|
errors.push("write_globs must be provided and non-empty");
|
|
394
400
|
}
|
|
395
401
|
|
|
396
|
-
if (normalizedTargetPaths.length > 0 && normalizedWriteGlobs.length > 0) {
|
|
402
|
+
if (!hintsOnly && normalizedTargetPaths.length > 0 && normalizedWriteGlobs.length > 0) {
|
|
397
403
|
for (const targetPath of normalizedTargetPaths) {
|
|
398
404
|
const covered = normalizedWriteGlobs.some((glob) => matchesGlob(targetPath, glob));
|
|
399
405
|
if (!covered) errors.push(`target_path not covered by write_globs: ${targetPath}`);
|
|
400
406
|
}
|
|
401
407
|
}
|
|
402
|
-
if (!normalizedComponentArea && !allowMultipleComponentRoots) {
|
|
408
|
+
if (!hintsOnly && !normalizedComponentArea && !allowMultipleComponentRoots) {
|
|
403
409
|
errors.push("component_area could not be derived from scope");
|
|
404
410
|
}
|
|
405
411
|
|
|
@@ -53,6 +53,7 @@ export interface CommunicationManagerOptions {
|
|
|
53
53
|
sessionId: string;
|
|
54
54
|
from: string;
|
|
55
55
|
authToken?: string | null;
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export class CommunicationManager {
|
|
@@ -60,12 +61,14 @@ export class CommunicationManager {
|
|
|
60
61
|
private readonly sessionId: string;
|
|
61
62
|
private readonly from: string;
|
|
62
63
|
private readonly authToken: string | null;
|
|
64
|
+
private readonly fetchImpl: typeof fetch;
|
|
63
65
|
|
|
64
66
|
constructor(opts: CommunicationManagerOptions) {
|
|
65
67
|
this.serverUrl = opts.serverUrl;
|
|
66
68
|
this.sessionId = opts.sessionId;
|
|
67
69
|
this.from = opts.from;
|
|
68
70
|
this.authToken = opts.authToken ?? null;
|
|
71
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
private headers(): Record<string, string> {
|
|
@@ -121,7 +124,7 @@ export class CommunicationManager {
|
|
|
121
124
|
if (meta.turnId) body.turnId = meta.turnId;
|
|
122
125
|
if (meta.parentId) body.parentId = meta.parentId;
|
|
123
126
|
|
|
124
|
-
const response = await
|
|
127
|
+
const response = await this.fetchImpl(this.commandUrl(sessionId), {
|
|
125
128
|
method: "POST",
|
|
126
129
|
headers: this.headers(),
|
|
127
130
|
body: JSON.stringify(body),
|
|
@@ -1929,7 +1929,7 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
|
|
|
1929
1929
|
),
|
|
1930
1930
|
allowReadAnywhere:
|
|
1931
1931
|
parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ??
|
|
1932
|
-
asBoolean(remoteAutonomyNode.allow_read_anywhere,
|
|
1932
|
+
asBoolean(remoteAutonomyNode.allow_read_anywhere, true),
|
|
1933
1933
|
prFeedbackCommentRows: Math.max(
|
|
1934
1934
|
1,
|
|
1935
1935
|
Math.min(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Completion requirement:
|
|
1
|
+
Completion requirement: solve the requested task before setting done=true. Target paths are relevance hints; edit other behavior-owning files when needed and explain why.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
Target path hints:
|
|
2
2
|
{{targets_block}}
|