@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.
Files changed (27) hide show
  1. package/dist/pushpals-cli.js +1 -1
  2. package/package.json +2 -2
  3. package/runtime/prompts/remotebuddy/autonomy_ideation_system_prompt.md +2 -1
  4. package/runtime/prompts/remotebuddy/autonomy_planning_system_prompt.md +1 -1
  5. package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +4 -4
  6. package/runtime/prompts/workerpals/miniswe_completion_requirement.md +1 -1
  7. package/runtime/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
  8. package/runtime/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
  9. package/runtime/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
  10. package/runtime/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
  11. package/runtime/prompts/workerpals/workerpals_system_prompt.md +2 -2
  12. package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +248 -98
  13. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +5 -34
  14. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +219 -130
  15. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +57 -0
  16. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +3 -2
  17. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +142 -134
  18. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +70 -25
  19. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +14 -8
  20. package/runtime/sandbox/packages/shared/src/communication.ts +4 -1
  21. package/runtime/sandbox/packages/shared/src/config.ts +1 -1
  22. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -1
  23. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
  24. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
  25. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
  26. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
  27. 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 write scope; treating them as publish blockers, not repair instructions.",
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: buildWorkerSandboxWritableEnv(repo),
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
- function asAutonomyComponentArea(value: unknown): AutonomyComponentArea | null {
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
- const writeGlobs = toStringArray(planning.scope.writeGlobs ?? []);
3957
- if (writeGlobs.length === 0) return [];
3958
-
3959
- const normalizedChangedPaths = changedPaths
3960
- .map((entry) => normalizeStagePath(entry))
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: buildWorkerSandboxWritableEnv(repo),
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 write scope and are treated as pre-existing repo blockers."
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
- !recycleWorkerAfterJob &&
1672
- job.sessionId &&
1673
- result?.cooldownMs &&
1674
- result.cooldownMs > 0
1675
- ) {
1676
- await transport.queueSessionCommand(job.sessionId, {
1677
- type: "assistant_message",
1678
- payload: {
1679
- text: `WorkerPal is cooling down for ${formatDurationMs(result.cooldownMs)} after transient infrastructure failures.`,
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
- from: `worker:${opts.workerId}`,
1682
- }, { priority: "high" });
1738
+ { priority: "high" },
1739
+ );
1683
1740
  }
1684
- if (!recycleWorkerAfterJob && result?.cooldownMs && result.cooldownMs > 0) {
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?: { requireWriteGlobs?: boolean; allowMultipleComponentRoots?: boolean },
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
- if (!normalizedComponentArea && scopeSeeds.length > 1 && !allowMultipleComponentRoots) {
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 fetch(this.commandUrl(sessionId), {
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, false),
1932
+ asBoolean(remoteAutonomyNode.allow_read_anywhere, true),
1933
1933
  prFeedbackCommentRows: Math.max(
1934
1934
  1,
1935
1935
  Math.min(
@@ -1 +1 @@
1
- Completion requirement: handle all requested edits across all explicit target paths before setting done=true.
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
- Explicit target paths:
1
+ Target path hints:
2
2
  {{targets_block}}