@pushpalsdev/cli 1.0.93 → 1.0.95

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.
@@ -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,11 +14,9 @@ import {
15
14
  extractVisionKeyItems,
16
15
  formatToolRequirement,
17
16
  matchesGlob,
18
- normalizeAutonomyComponentArea,
19
17
  normalizeTargetPath,
20
18
  requirementsForValidationCommand,
21
19
  sanitizeSourceControlIdentityField,
22
- type AutonomyComponentArea,
23
20
  type SourceControlCommitIdentity,
24
21
  type ToolRequirement,
25
22
  } from "shared";
@@ -388,13 +385,6 @@ export function shouldEnqueueNoChangeReviewCompletion(
388
385
  return extractReviewFixContext(params) == null;
389
386
  }
390
387
 
391
- function reviewAgentAllowsMultiRootScope(value: unknown): boolean {
392
- const normalized = String(value ?? "")
393
- .trim()
394
- .toLowerCase();
395
- return normalized === "review_fix" || normalized === "merge_conflict";
396
- }
397
-
398
388
  export function deriveQualityGatePolicy(
399
389
  params: Record<string, unknown> | null | undefined,
400
390
  runtimeConfig: WorkerpalsRuntimeConfig = DEFAULT_CONFIG,
@@ -1655,7 +1645,7 @@ async function runDeterministicQualityGate(
1655
1645
  if (scopedValidationFailure === "outside_task_scope") {
1656
1646
  onLog?.(
1657
1647
  "stderr",
1658
- "[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.",
1659
1649
  );
1660
1650
  }
1661
1651
 
@@ -2176,6 +2166,12 @@ export type WorkerGitCommitIdentity = SourceControlCommitIdentity;
2176
2166
 
2177
2167
  export const explicitWorkerCommitIdentityFromEnv = explicitSourceControlCommitIdentityFromEnv;
2178
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
+
2179
2175
  async function resolveGitConfigValue(repo: string, key: string): Promise<string> {
2180
2176
  const value = await git(repo, ["config", "--get", key]);
2181
2177
  return value.ok ? sanitizeSourceControlIdentityField(value.stdout) : "";
@@ -2292,19 +2288,21 @@ export async function createJobCommit(
2292
2288
  console.warn(
2293
2289
  `[WorkerPals] Stage target invalid/missing for ${job.kind}; retrying with fallback "git add -A".`,
2294
2290
  );
2295
- result = await git(repo, [
2296
- "add",
2297
- "-A",
2298
- "--",
2299
- ".",
2300
- ":(exclude)workspace/**",
2301
- ":(exclude)outputs/**",
2302
- ]);
2291
+ result = await git(repo, ["add", "-A"]);
2303
2292
  }
2304
2293
  if (!result.ok) {
2305
2294
  return { ok: false, error: `Failed to stage changes: ${result.stderr || result.stdout}` };
2306
2295
  }
2307
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
+ }
2308
2306
 
2309
2307
  // Check if there are changes to commit
2310
2308
  result = await git(repo, ["diff", "--cached", "--quiet"]);
@@ -2504,16 +2502,7 @@ function buildStageTargets(kind: string, params?: Record<string, unknown>): stri
2504
2502
 
2505
2503
  export function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
2506
2504
  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
- ];
2505
+ return ["add", "-A"];
2517
2506
  }
2518
2507
  const targets = buildStageTargets(kind, params);
2519
2508
  if (targets.length === 0) {
@@ -3058,25 +3047,11 @@ export async function resumePreparedMergeConflictRebase(
3058
3047
  "stdout",
3059
3048
  `[MergeConflict] Stage target invalid/missing for ${kind}; retrying with fallback "git add -A".`,
3060
3049
  );
3061
- stageResult = await git(repo, [
3062
- "add",
3063
- "-A",
3064
- "--",
3065
- ".",
3066
- ":(exclude)workspace/**",
3067
- ":(exclude)outputs/**",
3068
- ]);
3050
+ stageResult = await git(repo, ["add", "-A"]);
3069
3051
  }
3070
3052
  }
3071
3053
  } else {
3072
- stageResult = await git(repo, [
3073
- "add",
3074
- "-A",
3075
- "--",
3076
- ".",
3077
- ":(exclude)workspace/**",
3078
- ":(exclude)outputs/**",
3079
- ]);
3054
+ stageResult = await git(repo, ["add", "-A"]);
3080
3055
  }
3081
3056
  if (!stageResult.ok) {
3082
3057
  return {
@@ -3086,6 +3061,15 @@ export async function resumePreparedMergeConflictRebase(
3086
3061
  combinedGitOutput(stageResult),
3087
3062
  };
3088
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
+ }
3089
3073
 
3090
3074
  let rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
3091
3075
  let continueOutput = combinedGitOutput(rebaseContinue);
@@ -3235,19 +3219,19 @@ async function createMergeConflictJobCommit(
3235
3219
  console.warn(
3236
3220
  `[WorkerPals] Stage target invalid/missing for merge-conflict job ${job.id}; retrying with fallback "git add -A".`,
3237
3221
  );
3238
- result = await git(repo, [
3239
- "add",
3240
- "-A",
3241
- "--",
3242
- ".",
3243
- ":(exclude)workspace/**",
3244
- ":(exclude)outputs/**",
3245
- ]);
3222
+ result = await git(repo, ["add", "-A"]);
3246
3223
  }
3247
3224
  if (!result.ok) {
3248
3225
  return { ok: false, error: `Failed to stage merge-conflict changes: ${result.stderr || result.stdout}` };
3249
3226
  }
3250
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
+ }
3251
3235
 
3252
3236
  const cachedDiffQuiet = await git(repo, ["diff", "--cached", "--quiet"]);
3253
3237
  let headSha = await currentRefSha(repo, "HEAD");
@@ -3610,6 +3594,85 @@ export function shouldUseCodexCliForExecutor(executor: string): boolean {
3610
3594
  return executor.trim().toLowerCase() === "openai_codex";
3611
3595
  }
3612
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
+
3613
3676
  function normalizeCodexReasoningEffort(
3614
3677
  value: unknown,
3615
3678
  model = "",
@@ -3720,11 +3783,13 @@ async function generateCommitMessageFromDiffViaCodex(
3720
3783
  if (model) cmd.push("-m", model);
3721
3784
  cmd.push("-");
3722
3785
 
3786
+ const env = buildWorkerSandboxWritableEnv(repo);
3787
+ const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
3723
3788
  try {
3724
3789
  const stdinText = `${prompt.systemPrompt}\n\n${prompt.userMessage}`;
3725
3790
  const proc = Bun.spawn(cmd, {
3726
3791
  cwd: repo,
3727
- env: buildWorkerSandboxWritableEnv(repo),
3792
+ env,
3728
3793
  stdout: "pipe",
3729
3794
  stderr: "pipe",
3730
3795
  stdin: new Blob([stdinText]),
@@ -3759,6 +3824,7 @@ async function generateCommitMessageFromDiffViaCodex(
3759
3824
  } catch {
3760
3825
  return null;
3761
3826
  } finally {
3827
+ restoreRepoLocalCodexFilesForCodexCli(codexMask);
3762
3828
  try {
3763
3829
  unlinkSync(tmpOutputPath);
3764
3830
  } catch {
@@ -3938,9 +4004,7 @@ function hasInvalidRepoPathHint(values: string[]): boolean {
3938
4004
  return values.some((entry) => normalizeStagePath(entry) === null);
3939
4005
  }
3940
4006
 
3941
- function asAutonomyComponentArea(value: unknown): AutonomyComponentArea | null {
3942
- return normalizeAutonomyComponentArea(value);
3943
- }
4007
+ const SANDBOX_STAGE_ARTIFACT_PATHS = ["workspace", "outputs", ".codex"];
3944
4008
 
3945
4009
  function taskExecuteOrigin(params: Record<string, unknown>): "autonomy" | "user" {
3946
4010
  const explicit = String(params.origin ?? "")
@@ -3961,20 +4025,11 @@ export function collectWriteScopeIssuesFromChangedPaths(
3961
4025
  changedPaths: string[],
3962
4026
  planning: TaskExecutePlanning,
3963
4027
  ): string[] {
3964
- const normalizedChangedPaths = changedPaths
3965
- .map((entry) => normalizeStagePath(entry))
3966
- .filter((entry): entry is string => Boolean(entry) && entry !== ".");
3967
- if (normalizedChangedPaths.length === 0) return [];
3968
-
3969
- const forbidden = toStringArray(planning.scope.forbiddenGlobs ?? []);
3970
- const issues: string[] = [];
3971
- const forbiddenTouched = normalizedChangedPaths.filter((path) =>
3972
- forbidden.some((glob) => matchesGlob(path, glob)),
3973
- );
3974
- if (forbiddenTouched.length > 0) {
3975
- issues.push(`modified paths matching forbiddenGlobs: ${forbiddenTouched.join(", ")}`);
3976
- }
3977
- 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 [];
3978
4033
  }
3979
4034
 
3980
4035
  function sanitizeTaskExecutePlanningPathHints(value: unknown): unknown {
@@ -4096,37 +4151,6 @@ function validateTaskExecutePlanning(
4096
4151
  message: "task.execute planning.targetPaths must contain literal repo-relative paths",
4097
4152
  };
4098
4153
  }
4099
- const normalizedWriteGlobs = isStringArray(scope.writeGlobs)
4100
- ? toStringArray(scope.writeGlobs)
4101
- : [];
4102
- const allowMultiRootAutonomyScope =
4103
- origin === "autonomy" &&
4104
- reviewAgentAllowsMultiRootScope(options?.reviewAgentResolutionType);
4105
- if (origin === "autonomy") {
4106
- const declaredComponentArea = asAutonomyComponentArea(options?.autonomyComponentArea);
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
- }
4118
- }
4119
- } else if (normalizedWriteGlobs.length > 0) {
4120
- const uncoveredPaths = normalizedTargetPaths.filter(
4121
- (targetPath) => !normalizedWriteGlobs.some((glob) => matchesGlob(targetPath, glob)),
4122
- );
4123
- if (uncoveredPaths.length > 0) {
4124
- return {
4125
- ok: false,
4126
- message: `task.execute planning.targetPaths must be covered by planning.scope.writeGlobs: ${uncoveredPaths.join(", ")}`,
4127
- };
4128
- }
4129
- }
4130
4154
  }
4131
4155
 
4132
4156
  if (planning.discovery !== undefined) {
@@ -4353,10 +4377,12 @@ async function runCodexCriticReview(
4353
4377
  "-",
4354
4378
  ];
4355
4379
 
4380
+ const env = buildWorkerSandboxWritableEnv(repo);
4381
+ const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
4356
4382
  try {
4357
4383
  const proc = Bun.spawn(cmd, {
4358
4384
  cwd: repo,
4359
- env: buildWorkerSandboxWritableEnv(repo),
4385
+ env,
4360
4386
  stdout: "pipe",
4361
4387
  stderr: "pipe",
4362
4388
  stdin: new Blob([criticInstruction]),
@@ -4436,6 +4462,13 @@ async function runCodexCriticReview(
4436
4462
  } catch (err) {
4437
4463
  onLog?.("stderr", `[CriticGate] Codex error: ${toSingleLine(err, 220)} (skipping).`);
4438
4464
  return null;
4465
+ } finally {
4466
+ restoreRepoLocalCodexFilesForCodexCli(codexMask);
4467
+ try {
4468
+ unlinkSync(tmpOutputPath);
4469
+ } catch {
4470
+ /* ignore */
4471
+ }
4439
4472
  }
4440
4473
  }
4441
4474
 
@@ -4750,7 +4783,7 @@ export async function executeJob(
4750
4783
  [
4751
4784
  result.stderr ?? "",
4752
4785
  validationOutsideTaskScope
4753
- ? "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."
4754
4787
  : "",
4755
4788
  ...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
4756
4789
  ]