@pushpalsdev/cli 1.0.86 → 1.0.93

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 (25) 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 +2 -2
  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 +41 -45
  13. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +5 -34
  14. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +3 -2
  15. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +25 -50
  16. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +70 -25
  17. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +14 -8
  18. package/runtime/sandbox/packages/shared/src/communication.ts +4 -1
  19. package/runtime/sandbox/packages/shared/src/config.ts +1 -1
  20. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -1
  21. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
  22. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
  23. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
  24. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
  25. package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +2 -2
@@ -1071,7 +1071,7 @@ function loadPushPalsConfig(options = {}) {
1071
1071
  const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_ALERT_AUTONOMY_FAILURE_RATE_THRESHOLD, asString(remoteAutonomyNode.alert_autonomy_failure_rate_threshold, "0.45"), "0.45")));
1072
1072
  return Number.isFinite(parsed) ? parsed : 0.45;
1073
1073
  })())),
1074
- allowReadAnywhere: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ?? asBoolean(remoteAutonomyNode.allow_read_anywhere, false),
1074
+ allowReadAnywhere: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ?? asBoolean(remoteAutonomyNode.allow_read_anywhere, true),
1075
1075
  prFeedbackCommentRows: Math.max(1, Math.min(200, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_ROWS") ?? remoteAutonomyNode.pr_feedback_comment_rows, 16))),
1076
1076
  prFeedbackCommentChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_CHARS") ?? remoteAutonomyNode.pr_feedback_comment_chars, 600))),
1077
1077
  prFeedbackSummaryChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_SUMMARY_CHARS") ?? remoteAutonomyNode.pr_feedback_summary_chars, 600))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.86",
3
+ "version": "1.0.93",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "url": "https://github.com/PushPalsDev/pushpals/issues"
13
13
  },
14
14
  "bin": {
15
- "pushpals": "./bin/pushpals.cjs"
15
+ "pushpals": "bin/pushpals.cjs"
16
16
  },
17
17
  "files": [
18
18
  "bin",
@@ -56,9 +56,10 @@ Constraints:
56
56
  - `objective_type` is a governance lane, not a fixed feature catalog. Feature ideas are free-form and should be expressed in `title`, `problem_statement`, and `feature_hypotheses`.
57
57
  - `feature_hypotheses` may contain any suitable product/engineering features; keep each item concise and actionable.
58
58
  - target_paths must be literal repo-relative paths.
59
- - write_globs must be repo-relative globs.
59
+ - write_globs must be repo-relative globs used as starting-point/relevance hints, not hard write boundaries.
60
60
  - Choose target_paths that own the behavior being improved, not thin route wrappers, re-export files, or shell components, unless the requested change is explicitly at that wrapper boundary.
61
61
  - For UI/game/product-surface objectives, prefer files that render or compute the relevant state directly; use wrapper files only for navigation, mounting, or screen-level chrome work.
62
+ - Workers have repo-wide sandbox write access and may expand from these hints to the behavior-owning files; the review agent will judge whether the final diff stays relevant.
62
63
  - do not invent evidence ids.
63
64
  - If all signals are low/noisy, it is valid to return zero candidates.
64
65
  - Treat a low `sig_queue_health` value as maintenance-window evidence for safe proactive work, not only incident response.
@@ -1,5 +1,5 @@
1
1
  Write objective instruction text for a worker.
2
2
  Return strict JSON:
3
3
  { "instruction": "..." }
4
- Keep it concise, executable, and scoped to target_paths and write_globs only.
4
+ Keep it concise and executable. Treat target_paths and write_globs as starting-point/relevance hints, not hard write boundaries; the worker may edit other behavior-owning repo files when needed and the review agent will judge relevance.
5
5
  If you mention commands, use Bun/Bunx command forms (`bun ...`, `bunx ...`), never npm/npx/pnpm/yarn forms.
@@ -11,7 +11,7 @@ Repository boundary policy:
11
11
 
12
12
  - Treat `{{repo_root}}` as the only allowed repository scope.
13
13
  - Never plan edits or checks outside this repository root.
14
- - Prefer explicit repo-relative targets; use broad `"."` scope only when the user explicitly requests whole-repo work.
14
+ - Prefer explicit repo-relative targets as review/relevance hints. WorkerPals have repo-wide sandbox write access, so do not over-constrain write scope unless the user explicitly asks for a hard path limit.
15
15
 
16
16
  Intent taxonomy (choose the single best fit):
17
17
 
@@ -44,7 +44,7 @@ Execution policy:
44
44
  - Scope policy (for `requires_worker=true`):
45
45
  - `scope.read_anywhere` should default to `true` (do not set `false` unless user explicitly requested restrictive reading)
46
46
  - `scope.write_allowed` should default to `true`
47
- - `scope.write_globs` should be included only when you need to constrain edits
47
+ - `scope.write_globs` should be included as starting-point/relevance hints, not as hard write boundaries
48
48
  - `scope.forbidden_globs` should be included only when specific paths must be blocked
49
49
  - `scope.max_files_to_edit` should be included only when a cap is needed
50
50
 
@@ -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}}
@@ -11,7 +11,10 @@ Non-negotiable runtime invariants:
11
11
 
12
12
  Execution rules:
13
13
 
14
- - Keep edits minimal, correct, and scoped to the requested task.
14
+ - Keep edits minimal, correct, and relevant to the requested task.
15
+ - You have repo-wide read/write access inside an isolated WorkerPal sandbox. Target paths and write globs are starting-point/relevance hints, not hard write boundaries.
16
+ - If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
17
+ - Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
15
18
  - Read relevant files before editing, then run focused validation.
16
19
  - Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
17
20
  - Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
@@ -1,10 +1,12 @@
1
1
  You are PushPals WorkerPal running inside OpenHands.
2
2
 
3
- Operate with strict scope:
3
+ Operate with strict relevance:
4
4
  - Focus only on the current user task.
5
5
  - If the task is a question, answer directly and do not edit files.
6
6
  - If code/file changes are requested, implement them end-to-end.
7
7
  - Reuse existing repository conventions and avoid unrelated refactors/docs.
8
+ - Treat target paths and write globs as starting-point/relevance hints, not hard write boundaries.
9
+ - If the behavior lives outside the hinted files, edit the owning files and explain the scope expansion.
8
10
 
9
11
  Execution loop:
10
12
  - Prefer concrete execution over planning chatter.
@@ -3,7 +3,8 @@ You are PushPals WorkerPal running inside OpenHands.
3
3
  Execution rules:
4
4
 
5
5
  - Focus only on the task below.
6
- - Keep changes minimal, correct, and scoped to the request.
6
+ - Keep changes minimal, correct, and relevant to the request.
7
+ - Target paths and write globs are starting-point/relevance hints, not hard write boundaries; if the behavior lives elsewhere, edit the owning files and explain why.
7
8
  - Read relevant files before editing.
8
9
  - Reuse existing project conventions and tooling.
9
10
  - If the task is a question/explanation, answer directly and do not edit files.
@@ -6,7 +6,7 @@ Your mission:
6
6
 
7
7
  - Take the user (or RemoteBuddy) request and fully execute it end-to-end.
8
8
  - You are responsible for breaking the work down into concrete subtasks, completing them, validating, reviewing your own changes, and preparing a high-quality commit message when the work is ready.
9
- - You have full read/write access within the assigned repository only; do not access files outside that repository.
9
+ - You have full read/write access within the assigned repository sandbox only; do not access files outside that repository. Target paths and write globs are starting-point/relevance hints, not hard write boundaries.
10
10
 
11
11
  Mindset:
12
12
 
@@ -64,7 +64,7 @@ Execution workflow (you MUST follow this):
64
64
  - Performance: no unnecessary work on UI thread, no extra network calls, no large bundles
65
65
  - Cross-platform: iOS/Android/Web differences guarded appropriately
66
66
  - Security: no secret leakage, safe networking defaults, no unsafe shell usage
67
- - Make any final polish edits that improve clarity without changing scope.
67
+ - Make any final polish edits that improve clarity without drifting from the task.
68
68
 
69
69
  7. Prepare to commit (when appropriate)
70
70
  - When the work is ready, produce a detailed commit message (do NOT actually commit unless your system explicitly allows it).
@@ -86,11 +86,13 @@ class CommunicationManager {
86
86
  sessionId;
87
87
  from;
88
88
  authToken;
89
+ fetchImpl;
89
90
  constructor(opts) {
90
91
  this.serverUrl = opts.serverUrl;
91
92
  this.sessionId = opts.sessionId;
92
93
  this.from = opts.from;
93
94
  this.authToken = opts.authToken ?? null;
95
+ this.fetchImpl = opts.fetchImpl ?? fetch;
94
96
  }
95
97
  headers() {
96
98
  const headers = { "Content-Type": "application/json" };
@@ -132,7 +134,7 @@ class CommunicationManager {
132
134
  body.turnId = meta.turnId;
133
135
  if (meta.parentId)
134
136
  body.parentId = meta.parentId;
135
- const response = await fetch(this.commandUrl(sessionId), {
137
+ const response = await this.fetchImpl(this.commandUrl(sessionId), {
136
138
  method: "POST",
137
139
  headers: this.headers(),
138
140
  body: JSON.stringify(body)
@@ -588,7 +590,8 @@ function validateScopeInvariants(componentArea, targetPathsInput, writeGlobsInpu
588
590
  const scopeSeeds = collectScopeSeedPaths(targetPathsInput, writeGlobsInput);
589
591
  const normalizedComponentArea = normalizeAutonomyComponentArea(componentArea) ?? deriveAutonomyComponentArea(targetPathsInput, writeGlobsInput);
590
592
  const allowMultipleComponentRoots = options?.allowMultipleComponentRoots === true;
591
- if (!normalizedComponentArea && scopeSeeds.length > 1 && !allowMultipleComponentRoots) {
593
+ const hintsOnly = options?.hintsOnly === true;
594
+ if (!hintsOnly && !normalizedComponentArea && scopeSeeds.length > 1 && !allowMultipleComponentRoots) {
592
595
  errors.push(`scope spans multiple component roots: ${scopeSeeds.slice(0, 6).join(", ")}`);
593
596
  }
594
597
  const rootPrefix = normalizedComponentArea ? componentRootPrefix(normalizedComponentArea) : "";
@@ -600,7 +603,7 @@ function validateScopeInvariants(componentArea, targetPathsInput, writeGlobsInpu
600
603
  errors.push(`invalid target_path: ${String(raw ?? "")}`);
601
604
  continue;
602
605
  }
603
- if (rootPrefix && !underRoot(normalized, rootPrefix)) {
606
+ if (!hintsOnly && rootPrefix && !underRoot(normalized, rootPrefix)) {
604
607
  errors.push(`target_path outside component root: ${normalized}`);
605
608
  continue;
606
609
  }
@@ -621,20 +624,20 @@ function validateScopeInvariants(componentArea, targetPathsInput, writeGlobsInpu
621
624
  errors.push(`invalid write_glob: ${String(raw ?? "")}`);
622
625
  continue;
623
626
  }
624
- if (hasForbiddenBroadGlob(normalized)) {
627
+ if (!hintsOnly && hasForbiddenBroadGlob(normalized)) {
625
628
  errors.push(`forbidden broad write_glob: ${normalized}`);
626
629
  continue;
627
630
  }
628
631
  const prefix = literalPrefix(normalized);
629
- if (!prefix) {
632
+ if (!hintsOnly && !prefix) {
630
633
  errors.push(`write_glob literal prefix cannot be empty: ${normalized}`);
631
634
  continue;
632
635
  }
633
- if (rootPrefix && !underRoot(prefix, rootPrefix)) {
636
+ if (!hintsOnly && rootPrefix && !underRoot(prefix, rootPrefix)) {
634
637
  errors.push(`write_glob outside component root: ${normalized}`);
635
638
  continue;
636
639
  }
637
- if (!normalizedTargetPaths.some((targetPath) => targetPath === prefix || targetPath.startsWith(`${prefix}/`))) {
640
+ if (!hintsOnly && !normalizedTargetPaths.some((targetPath) => targetPath === prefix || targetPath.startsWith(`${prefix}/`))) {
638
641
  errors.push(`write_glob prefix does not align with target_paths: ${normalized}`);
639
642
  continue;
640
643
  }
@@ -647,14 +650,14 @@ function validateScopeInvariants(componentArea, targetPathsInput, writeGlobsInpu
647
650
  if ((options?.requireWriteGlobs ?? true) && normalizedWriteGlobs.length === 0) {
648
651
  errors.push("write_globs must be provided and non-empty");
649
652
  }
650
- if (normalizedTargetPaths.length > 0 && normalizedWriteGlobs.length > 0) {
653
+ if (!hintsOnly && normalizedTargetPaths.length > 0 && normalizedWriteGlobs.length > 0) {
651
654
  for (const targetPath of normalizedTargetPaths) {
652
655
  const covered = normalizedWriteGlobs.some((glob) => matchesGlob(targetPath, glob));
653
656
  if (!covered)
654
657
  errors.push(`target_path not covered by write_globs: ${targetPath}`);
655
658
  }
656
659
  }
657
- if (!normalizedComponentArea && !allowMultipleComponentRoots) {
660
+ if (!hintsOnly && !normalizedComponentArea && !allowMultipleComponentRoots) {
658
661
  errors.push("component_area could not be derived from scope");
659
662
  }
660
663
  const breadth = classifyGlobBreadth(normalizedWriteGlobs);
@@ -1357,7 +1360,7 @@ function loadPushPalsConfig(options = {}) {
1357
1360
  const parsed = Number.parseFloat(String(firstNonEmpty(process.env.REMOTEBUDDY_AUTONOMY_ALERT_AUTONOMY_FAILURE_RATE_THRESHOLD, asString(remoteAutonomyNode.alert_autonomy_failure_rate_threshold, "0.45"), "0.45")));
1358
1361
  return Number.isFinite(parsed) ? parsed : 0.45;
1359
1362
  })())),
1360
- allowReadAnywhere: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ?? asBoolean(remoteAutonomyNode.allow_read_anywhere, false),
1363
+ allowReadAnywhere: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ALLOW_READ_ANYWHERE") ?? asBoolean(remoteAutonomyNode.allow_read_anywhere, true),
1361
1364
  prFeedbackCommentRows: Math.max(1, Math.min(200, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_ROWS") ?? remoteAutonomyNode.pr_feedback_comment_rows, 16))),
1362
1365
  prFeedbackCommentChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_COMMENT_CHARS") ?? remoteAutonomyNode.pr_feedback_comment_chars, 600))),
1363
1366
  prFeedbackSummaryChars: Math.max(32, Math.min(20000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_PR_FEEDBACK_SUMMARY_CHARS") ?? remoteAutonomyNode.pr_feedback_summary_chars, 600))),
@@ -4257,11 +4260,6 @@ var POLICY = {
4257
4260
  }
4258
4261
  };
4259
4262
  var RISK_ORDER = { low: 0, medium: 1, high: 2 };
4260
- var BREADTH_ORDER = {
4261
- narrow: 0,
4262
- medium: 1,
4263
- broad: 2
4264
- };
4265
4263
  var IDEATION_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_ideation_system_prompt.md").trim();
4266
4264
  var SCORING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_scoring_system_prompt.md").trim();
4267
4265
  var PLANNING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_planning_system_prompt.md").trim();
@@ -4996,7 +4994,8 @@ function chooseRepoObjectiveTargetProfile(profiles, objective) {
4996
4994
  function adaptCandidateShapeToRepo(params) {
4997
4995
  const shape = params.shape;
4998
4996
  const scopeValidation = validateScopeInvariants(shape.component_area, shape.target_paths, shape.write_globs, {
4999
- requireWriteGlobs: true
4997
+ requireWriteGlobs: true,
4998
+ hintsOnly: true
5000
4999
  });
5001
5000
  const pathsExist = params.repoRoot && scopeValidation.ok ? findMissingRepoTargetPaths(params.repoRoot, scopeValidation.normalizedTargetPaths).length === 0 : scopeValidation.ok;
5002
5001
  if (scopeValidation.ok && pathsExist) {
@@ -5782,7 +5781,7 @@ ${pattern.tags.join(" ")}`.toLowerCase();
5782
5781
  const targetPaths = asStringArray2(metadataShape.target_paths ?? metadataShape.targetPaths ?? metadata.target_paths);
5783
5782
  const writeGlobs = asStringArray2(metadataShape.write_globs ?? metadataShape.writeGlobs ?? metadata.write_globs);
5784
5783
  const validationIdeas = asStringArray2(metadataShape.expected_validation ?? metadataShape.expectedValidation ?? metadata.expected_validation ?? pattern.validationIdeas);
5785
- const scopeCheck = validateScopeInvariants(componentArea, targetPaths.length > 0 ? targetPaths : defaults.target_paths, writeGlobs.length > 0 ? writeGlobs : defaults.write_globs, { requireWriteGlobs: true });
5784
+ const scopeCheck = validateScopeInvariants(componentArea, targetPaths.length > 0 ? targetPaths : defaults.target_paths, writeGlobs.length > 0 ? writeGlobs : defaults.write_globs, { requireWriteGlobs: true, hintsOnly: true });
5786
5785
  return adaptCandidateShapeToRepo({
5787
5786
  shape: {
5788
5787
  objective_type: objectiveType,
@@ -6217,7 +6216,7 @@ function buildRepoVisionFallbackCandidates(params) {
6217
6216
  component_area: componentArea,
6218
6217
  target_paths: targetPaths,
6219
6218
  scope: {
6220
- read_anywhere: false,
6219
+ read_anywhere: true,
6221
6220
  write_globs: writeGlobs
6222
6221
  },
6223
6222
  risk_level: "low",
@@ -6271,7 +6270,7 @@ function buildEngineFallbackCandidates(params) {
6271
6270
  component_area: candidateShape.component_area,
6272
6271
  target_paths: candidateShape.target_paths,
6273
6272
  scope: {
6274
- read_anywhere: false,
6273
+ read_anywhere: true,
6275
6274
  write_globs: candidateShape.write_globs
6276
6275
  },
6277
6276
  risk_level: candidateShape.risk_level,
@@ -7367,15 +7366,11 @@ ${JSON.stringify(input.messages ?? [])}`),
7367
7366
  recordDropReason(`${source}_risk_exceeds_policy`);
7368
7367
  continue;
7369
7368
  }
7370
- const scopeValidation = validateScopeInvariants(candidate.component_area, candidate.target_paths, candidate.scope.write_globs, { requireWriteGlobs: true });
7369
+ const scopeValidation = validateScopeInvariants(candidate.component_area, candidate.target_paths, candidate.scope.write_globs, { requireWriteGlobs: true, hintsOnly: true });
7371
7370
  if (!scopeValidation.ok) {
7372
7371
  recordDropReason(`${source}_scope_validation_failed`);
7373
7372
  continue;
7374
7373
  }
7375
- if (BREADTH_ORDER[scopeValidation.breadth] > BREADTH_ORDER[policy.maxBreadth]) {
7376
- recordDropReason(`${source}_scope_breadth_exceeds_policy`);
7377
- continue;
7378
- }
7379
7374
  if (candidate.scope.read_anywhere && !this.cfg.allowReadAnywhere) {
7380
7375
  recordDropReason(`${source}_read_anywhere_not_allowed`);
7381
7376
  continue;
@@ -8266,12 +8261,13 @@ function buildExecutionGuidance(plan, targetPaths, requiredValidationSteps = [])
8266
8261
  const lines = [];
8267
8262
  const targets = normalizePathHints(targetPaths.length > 0 ? targetPaths : plan.scope.write_globs ?? []);
8268
8263
  if (targets.length > 0) {
8269
- lines.push("Target paths:");
8264
+ lines.push("Target paths / starting points:");
8270
8265
  for (const path of targets)
8271
8266
  lines.push(`- ${path}`);
8272
8267
  lines.push("Path handling:");
8273
8268
  lines.push("- Treat all target paths as repo-relative to the current working directory.");
8274
8269
  lines.push("- Do not prepend a leading slash to target paths.");
8270
+ lines.push("- These paths are relevance hints, not hard write boundaries; edit the behavior-owning files needed for the task and explain any expansion.");
8275
8271
  }
8276
8272
  lines.push("Scope:");
8277
8273
  lines.push(`- read_anywhere: ${plan.scope.read_anywhere ? "true" : "false"}`);
@@ -8280,7 +8276,7 @@ function buildExecutionGuidance(plan, targetPaths, requiredValidationSteps = [])
8280
8276
  lines.push(`- max_files_to_edit: ${plan.scope.max_files_to_edit}`);
8281
8277
  }
8282
8278
  if (Array.isArray(plan.scope.write_globs) && plan.scope.write_globs.length > 0) {
8283
- lines.push("Write globs:");
8279
+ lines.push("Write intent hints:");
8284
8280
  for (const glob of plan.scope.write_globs)
8285
8281
  lines.push(`- ${glob}`);
8286
8282
  }
@@ -8556,6 +8552,7 @@ class RemoteBuddyOrchestrator {
8556
8552
  server;
8557
8553
  sessionId;
8558
8554
  authToken;
8555
+ fetchImpl;
8559
8556
  repo;
8560
8557
  jobsDbPath;
8561
8558
  workerOnlineTtlMs;
@@ -8624,6 +8621,7 @@ class RemoteBuddyOrchestrator {
8624
8621
  this.server = opts.server;
8625
8622
  this.sessionId = opts.sessionId;
8626
8623
  this.authToken = opts.authToken;
8624
+ this.fetchImpl = opts.fetchImpl ?? fetch;
8627
8625
  this.brain = opts.brain;
8628
8626
  this.idempotency = opts.idempotency;
8629
8627
  this.persistentMemory = opts.persistentMemory;
@@ -8687,7 +8685,8 @@ class RemoteBuddyOrchestrator {
8687
8685
  serverUrl: this.server,
8688
8686
  sessionId: this.sessionId,
8689
8687
  authToken: this.authToken,
8690
- from: `agent:${this.agentId}`
8688
+ from: `agent:${this.agentId}`,
8689
+ fetchImpl: this.fetchImpl
8691
8690
  });
8692
8691
  this.autonomousEngine = new RemoteBuddyAutonomousEngine({
8693
8692
  server: this.server,
@@ -8749,7 +8748,7 @@ class RemoteBuddyOrchestrator {
8749
8748
  async ensureSessionWithRetry(sessionId = this.sessionId, maxRetries = 20, baseDelayMs = 500, maxDelayMs = 5000) {
8750
8749
  for (let attempt = 1;attempt <= maxRetries && !this.disposed; attempt++) {
8751
8750
  try {
8752
- const res = await fetch(`${this.server}/sessions`, {
8751
+ const res = await this.fetchImpl(`${this.server}/sessions`, {
8753
8752
  method: "POST",
8754
8753
  headers: this.authHeaders(),
8755
8754
  body: JSON.stringify({ sessionId })
@@ -8796,7 +8795,7 @@ class RemoteBuddyOrchestrator {
8796
8795
  }
8797
8796
  async fetchJobLogs(jobId, limit = 80) {
8798
8797
  try {
8799
- const res = await fetch(`${this.server}/jobs/${jobId}/logs?limit=${Math.max(1, Math.min(500, limit))}`, {
8798
+ const res = await this.fetchImpl(`${this.server}/jobs/${jobId}/logs?limit=${Math.max(1, Math.min(500, limit))}`, {
8800
8799
  method: "GET",
8801
8800
  headers: this.authHeaders()
8802
8801
  });
@@ -8812,7 +8811,7 @@ class RemoteBuddyOrchestrator {
8812
8811
  }
8813
8812
  async fetchJobToolRuns(jobId, limit = 20) {
8814
8813
  try {
8815
- const res = await fetch(`${this.server}/jobs/${jobId}/tool-runs?limit=${Math.max(1, Math.min(100, limit))}`, {
8814
+ const res = await this.fetchImpl(`${this.server}/jobs/${jobId}/tool-runs?limit=${Math.max(1, Math.min(100, limit))}`, {
8816
8815
  method: "GET",
8817
8816
  headers: this.authHeaders()
8818
8817
  });
@@ -8868,7 +8867,7 @@ class RemoteBuddyOrchestrator {
8868
8867
  query.set("feedbackLimit", "3");
8869
8868
  const suffix = query.toString();
8870
8869
  try {
8871
- const res = await fetch(`${this.server}/autonomy/insights${suffix ? `?${suffix}` : ""}`, {
8870
+ const res = await this.fetchImpl(`${this.server}/autonomy/insights${suffix ? `?${suffix}` : ""}`, {
8872
8871
  method: "GET",
8873
8872
  headers: this.authHeaders()
8874
8873
  });
@@ -9169,7 +9168,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9169
9168
  payload.dedupeKey = dedupeKey;
9170
9169
  if (targetWorkerId)
9171
9170
  payload.targetWorkerId = targetWorkerId;
9172
- const res = await fetch(`${this.server}/jobs/enqueue`, {
9171
+ const res = await this.fetchImpl(`${this.server}/jobs/enqueue`, {
9173
9172
  method: "POST",
9174
9173
  headers: this.authHeaders(),
9175
9174
  body: JSON.stringify(payload)
@@ -9439,7 +9438,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9439
9438
  }
9440
9439
  async fetchWorkers() {
9441
9440
  try {
9442
- const res = await fetch(`${this.server}/workers?ttlMs=${this.workerOnlineTtlMs}`, {
9441
+ const res = await this.fetchImpl(`${this.server}/workers?ttlMs=${this.workerOnlineTtlMs}`, {
9443
9442
  method: "GET",
9444
9443
  headers: this.authHeaders()
9445
9444
  });
@@ -9453,7 +9452,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9453
9452
  }
9454
9453
  async fetchWorkerAutoscaleSnapshot() {
9455
9454
  try {
9456
- const res = await fetch(`${this.server}/workers/autoscale?ttlMs=${this.workerOnlineTtlMs}`, {
9455
+ const res = await this.fetchImpl(`${this.server}/workers/autoscale?ttlMs=${this.workerOnlineTtlMs}`, {
9457
9456
  method: "GET",
9458
9457
  headers: this.authHeaders()
9459
9458
  });
@@ -9715,7 +9714,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9715
9714
  const prompt = String(request.prompt ?? "").trim();
9716
9715
  if (!prompt) {
9717
9716
  console.warn(`[RemoteBuddy] Request ${requestId} missing prompt; marking failed`);
9718
- await fetch(`${this.server}/requests/${requestId}/fail`, {
9717
+ await this.fetchImpl(`${this.server}/requests/${requestId}/fail`, {
9719
9718
  method: "POST",
9720
9719
  headers: this.authHeaders(),
9721
9720
  body: JSON.stringify({ message: "Request missing prompt" })
@@ -9749,7 +9748,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9749
9748
  plan.job_kind = "task.execute";
9750
9749
  plan.lane = "worker";
9751
9750
  }
9752
- plan.scope.read_anywhere = false;
9751
+ plan.scope.read_anywhere = true;
9753
9752
  plan.scope.write_allowed = true;
9754
9753
  plan.scope.write_globs = [...autonomyMetadata.writeGlobs];
9755
9754
  }
@@ -9774,7 +9773,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9774
9773
  if (scopeCoverage.addedGlobs.length > 0) {
9775
9774
  console.warn(`[RemoteBuddy] Planner write_globs did not cover target paths. Added scope globs: ${scopeCoverage.addedGlobs.join(", ")}`);
9776
9775
  }
9777
- if (forceWorker) {
9776
+ if (forceWorker && !autonomyMetadata) {
9778
9777
  const concreteTargetCount = targetPaths.filter((entry) => entry && entry !== ".").length;
9779
9778
  if (concreteTargetCount > 0) {
9780
9779
  const currentMax = Number.isFinite(Number(plan.scope.max_files_to_edit)) && Number(plan.scope.max_files_to_edit) > 0 ? Math.floor(Number(plan.scope.max_files_to_edit)) : 0;
@@ -9783,9 +9782,6 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9783
9782
  }
9784
9783
  }
9785
9784
  }
9786
- if (autonomyMetadata && (!plan.scope.write_globs || plan.scope.write_globs.length === 0)) {
9787
- throw new Error("Autonomy-origin request requires non-empty planning.scope.write_globs before task dispatch.");
9788
- }
9789
9785
  if (plan.acceptance_criteria.length === 0) {
9790
9786
  plan.acceptance_criteria = ["Produce a correct and helpful result for the user request."];
9791
9787
  }
@@ -9844,7 +9840,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9844
9840
  await this.assistantMessage(requestSessionId, "Should I have a WorkerPal implement this? Reply to confirm and I'll enqueue the work, or clarify what you'd like focused on.", { turnId, correlationId: requestId, from: eventFrom });
9845
9841
  }
9846
9842
  }
9847
- await fetch(`${this.server}/requests/${requestId}/complete`, {
9843
+ await this.fetchImpl(`${this.server}/requests/${requestId}/complete`, {
9848
9844
  method: "POST",
9849
9845
  headers: this.authHeaders(),
9850
9846
  body: JSON.stringify({
@@ -9875,7 +9871,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
9875
9871
  correlationId: requestId,
9876
9872
  from: eventFrom
9877
9873
  });
9878
- await fetch(`${this.server}/requests/${requestId}/fail`, {
9874
+ await this.fetchImpl(`${this.server}/requests/${requestId}/fail`, {
9879
9875
  method: "POST",
9880
9876
  headers: this.authHeaders(),
9881
9877
  body: JSON.stringify({
@@ -10010,7 +10006,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
10010
10006
  await this.assistantMessage(requestSessionId, "I could not queue this WorkerPal task. No task was started.", { turnId, correlationId: requestId, from: eventFrom });
10011
10007
  this.rememberPersistentMemory("job_enqueue_failed", `enqueue_failed lane=${lane} intent=${plan.intent}`, requestId, requestSessionId);
10012
10008
  }
10013
- await fetch(`${this.server}/requests/${requestId}/complete`, {
10009
+ await this.fetchImpl(`${this.server}/requests/${requestId}/complete`, {
10014
10010
  method: "POST",
10015
10011
  headers: this.authHeaders(),
10016
10012
  body: JSON.stringify({
@@ -10041,7 +10037,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
10041
10037
  correlationId: requestId,
10042
10038
  from: eventFrom
10043
10039
  });
10044
- await fetch(`${this.server}/requests/${requestId}/fail`, {
10040
+ await this.fetchImpl(`${this.server}/requests/${requestId}/fail`, {
10045
10041
  method: "POST",
10046
10042
  headers: this.authHeaders(),
10047
10043
  body: JSON.stringify({
@@ -10056,7 +10052,7 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
10056
10052
  while (!this.disposed) {
10057
10053
  try {
10058
10054
  await this.maybeAutoscaleWorkers();
10059
- const res = await fetch(`${this.server}/requests/claim`, {
10055
+ const res = await this.fetchImpl(`${this.server}/requests/claim`, {
10060
10056
  method: "POST",
10061
10057
  headers: this.authHeaders(),
10062
10058
  body: JSON.stringify({ agentId: this.agentId })
@@ -465,25 +465,9 @@ def _extract_write_globs_from_payload(payload: Optional[Dict[str, Any]]) -> List
465
465
 
466
466
 
467
467
  def _assert_write_allowed(repo: str, path: str, write_globs: Optional[List[str]]) -> None:
468
- if not write_globs:
469
- return
470
- normalized = _normalize_concrete_repo_path(repo, path)
471
- if not normalized:
472
- raise RuntimeError(f"Invalid write path for scope enforcement: {path!r}")
473
- for glob in write_globs:
474
- pattern = str(glob or "").strip()
475
- if not pattern:
476
- continue
477
- if any(ch in pattern for ch in "*?[]"):
478
- if fnmatch.fnmatchcase(normalized, pattern):
479
- return
480
- continue
481
- if normalized == pattern or normalized.startswith(pattern + "/"):
482
- return
483
- raise RuntimeError(
484
- "Scope violation: attempted write outside writeGlobs. "
485
- f"path={normalized!r} write_globs={write_globs!r}"
486
- )
468
+ # WorkerPal jobs run in isolated sandboxes. Scope hints are used for review
469
+ # relevance, not per-write filesystem enforcement.
470
+ return
487
471
 
488
472
 
489
473
  def _read_text_file(repo: str, path: str, max_chars: int = 60000) -> str:
@@ -1587,11 +1571,6 @@ def _broker_run(
1587
1571
  if expected_targets and changed_paths:
1588
1572
  changed_set = {str(p).strip().replace("\\", "/") for p in changed_paths}
1589
1573
  expected_set = {str(p).strip().replace("\\", "/") for p in expected_targets}
1590
- strict_target_match = bool(
1591
- explicit_target_set
1592
- and not any(t in {".", "/"} for t in explicit_target_set)
1593
- and not any(any(ch in t for ch in "*?[]") for t in explicit_target_set)
1594
- )
1595
1574
  matched = any(
1596
1575
  _target_hint_matches_changed_path(expected, changed)
1597
1576
  for expected in expected_set
@@ -1602,14 +1581,6 @@ def _broker_run(
1602
1581
  "Expected one of target paths to change, but observed different files. "
1603
1582
  f"expected={sorted(expected_set)} observed={sorted(changed_set)}"
1604
1583
  )
1605
- if strict_target_match:
1606
- return {
1607
- "ok": False,
1608
- "summary": "tool broker failed: changed files do not match explicit target paths",
1609
- "stdout": stdout + "\n\nChanged files:\n" + "\n".join(f"- {p}" for p in changed_paths),
1610
- "stderr": msg,
1611
- "exitCode": 3,
1612
- }
1613
1584
  stdout += "\n\nTarget-path mismatch (heuristic, non-fatal):\n" + msg
1614
1585
  if edits_made and not shell_validation_ran:
1615
1586
  stdout += (
@@ -1786,10 +1757,10 @@ def _run_miniswe_task(
1786
1757
  agent = None
1787
1758
  agent_messages: List[Dict[str, Any]] = []
1788
1759
  broker_enabled = _tool_broker_enabled(base_url)
1789
- prefer_broker_for_scoped_writes = bool(explicit_write_globs)
1760
+ prefer_broker_for_scoped_writes = False
1790
1761
  ran_primary_broker = False
1791
1762
  if prefer_broker_for_scoped_writes and broker_enabled:
1792
- log.info("Using tool broker shim for strict per-write scope enforcement.")
1763
+ log.info("Using tool broker shim for task execution.")
1793
1764
  broker_result = _run_broker_with_recovery()
1794
1765
  if not bool(broker_result.get("ok")):
1795
1766
  return {
@@ -352,9 +352,10 @@ def _build_path_handling_message(target_paths: List[str], repo: str) -> str:
352
352
  "- Prefer the repo-relative paths for shell commands.\n"
353
353
  "- If FileEditor rejects a repo-relative path, retry with the matching absolute path.\n"
354
354
  "- Do not run broad filesystem scans when concrete target paths are listed.\n"
355
- "Concrete target paths (repo-relative):\n"
355
+ "- These paths are starting points, not hard write boundaries; edit other behavior-owning files when needed and explain why.\n"
356
+ "Target path hints (repo-relative):\n"
356
357
  f"{listed_rel}\n"
357
- "Concrete target paths (absolute):\n"
358
+ "Target path hints (absolute):\n"
358
359
  f"{listed_abs}"
359
360
  )
360
361
 
@@ -19,7 +19,6 @@ import {
19
19
  normalizeTargetPath,
20
20
  requirementsForValidationCommand,
21
21
  sanitizeSourceControlIdentityField,
22
- validateScopeInvariants,
23
22
  type AutonomyComponentArea,
24
23
  type SourceControlCommitIdentity,
25
24
  type ToolRequirement,
@@ -2503,12 +2502,21 @@ function buildStageTargets(kind: string, params?: Record<string, unknown>): stri
2503
2502
  }
2504
2503
  }
2505
2504
 
2506
- function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
2505
+ export function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
2506
+ if (kind === "task.execute") {
2507
+ return [
2508
+ "add",
2509
+ "-A",
2510
+ "--",
2511
+ ".",
2512
+ ":(exclude)workspace/**",
2513
+ ":(exclude)outputs/**",
2514
+ ":(exclude).codex",
2515
+ ":(exclude).codex/**",
2516
+ ];
2517
+ }
2507
2518
  const targets = buildStageTargets(kind, params);
2508
2519
  if (targets.length === 0) {
2509
- if (kind === "task.execute") {
2510
- return ["add", "-A", "--", ".", ":(exclude)workspace/**", ":(exclude)outputs/**"];
2511
- }
2512
2520
  return null;
2513
2521
  }
2514
2522
  return ["add", "-A", "--", ...targets];
@@ -3949,13 +3957,10 @@ function taskExecuteOrigin(params: Record<string, unknown>): "autonomy" | "user"
3949
3957
  return "user";
3950
3958
  }
3951
3959
 
3952
- function collectWriteScopeIssuesFromChangedPaths(
3960
+ export function collectWriteScopeIssuesFromChangedPaths(
3953
3961
  changedPaths: string[],
3954
3962
  planning: TaskExecutePlanning,
3955
3963
  ): string[] {
3956
- const writeGlobs = toStringArray(planning.scope.writeGlobs ?? []);
3957
- if (writeGlobs.length === 0) return [];
3958
-
3959
3964
  const normalizedChangedPaths = changedPaths
3960
3965
  .map((entry) => normalizeStagePath(entry))
3961
3966
  .filter((entry): entry is string => Boolean(entry) && entry !== ".");
@@ -3963,12 +3968,6 @@ function collectWriteScopeIssuesFromChangedPaths(
3963
3968
 
3964
3969
  const forbidden = toStringArray(planning.scope.forbiddenGlobs ?? []);
3965
3970
  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
3971
  const forbiddenTouched = normalizedChangedPaths.filter((path) =>
3973
3972
  forbidden.some((glob) => matchesGlob(path, glob)),
3974
3973
  );
@@ -4105,41 +4104,17 @@ function validateTaskExecutePlanning(
4105
4104
  reviewAgentAllowsMultiRootScope(options?.reviewAgentResolutionType);
4106
4105
  if (origin === "autonomy") {
4107
4106
  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
- };
4107
+ if (!allowMultiRootAutonomyScope && declaredComponentArea) {
4108
+ const inferredComponentArea = deriveAutonomyComponentArea(
4109
+ normalizedTargetPaths,
4110
+ normalizedWriteGlobs,
4111
+ );
4112
+ if (inferredComponentArea && declaredComponentArea !== inferredComponentArea) {
4113
+ return {
4114
+ ok: false,
4115
+ message: "task.execute planning.targetPaths do not match autonomy componentArea",
4116
+ };
4117
+ }
4143
4118
  }
4144
4119
  } else if (normalizedWriteGlobs.length > 0) {
4145
4120
  const uncoveredPaths = normalizedTargetPaths.filter(
@@ -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}}
@@ -11,7 +11,10 @@ Non-negotiable runtime invariants:
11
11
 
12
12
  Execution rules:
13
13
 
14
- - Keep edits minimal, correct, and scoped to the requested task.
14
+ - Keep edits minimal, correct, and relevant to the requested task.
15
+ - You have repo-wide read/write access inside an isolated WorkerPal sandbox. Target paths and write globs are starting-point/relevance hints, not hard write boundaries.
16
+ - If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
17
+ - Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
15
18
  - Read relevant files before editing, then run focused validation.
16
19
  - Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
17
20
  - Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
@@ -1,10 +1,12 @@
1
1
  You are PushPals WorkerPal running inside OpenHands.
2
2
 
3
- Operate with strict scope:
3
+ Operate with strict relevance:
4
4
  - Focus only on the current user task.
5
5
  - If the task is a question, answer directly and do not edit files.
6
6
  - If code/file changes are requested, implement them end-to-end.
7
7
  - Reuse existing repository conventions and avoid unrelated refactors/docs.
8
+ - Treat target paths and write globs as starting-point/relevance hints, not hard write boundaries.
9
+ - If the behavior lives outside the hinted files, edit the owning files and explain the scope expansion.
8
10
 
9
11
  Execution loop:
10
12
  - Prefer concrete execution over planning chatter.
@@ -3,7 +3,8 @@ You are PushPals WorkerPal running inside OpenHands.
3
3
  Execution rules:
4
4
 
5
5
  - Focus only on the task below.
6
- - Keep changes minimal, correct, and scoped to the request.
6
+ - Keep changes minimal, correct, and relevant to the request.
7
+ - Target paths and write globs are starting-point/relevance hints, not hard write boundaries; if the behavior lives elsewhere, edit the owning files and explain why.
7
8
  - Read relevant files before editing.
8
9
  - Reuse existing project conventions and tooling.
9
10
  - If the task is a question/explanation, answer directly and do not edit files.
@@ -6,7 +6,7 @@ Your mission:
6
6
 
7
7
  - Take the user (or RemoteBuddy) request and fully execute it end-to-end.
8
8
  - You are responsible for breaking the work down into concrete subtasks, completing them, validating, reviewing your own changes, and preparing a high-quality commit message when the work is ready.
9
- - You have full read/write access within the assigned repository only; do not access files outside that repository.
9
+ - You have full read/write access within the assigned repository sandbox only; do not access files outside that repository. Target paths and write globs are starting-point/relevance hints, not hard write boundaries.
10
10
 
11
11
  Mindset:
12
12
 
@@ -64,7 +64,7 @@ Execution workflow (you MUST follow this):
64
64
  - Performance: no unnecessary work on UI thread, no extra network calls, no large bundles
65
65
  - Cross-platform: iOS/Android/Web differences guarded appropriately
66
66
  - Security: no secret leakage, safe networking defaults, no unsafe shell usage
67
- - Make any final polish edits that improve clarity without changing scope.
67
+ - Make any final polish edits that improve clarity without drifting from the task.
68
68
 
69
69
  7. Prepare to commit (when appropriate)
70
70
  - When the work is ready, produce a detailed commit message (do NOT actually commit unless your system explicitly allows it).