@pushpalsdev/cli 1.0.93 → 1.0.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +2 -2
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +207 -53
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +219 -130
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +57 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +138 -105
package/package.json
CHANGED
|
@@ -45,8 +45,8 @@ Execution policy:
|
|
|
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
47
|
- `scope.write_globs` should be included as starting-point/relevance hints, not as hard write boundaries
|
|
48
|
-
- `scope.forbidden_globs` should be included only
|
|
49
|
-
- `scope.max_files_to_edit` should be included only
|
|
48
|
+
- `scope.forbidden_globs` should be included only as review guardrail hints, not as hard write blockers
|
|
49
|
+
- `scope.max_files_to_edit` should be included only as a planning/review hint; WorkerPal write access is repo-sandbox-wide
|
|
50
50
|
|
|
51
51
|
Quality gates:
|
|
52
52
|
|
|
@@ -4264,6 +4264,9 @@ var IDEATION_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_ideation_s
|
|
|
4264
4264
|
var SCORING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_scoring_system_prompt.md").trim();
|
|
4265
4265
|
var PLANNING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_planning_system_prompt.md").trim();
|
|
4266
4266
|
var IDEATION_TIMEOUT_RECOVERY_INSTRUCTION = "Previous ideation timed out before you returned JSON. For this round only, stay within the time budget: prioritize the top 1-3 highest-confidence candidates, keep reasoning brief, avoid exhaustive exploration, and return valid JSON as soon as possible.";
|
|
4267
|
+
var STARTUP_FAST_TICK_MAX_ATTEMPTS = 4;
|
|
4268
|
+
var STARTUP_FAST_TICK_MAX_DELAY_MS = 15000;
|
|
4269
|
+
var STARTUP_STALE_LOCK_AFTER_MS = 30000;
|
|
4267
4270
|
var VISION_DOC_FNAME = "vision.md";
|
|
4268
4271
|
var MAX_VISION_SECTION_CHARS = 1200;
|
|
4269
4272
|
var DOCS_MIN_IMPACT_SIGNAL_FOR_NO_PENALTY = 0.45;
|
|
@@ -6100,6 +6103,92 @@ function buildEngineInspirationContext(params) {
|
|
|
6100
6103
|
commit_history_hints: commitHistoryHints
|
|
6101
6104
|
};
|
|
6102
6105
|
}
|
|
6106
|
+
function compactIdeationText(value, maxChars) {
|
|
6107
|
+
const text = asString2(value).trim();
|
|
6108
|
+
if (text.length <= maxChars)
|
|
6109
|
+
return text;
|
|
6110
|
+
return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}...`;
|
|
6111
|
+
}
|
|
6112
|
+
function compactIdeationTextList(values, maxItems, maxChars) {
|
|
6113
|
+
return values.slice(0, maxItems).map((value) => compactIdeationText(value, maxChars)).filter(Boolean);
|
|
6114
|
+
}
|
|
6115
|
+
function compactVisionContextForIdeationRetry(vision) {
|
|
6116
|
+
const compactKeyItems = Object.fromEntries(Object.entries(vision.key_items).map(([key, value]) => [
|
|
6117
|
+
key,
|
|
6118
|
+
Array.isArray(value) ? compactIdeationTextList(value, 6, 260) : compactIdeationText(value, 260)
|
|
6119
|
+
]));
|
|
6120
|
+
return {
|
|
6121
|
+
one_sentence: compactIdeationText(vision.one_sentence, 360),
|
|
6122
|
+
sections: vision.sections.slice(0, 4).map((section) => ({
|
|
6123
|
+
number: section.number,
|
|
6124
|
+
title: compactIdeationText(section.title, 160),
|
|
6125
|
+
markdown: compactIdeationText(section.markdown, 500),
|
|
6126
|
+
truncated: section.truncated || section.markdown.length > 500
|
|
6127
|
+
})),
|
|
6128
|
+
key_items: compactKeyItems,
|
|
6129
|
+
section_numbers: vision.section_numbers.slice(0, 8),
|
|
6130
|
+
truncated: vision.truncated
|
|
6131
|
+
};
|
|
6132
|
+
}
|
|
6133
|
+
function compactEngineInspirationForIdeationRetry(context) {
|
|
6134
|
+
return {
|
|
6135
|
+
compiled_repo_objectives: context.compiled_repo_objectives.slice(0, 4).map((objective) => ({
|
|
6136
|
+
id: objective.id,
|
|
6137
|
+
title: objective.title,
|
|
6138
|
+
weight: objective.weight,
|
|
6139
|
+
section_ref: objective.section_ref,
|
|
6140
|
+
category: objective.category,
|
|
6141
|
+
success_criteria: compactIdeationTextList(objective.success_criteria, 3, 220),
|
|
6142
|
+
validation_expectations: compactIdeationTextList(objective.validation_expectations, 3, 220)
|
|
6143
|
+
})),
|
|
6144
|
+
compiled_objectives: context.compiled_objectives.slice(0, 4).map((objective) => ({
|
|
6145
|
+
id: objective.id,
|
|
6146
|
+
title: compactIdeationText(objective.title, 220),
|
|
6147
|
+
weight: objective.weight,
|
|
6148
|
+
evidence: compactIdeationTextList(objective.evidence, 3, 220)
|
|
6149
|
+
})),
|
|
6150
|
+
opportunity_gaps: context.opportunity_gaps.slice(0, 4).map((gap) => ({
|
|
6151
|
+
id: gap.id,
|
|
6152
|
+
label: compactIdeationText(gap.label, 220),
|
|
6153
|
+
score: gap.score,
|
|
6154
|
+
evidence: compactIdeationTextList(gap.evidence, 3, 220)
|
|
6155
|
+
})),
|
|
6156
|
+
building_blocks: context.building_blocks.slice(0, 6).map((block) => ({
|
|
6157
|
+
id: block.id,
|
|
6158
|
+
algorithm: block.algorithm,
|
|
6159
|
+
summary: compactIdeationText(block.summary, 260),
|
|
6160
|
+
hypothesis: compactIdeationText(block.hypothesis, 260),
|
|
6161
|
+
score: block.score,
|
|
6162
|
+
objective_ids: block.objective_ids.slice(0, 3),
|
|
6163
|
+
gap_ids: block.gap_ids.slice(0, 3),
|
|
6164
|
+
candidate_shape: {
|
|
6165
|
+
objective_type: block.candidate_shape.objective_type,
|
|
6166
|
+
trigger_type: block.candidate_shape.trigger_type,
|
|
6167
|
+
component_area: block.candidate_shape.component_area,
|
|
6168
|
+
target_paths: block.candidate_shape.target_paths.slice(0, 4),
|
|
6169
|
+
write_globs: block.candidate_shape.write_globs.slice(0, 4)
|
|
6170
|
+
}
|
|
6171
|
+
})),
|
|
6172
|
+
source_patterns: context.source_patterns.slice(0, 4).map((pattern) => ({
|
|
6173
|
+
id: pattern.id,
|
|
6174
|
+
algorithm: pattern.algorithm,
|
|
6175
|
+
summary: compactIdeationText(pattern.summary, 260),
|
|
6176
|
+
tags: compactIdeationTextList(pattern.tags, 5, 80),
|
|
6177
|
+
quality_score: pattern.quality_score,
|
|
6178
|
+
freshness_score: pattern.freshness_score,
|
|
6179
|
+
source_trust_score: pattern.source_trust_score
|
|
6180
|
+
})),
|
|
6181
|
+
commit_history_hints: context.commit_history_hints.slice(0, 4).map((hint) => ({
|
|
6182
|
+
motif_id: hint.motif_id,
|
|
6183
|
+
label: compactIdeationText(hint.label, 220),
|
|
6184
|
+
count: hint.count,
|
|
6185
|
+
signal: hint.signal,
|
|
6186
|
+
objective_ids: hint.objective_ids.slice(0, 3),
|
|
6187
|
+
gap_ids: hint.gap_ids.slice(0, 3),
|
|
6188
|
+
sample_subjects: compactIdeationTextList(hint.sample_subjects, 3, 180)
|
|
6189
|
+
}))
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6103
6192
|
function selectVisionSectionRefs(sectionRefs) {
|
|
6104
6193
|
const preferred = ["6", "7", "8", "4", "3", "0", "5"];
|
|
6105
6194
|
const normalized = sectionRefs.map((value) => asString2(value)).filter(Boolean);
|
|
@@ -6199,7 +6288,7 @@ function buildRepoVisionFallbackCandidates(params) {
|
|
|
6199
6288
|
const target = chooseRepoObjectiveTargetProfile(params.repoTargets ?? [], objective);
|
|
6200
6289
|
const targetPaths = target?.target_paths ?? [objective.section_ref ? `vision.md` : "README.md"];
|
|
6201
6290
|
const writeGlobs = target?.write_globs ?? targetPaths;
|
|
6202
|
-
const componentArea = target?.component_area ??
|
|
6291
|
+
const componentArea = target?.component_area ?? normalizeAutonomyComponentArea(pathDirname(targetPaths[0]) || targetPaths[0]) ?? "docs";
|
|
6203
6292
|
const triggerType = categoryTriggerType(objective.category, params.snapshotTopSignals);
|
|
6204
6293
|
const signalIds = pickSignalIdsForTrigger(params.snapshotTopSignals, triggerType);
|
|
6205
6294
|
const sectionRef = objective.section_ref || sectionRefs[0] || "";
|
|
@@ -6409,9 +6498,11 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6409
6498
|
cfg;
|
|
6410
6499
|
runtimeEnabled = true;
|
|
6411
6500
|
timer = null;
|
|
6501
|
+
startupFastTickTimer = null;
|
|
6412
6502
|
heartbeatTimer = null;
|
|
6413
6503
|
inFlight = false;
|
|
6414
6504
|
nextTickAtMs = 0;
|
|
6505
|
+
startupFastTickAttemptsRemaining = 0;
|
|
6415
6506
|
currentRunId = null;
|
|
6416
6507
|
currentPhase = "idle";
|
|
6417
6508
|
currentPhaseStartedAtMs = 0;
|
|
@@ -6441,6 +6532,8 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6441
6532
|
this.runtimeEnabled = Boolean(enabled);
|
|
6442
6533
|
if (!this.runtimeEnabled) {
|
|
6443
6534
|
this.nextTickAtMs = 0;
|
|
6535
|
+
this.startupFastTickAttemptsRemaining = 0;
|
|
6536
|
+
this.clearStartupFastTickTimer();
|
|
6444
6537
|
if (!this.currentRunId) {
|
|
6445
6538
|
this.lastOutcome = "skipped";
|
|
6446
6539
|
this.lastDetail = "disabled_by_runtime_config";
|
|
@@ -6494,6 +6587,38 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6494
6587
|
lockStaleAfterMs() {
|
|
6495
6588
|
return Math.max(this.phaseTimeoutMs("ideation") + 30000, this.cfg.heartbeatLogMs * 2, 120000);
|
|
6496
6589
|
}
|
|
6590
|
+
startupLockStaleAfterMs() {
|
|
6591
|
+
return Math.min(this.lockStaleAfterMs(), Math.max(5000, Math.min(STARTUP_STALE_LOCK_AFTER_MS, Math.floor(this.cfg.tickIntervalMs / 4))));
|
|
6592
|
+
}
|
|
6593
|
+
lockStaleAfterMsForAcquire() {
|
|
6594
|
+
return this.startupFastTickAttemptsRemaining > 0 ? this.startupLockStaleAfterMs() : this.lockStaleAfterMs();
|
|
6595
|
+
}
|
|
6596
|
+
startupFastTickDelayMs() {
|
|
6597
|
+
return Math.max(1000, Math.min(STARTUP_FAST_TICK_MAX_DELAY_MS, Math.floor(this.cfg.tickIntervalMs / 10)));
|
|
6598
|
+
}
|
|
6599
|
+
clearStartupFastTickTimer() {
|
|
6600
|
+
if (this.startupFastTickTimer) {
|
|
6601
|
+
clearTimeout(this.startupFastTickTimer);
|
|
6602
|
+
this.startupFastTickTimer = null;
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
scheduleStartupFastTick(reason) {
|
|
6606
|
+
if (!this.runtimeEnabled || !this.timer || this.startupFastTickTimer)
|
|
6607
|
+
return;
|
|
6608
|
+
if (this.startupFastTickAttemptsRemaining <= 0)
|
|
6609
|
+
return;
|
|
6610
|
+
const delayMs = this.startupFastTickDelayMs();
|
|
6611
|
+
this.startupFastTickAttemptsRemaining -= 1;
|
|
6612
|
+
this.nextTickAtMs = Date.now() + delayMs;
|
|
6613
|
+
console.log(`[RemoteBuddyAutonomousEngine] startup fast tick scheduled in ${delayMs}ms after ${reason} (remaining=${this.startupFastTickAttemptsRemaining}).`);
|
|
6614
|
+
this.startupFastTickTimer = setTimeout(() => {
|
|
6615
|
+
this.startupFastTickTimer = null;
|
|
6616
|
+
if (!this.runtimeEnabled || !this.timer)
|
|
6617
|
+
return;
|
|
6618
|
+
this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
|
|
6619
|
+
this.tick();
|
|
6620
|
+
}, delayMs);
|
|
6621
|
+
}
|
|
6497
6622
|
cycleBudgetMs() {
|
|
6498
6623
|
const ideationTimeoutMs = this.phaseTimeoutMs("ideation");
|
|
6499
6624
|
const scoringTimeoutMs = this.phaseTimeoutMs("scoring");
|
|
@@ -6806,7 +6931,7 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6806
6931
|
sessionId: this.sessionId,
|
|
6807
6932
|
runId,
|
|
6808
6933
|
ttlMs,
|
|
6809
|
-
staleAfterMs: this.
|
|
6934
|
+
staleAfterMs: this.lockStaleAfterMsForAcquire()
|
|
6810
6935
|
})
|
|
6811
6936
|
});
|
|
6812
6937
|
if (res.ok)
|
|
@@ -7122,6 +7247,8 @@ ${JSON.stringify(input.messages ?? [])}`),
|
|
|
7122
7247
|
outcomeDetail = lockResult.reason ? compactStatusDetail(`lock_not_acquired:${lockResult.reason}`) : "lock_not_acquired";
|
|
7123
7248
|
return;
|
|
7124
7249
|
}
|
|
7250
|
+
this.startupFastTickAttemptsRemaining = 0;
|
|
7251
|
+
this.clearStartupFastTickTimer();
|
|
7125
7252
|
this.setPhase("prepare_worktree");
|
|
7126
7253
|
const ready = await this.ensureAutonomyRepoReady(runId);
|
|
7127
7254
|
if (!ready) {
|
|
@@ -7230,58 +7357,79 @@ ${JSON.stringify(input.messages ?? [])}`),
|
|
|
7230
7357
|
return;
|
|
7231
7358
|
}
|
|
7232
7359
|
this.setPhase("ideation");
|
|
7233
|
-
const
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7360
|
+
const buildIdeationInput = (ideationRecovery2, compactRetry) => {
|
|
7361
|
+
const reduced = compactRetry || Boolean(ideationRecovery2);
|
|
7362
|
+
const ideationTopSignals = snapshot.top_signals.slice(0, reduced ? 5 : 16);
|
|
7363
|
+
const ideationStateTraits = snapshot.state_traits.slice(0, reduced ? 6 : 24);
|
|
7364
|
+
const ideationFeedbackPriors = snapshot.feedback_priors.slice(0, reduced ? 4 : 20);
|
|
7365
|
+
const ideationEngineIdeaPriors = (snapshot.engine_idea_priors ?? []).slice(0, reduced ? 4 : 20);
|
|
7366
|
+
const ideationOpenObjectives = snapshot.open_objectives.slice(0, reduced ? 4 : 20);
|
|
7367
|
+
const ideationActiveCooldowns = snapshot.active_cooldowns.slice(0, reduced ? 4 : 20);
|
|
7368
|
+
const ideationRepoTargets = repoTargets.slice(0, reduced ? 4 : repoTargets.length);
|
|
7369
|
+
return {
|
|
7370
|
+
system: IDEATION_SYSTEM_PROMPT,
|
|
7371
|
+
json: true,
|
|
7372
|
+
maxTokens: reduced ? 900 : 2800,
|
|
7373
|
+
temperature: 0.2,
|
|
7374
|
+
messages: [
|
|
7375
|
+
...ideationRecovery2 ? [
|
|
7376
|
+
{
|
|
7377
|
+
role: "user",
|
|
7378
|
+
content: `${IDEATION_TIMEOUT_RECOVERY_INSTRUCTION} Previous timed-out run: ${ideationRecovery2.previousRunId}. Timeout budget for this round: ${this.phaseTimeoutMs("ideation")}ms.`
|
|
7379
|
+
}
|
|
7380
|
+
] : [],
|
|
7251
7381
|
{
|
|
7252
7382
|
role: "user",
|
|
7253
|
-
content:
|
|
7383
|
+
content: JSON.stringify({
|
|
7384
|
+
snapshot: {
|
|
7385
|
+
snapshot_id: snapshot.snapshot_id,
|
|
7386
|
+
top_signals: ideationTopSignals,
|
|
7387
|
+
state_traits: ideationStateTraits,
|
|
7388
|
+
feedback_priors: ideationFeedbackPriors,
|
|
7389
|
+
engine_idea_priors: ideationEngineIdeaPriors,
|
|
7390
|
+
open_objectives: ideationOpenObjectives,
|
|
7391
|
+
active_cooldowns: ideationActiveCooldowns
|
|
7392
|
+
},
|
|
7393
|
+
vision: reduced ? compactVisionContextForIdeationRetry(visionContext) : visionContext,
|
|
7394
|
+
repo_targets: ideationRepoTargets.map((target) => ({
|
|
7395
|
+
component_area: target.component_area,
|
|
7396
|
+
target_paths: target.target_paths,
|
|
7397
|
+
write_globs: target.write_globs,
|
|
7398
|
+
label: target.label,
|
|
7399
|
+
keywords: target.keywords.slice(0, reduced ? 4 : 8)
|
|
7400
|
+
})),
|
|
7401
|
+
engine_inspiration: reduced ? compactEngineInspirationForIdeationRetry(engineInspiration) : engineInspiration,
|
|
7402
|
+
limits: {
|
|
7403
|
+
ideation_max_candidates: reduced ? Math.max(1, Math.min(3, this.cfg.ideationMaxCandidates)) : this.cfg.ideationMaxCandidates,
|
|
7404
|
+
min_confidence: this.cfg.minConfidence
|
|
7405
|
+
}
|
|
7406
|
+
}, null, reduced ? 0 : 2)
|
|
7254
7407
|
}
|
|
7255
|
-
]
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
}
|
|
7281
|
-
}, null, 2)
|
|
7282
|
-
}
|
|
7283
|
-
]
|
|
7284
|
-
});
|
|
7408
|
+
]
|
|
7409
|
+
};
|
|
7410
|
+
};
|
|
7411
|
+
let ideationRecovery = this.consumeIdeationTimeoutRecovery();
|
|
7412
|
+
if (ideationRecovery) {
|
|
7413
|
+
console.warn(`[RemoteBuddyAutonomousEngine] tick ${runId}: applying one-shot ideation timeout recovery from ${ideationRecovery.previousRunId} after ${ideationRecovery.timeoutMs}ms timeout.`);
|
|
7414
|
+
}
|
|
7415
|
+
let ideationPhase;
|
|
7416
|
+
try {
|
|
7417
|
+
ideationPhase = await this.llmPhase("ideation", runId, snapshot.snapshot_id, buildIdeationInput(ideationRecovery, Boolean(ideationRecovery)));
|
|
7418
|
+
} catch (error) {
|
|
7419
|
+
if (error instanceof Error && error.message === "autonomy ideation phase timeout" && !ideationRecovery) {
|
|
7420
|
+
ideationRecovery = {
|
|
7421
|
+
previousRunId: runId,
|
|
7422
|
+
timedOutAt: new Date().toISOString(),
|
|
7423
|
+
timeoutMs: this.phaseTimeoutMs("ideation")
|
|
7424
|
+
};
|
|
7425
|
+
this.pendingIdeationTimeoutRecovery = null;
|
|
7426
|
+
console.warn(`[RemoteBuddyAutonomousEngine] tick ${runId}: ideation timed out; retrying once immediately with reduced context and budget-focused guidance.`);
|
|
7427
|
+
ideationPhase = await this.llmPhase("ideation", runId, snapshot.snapshot_id, buildIdeationInput(ideationRecovery, true));
|
|
7428
|
+
this.pendingIdeationTimeoutRecovery = null;
|
|
7429
|
+
} else {
|
|
7430
|
+
throw error;
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7285
7433
|
llmCalls.push(ideationPhase.llmCall);
|
|
7286
7434
|
const ideationJson = ideationPhase.json;
|
|
7287
7435
|
if (this.isSnapshotExpired(snapshot) || Date.now() > cycleDeadline) {
|
|
@@ -7907,6 +8055,9 @@ Scope:
|
|
|
7907
8055
|
await this.releaseDispatchLock(runId);
|
|
7908
8056
|
this.inFlight = false;
|
|
7909
8057
|
this.markTickDone(outcome, outcomeDetail);
|
|
8058
|
+
if (!lockAcquired && outcomeDetail.startsWith("lock_not_acquired")) {
|
|
8059
|
+
this.scheduleStartupFastTick("dispatch lock contention");
|
|
8060
|
+
}
|
|
7910
8061
|
}
|
|
7911
8062
|
}
|
|
7912
8063
|
async enqueueFromAnalysis(instruction, autonomyCtx, originRequestId) {
|
|
@@ -7931,7 +8082,8 @@ Scope:
|
|
|
7931
8082
|
if (!this.runtimeEnabled || this.timer)
|
|
7932
8083
|
return;
|
|
7933
8084
|
console.log(`[RemoteBuddyAutonomousEngine] Using dedicated autonomy worktree ${this.autonomyRepo} (remote=${this.gitRemote} integration=${this.integrationBranch} base=${this.baseBranch}).`);
|
|
7934
|
-
this.
|
|
8085
|
+
this.startupFastTickAttemptsRemaining = STARTUP_FAST_TICK_MAX_ATTEMPTS;
|
|
8086
|
+
this.nextTickAtMs = Date.now();
|
|
7935
8087
|
this.timer = setInterval(() => {
|
|
7936
8088
|
this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
|
|
7937
8089
|
this.tick();
|
|
@@ -7943,6 +8095,7 @@ Scope:
|
|
|
7943
8095
|
this.tick();
|
|
7944
8096
|
}
|
|
7945
8097
|
stop() {
|
|
8098
|
+
this.clearStartupFastTickTimer();
|
|
7946
8099
|
if (this.timer) {
|
|
7947
8100
|
clearInterval(this.timer);
|
|
7948
8101
|
this.timer = null;
|
|
@@ -7951,6 +8104,7 @@ Scope:
|
|
|
7951
8104
|
clearInterval(this.heartbeatTimer);
|
|
7952
8105
|
this.heartbeatTimer = null;
|
|
7953
8106
|
}
|
|
8107
|
+
this.startupFastTickAttemptsRemaining = 0;
|
|
7954
8108
|
this.nextTickAtMs = 0;
|
|
7955
8109
|
}
|
|
7956
8110
|
}
|
|
@@ -8281,7 +8435,7 @@ function buildExecutionGuidance(plan, targetPaths, requiredValidationSteps = [])
|
|
|
8281
8435
|
lines.push(`- ${glob}`);
|
|
8282
8436
|
}
|
|
8283
8437
|
if (Array.isArray(plan.scope.forbidden_globs) && plan.scope.forbidden_globs.length > 0) {
|
|
8284
|
-
lines.push("
|
|
8438
|
+
lines.push("Review guardrail hints:");
|
|
8285
8439
|
for (const glob of plan.scope.forbidden_globs)
|
|
8286
8440
|
lines.push(`- ${glob}`);
|
|
8287
8441
|
}
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
-
from shutil import which
|
|
13
|
+
from shutil import rmtree, which
|
|
14
14
|
import shlex
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess
|
|
@@ -21,7 +21,7 @@ import time
|
|
|
21
21
|
import traceback
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Any, Dict, List, Optional
|
|
24
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
25
25
|
|
|
26
26
|
_SHARED_DIR = Path(__file__).resolve().parents[1] / "shared"
|
|
27
27
|
if str(_SHARED_DIR) not in sys.path:
|
|
@@ -386,6 +386,87 @@ def _is_git_repo(repo: str) -> bool:
|
|
|
386
386
|
return False
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def _codex_project_config_roots(repo: str, env: Dict[str, str]) -> List[Path]:
|
|
390
|
+
roots: List[Path] = []
|
|
391
|
+
seen: set[str] = set()
|
|
392
|
+
|
|
393
|
+
def add(raw: object) -> None:
|
|
394
|
+
text = str(raw or "").strip()
|
|
395
|
+
if not text:
|
|
396
|
+
return
|
|
397
|
+
try:
|
|
398
|
+
path = Path(text).resolve()
|
|
399
|
+
except Exception:
|
|
400
|
+
return
|
|
401
|
+
key = str(path)
|
|
402
|
+
if key in seen:
|
|
403
|
+
return
|
|
404
|
+
seen.add(key)
|
|
405
|
+
roots.append(path)
|
|
406
|
+
|
|
407
|
+
add(repo)
|
|
408
|
+
try:
|
|
409
|
+
proc = subprocess.run(
|
|
410
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
411
|
+
cwd=repo,
|
|
412
|
+
capture_output=True,
|
|
413
|
+
text=True,
|
|
414
|
+
timeout=10,
|
|
415
|
+
check=False,
|
|
416
|
+
)
|
|
417
|
+
if proc.returncode == 0:
|
|
418
|
+
add((proc.stdout or "").strip())
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
for key in (
|
|
423
|
+
"PUSHPALS_REPO_ROOT_OVERRIDE",
|
|
424
|
+
"PUSHPALS_PROJECT_ROOT_OVERRIDE",
|
|
425
|
+
"PUSHPALS_ASSIGNED_REPO_ROOT",
|
|
426
|
+
"PUSHPALS_REPO_PATH",
|
|
427
|
+
):
|
|
428
|
+
add(env.get(key))
|
|
429
|
+
return roots
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _mask_repo_local_codex_files(repo: str, env: Dict[str, str]) -> List[Tuple[Path, Path]]:
|
|
433
|
+
masked: List[Tuple[Path, Path]] = []
|
|
434
|
+
for root in _codex_project_config_roots(repo, env):
|
|
435
|
+
codex_path = root / ".codex"
|
|
436
|
+
if not os.path.lexists(codex_path):
|
|
437
|
+
continue
|
|
438
|
+
if codex_path.is_dir():
|
|
439
|
+
continue
|
|
440
|
+
backup = root / f".codex.pushpals-masked-{os.getpid()}-{len(masked)}"
|
|
441
|
+
suffix = 0
|
|
442
|
+
while os.path.lexists(backup):
|
|
443
|
+
suffix += 1
|
|
444
|
+
backup = root / f".codex.pushpals-masked-{os.getpid()}-{len(masked)}-{suffix}"
|
|
445
|
+
try:
|
|
446
|
+
os.replace(codex_path, backup)
|
|
447
|
+
masked.append((codex_path, backup))
|
|
448
|
+
log.info(
|
|
449
|
+
f"Temporarily masked repo-local .codex file so Codex CLI can use CODEX_HOME: {codex_path}"
|
|
450
|
+
)
|
|
451
|
+
except Exception as exc:
|
|
452
|
+
log.warning(f"Failed to mask repo-local .codex file {codex_path}: {exc}")
|
|
453
|
+
return masked
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _restore_repo_local_codex_files(masked: List[Tuple[Path, Path]]) -> None:
|
|
457
|
+
for codex_path, backup in reversed(masked):
|
|
458
|
+
try:
|
|
459
|
+
if os.path.lexists(codex_path):
|
|
460
|
+
if codex_path.is_dir() and not codex_path.is_symlink():
|
|
461
|
+
rmtree(codex_path)
|
|
462
|
+
else:
|
|
463
|
+
codex_path.unlink()
|
|
464
|
+
if os.path.lexists(backup):
|
|
465
|
+
os.replace(backup, codex_path)
|
|
466
|
+
except Exception as exc:
|
|
467
|
+
log.warning(f"Failed to restore repo-local .codex file {codex_path}: {exc}")
|
|
468
|
+
|
|
469
|
+
|
|
389
470
|
def _resolve_codex_command_prefix(config: OpenAICodexRuntimeConfig) -> List[str]:
|
|
390
471
|
override_json = config.codex_bin_json
|
|
391
472
|
if override_json:
|
|
@@ -698,7 +779,7 @@ def _summarize_json_event(obj: Dict[str, Any]) -> str:
|
|
|
698
779
|
event_type = "event"
|
|
699
780
|
# Skip noisy streaming deltas unless they contain meaningful text fragments.
|
|
700
781
|
delta_like = event_type.endswith(".delta") or event_type.endswith("_delta")
|
|
701
|
-
# Reasoning/thinking events are always surfaced
|
|
782
|
+
# Reasoning/thinking events are always surfaced because they show the model's reasoning process.
|
|
702
783
|
reasoning_like = _contains_reasoning_marker(event_type) or _event_contains_reasoning(obj)
|
|
703
784
|
|
|
704
785
|
tool_name = ""
|
|
@@ -1487,7 +1568,11 @@ def _run_codex_task(
|
|
|
1487
1568
|
env.pop("OPENAI_API_KEY", None)
|
|
1488
1569
|
env.pop("OPENAI_BASE_URL", None)
|
|
1489
1570
|
env.pop("OPENAI_API_BASE", None)
|
|
1490
|
-
|
|
1571
|
+
codex_project_mask = _mask_repo_local_codex_files(repo, env)
|
|
1572
|
+
try:
|
|
1573
|
+
login_status = _run_codex_login_status(codex_cmd_prefix, repo, env)
|
|
1574
|
+
finally:
|
|
1575
|
+
_restore_repo_local_codex_files(codex_project_mask)
|
|
1491
1576
|
if not login_status.get("ok"):
|
|
1492
1577
|
detail = (
|
|
1493
1578
|
str(login_status.get("stderr") or "").strip()
|
|
@@ -1554,148 +1639,152 @@ def _run_codex_task(
|
|
|
1554
1639
|
if communicate_timeout_s:
|
|
1555
1640
|
log.debug(f"communicate timeout: {communicate_timeout_s}s")
|
|
1556
1641
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if chunk == "":
|
|
1586
|
-
break
|
|
1587
|
-
stdout_chunks.append(chunk)
|
|
1588
|
-
line = chunk.strip()
|
|
1589
|
-
if not line:
|
|
1590
|
-
continue
|
|
1591
|
-
with trace_lock:
|
|
1592
|
-
last_activity_at["ts"] = time.monotonic()
|
|
1593
|
-
_record_live_codex_stdout_line(line, use_json, stdout_trace_state)
|
|
1594
|
-
except Exception:
|
|
1595
|
-
pass
|
|
1596
|
-
finally:
|
|
1642
|
+
codex_project_mask = _mask_repo_local_codex_files(repo, env)
|
|
1643
|
+
try:
|
|
1644
|
+
proc = subprocess.Popen(
|
|
1645
|
+
cmd,
|
|
1646
|
+
cwd=repo,
|
|
1647
|
+
env=env,
|
|
1648
|
+
stdout=subprocess.PIPE,
|
|
1649
|
+
stderr=subprocess.PIPE,
|
|
1650
|
+
stdin=subprocess.PIPE,
|
|
1651
|
+
text=True,
|
|
1652
|
+
encoding="utf-8",
|
|
1653
|
+
errors="replace",
|
|
1654
|
+
)
|
|
1655
|
+
_ACTIVE_CHILD = proc
|
|
1656
|
+
started_at = time.monotonic()
|
|
1657
|
+
progress_interval_s = _resolve_progress_log_interval_seconds(runtime_config)
|
|
1658
|
+
|
|
1659
|
+
stdout_chunks: List[str] = []
|
|
1660
|
+
stderr_chunks: List[str] = []
|
|
1661
|
+
stdout_trace_state = _empty_codex_trace()
|
|
1662
|
+
trace_lock = threading.Lock()
|
|
1663
|
+
last_activity_at = {"ts": started_at}
|
|
1664
|
+
wrapper_rejection_state: Dict[str, Any] = {"count": 0, "commands": []}
|
|
1665
|
+
|
|
1666
|
+
def _drain_stdout() -> None:
|
|
1667
|
+
stream = proc.stdout
|
|
1668
|
+
if stream is None:
|
|
1669
|
+
return
|
|
1597
1670
|
try:
|
|
1598
|
-
stream.
|
|
1671
|
+
for chunk in iter(stream.readline, ""):
|
|
1672
|
+
if chunk == "":
|
|
1673
|
+
break
|
|
1674
|
+
stdout_chunks.append(chunk)
|
|
1675
|
+
line = chunk.strip()
|
|
1676
|
+
if not line:
|
|
1677
|
+
continue
|
|
1678
|
+
with trace_lock:
|
|
1679
|
+
last_activity_at["ts"] = time.monotonic()
|
|
1680
|
+
_record_live_codex_stdout_line(line, use_json, stdout_trace_state)
|
|
1599
1681
|
except Exception:
|
|
1600
1682
|
pass
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
|
|
1612
|
-
if rejected_commands:
|
|
1613
|
-
with trace_lock:
|
|
1614
|
-
wrapper_rejection_state["count"] = to_int(
|
|
1615
|
-
wrapper_rejection_state.get("count"), 0
|
|
1616
|
-
) + len(rejected_commands)
|
|
1617
|
-
tracked = wrapper_rejection_state.get("commands")
|
|
1618
|
-
if not isinstance(tracked, list):
|
|
1619
|
-
tracked = []
|
|
1620
|
-
for command in rejected_commands:
|
|
1621
|
-
lowered = command.lower()
|
|
1622
|
-
if any(str(item).lower() == lowered for item in tracked):
|
|
1623
|
-
continue
|
|
1624
|
-
tracked.append(command)
|
|
1625
|
-
wrapper_rejection_state["commands"] = tracked[:6]
|
|
1626
|
-
except Exception:
|
|
1627
|
-
pass
|
|
1628
|
-
finally:
|
|
1683
|
+
finally:
|
|
1684
|
+
try:
|
|
1685
|
+
stream.close()
|
|
1686
|
+
except Exception:
|
|
1687
|
+
pass
|
|
1688
|
+
|
|
1689
|
+
def _drain_stderr() -> None:
|
|
1690
|
+
stream = proc.stderr
|
|
1691
|
+
if stream is None:
|
|
1692
|
+
return
|
|
1629
1693
|
try:
|
|
1630
|
-
stream.
|
|
1694
|
+
for chunk in iter(stream.readline, ""):
|
|
1695
|
+
if chunk == "":
|
|
1696
|
+
break
|
|
1697
|
+
stderr_chunks.append(chunk)
|
|
1698
|
+
rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
|
|
1699
|
+
if rejected_commands:
|
|
1700
|
+
with trace_lock:
|
|
1701
|
+
wrapper_rejection_state["count"] = to_int(
|
|
1702
|
+
wrapper_rejection_state.get("count"), 0
|
|
1703
|
+
) + len(rejected_commands)
|
|
1704
|
+
tracked = wrapper_rejection_state.get("commands")
|
|
1705
|
+
if not isinstance(tracked, list):
|
|
1706
|
+
tracked = []
|
|
1707
|
+
for command in rejected_commands:
|
|
1708
|
+
lowered = command.lower()
|
|
1709
|
+
if any(str(item).lower() == lowered for item in tracked):
|
|
1710
|
+
continue
|
|
1711
|
+
tracked.append(command)
|
|
1712
|
+
wrapper_rejection_state["commands"] = tracked[:6]
|
|
1713
|
+
except Exception:
|
|
1714
|
+
pass
|
|
1715
|
+
finally:
|
|
1716
|
+
try:
|
|
1717
|
+
stream.close()
|
|
1718
|
+
except Exception:
|
|
1719
|
+
pass
|
|
1720
|
+
|
|
1721
|
+
stdout_thread = threading.Thread(target=_drain_stdout, daemon=True)
|
|
1722
|
+
stderr_thread = threading.Thread(target=_drain_stderr, daemon=True)
|
|
1723
|
+
stdout_thread.start()
|
|
1724
|
+
stderr_thread.start()
|
|
1725
|
+
|
|
1726
|
+
if proc.stdin is not None:
|
|
1727
|
+
try:
|
|
1728
|
+
proc.stdin.write(prompt)
|
|
1729
|
+
proc.stdin.close()
|
|
1631
1730
|
except Exception:
|
|
1632
1731
|
pass
|
|
1633
1732
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
proc.stdin.close()
|
|
1643
|
-
except Exception:
|
|
1644
|
-
pass
|
|
1645
|
-
|
|
1646
|
-
deadline = (
|
|
1647
|
-
started_at + float(communicate_timeout_s)
|
|
1648
|
-
if communicate_timeout_s and communicate_timeout_s > 0
|
|
1649
|
-
else None
|
|
1650
|
-
)
|
|
1651
|
-
next_progress_at = started_at + float(progress_interval_s)
|
|
1652
|
-
timed_out = False
|
|
1653
|
-
command_policy_rejection_loop = False
|
|
1654
|
-
|
|
1655
|
-
while proc.poll() is None:
|
|
1656
|
-
now = time.monotonic()
|
|
1657
|
-
if deadline is not None and now >= deadline:
|
|
1658
|
-
timed_out = True
|
|
1659
|
-
_terminate_active_child()
|
|
1660
|
-
break
|
|
1733
|
+
deadline = (
|
|
1734
|
+
started_at + float(communicate_timeout_s)
|
|
1735
|
+
if communicate_timeout_s and communicate_timeout_s > 0
|
|
1736
|
+
else None
|
|
1737
|
+
)
|
|
1738
|
+
next_progress_at = started_at + float(progress_interval_s)
|
|
1739
|
+
timed_out = False
|
|
1740
|
+
command_policy_rejection_loop = False
|
|
1661
1741
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1742
|
+
while proc.poll() is None:
|
|
1743
|
+
now = time.monotonic()
|
|
1744
|
+
if deadline is not None and now >= deadline:
|
|
1745
|
+
timed_out = True
|
|
1746
|
+
_terminate_active_child()
|
|
1747
|
+
break
|
|
1668
1748
|
|
|
1669
|
-
if now >= next_progress_at:
|
|
1670
|
-
elapsed = int(max(0.0, now - started_at))
|
|
1671
1749
|
with trace_lock:
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
)
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1750
|
+
wrapper_rejections = to_int(wrapper_rejection_state.get("count"), 0)
|
|
1751
|
+
if wrapper_rejections >= 3:
|
|
1752
|
+
command_policy_rejection_loop = True
|
|
1753
|
+
_terminate_active_child()
|
|
1754
|
+
break
|
|
1755
|
+
|
|
1756
|
+
if now >= next_progress_at:
|
|
1757
|
+
elapsed = int(max(0.0, now - started_at))
|
|
1758
|
+
with trace_lock:
|
|
1759
|
+
last_event = float(last_activity_at.get("ts", started_at))
|
|
1760
|
+
valid_json = to_int(stdout_trace_state.get("valid_json"), 0)
|
|
1761
|
+
total_lines = to_int(stdout_trace_state.get("line_count"), 0)
|
|
1762
|
+
idle_for = int(max(0.0, now - last_event))
|
|
1763
|
+
if use_json:
|
|
1764
|
+
log.info(
|
|
1765
|
+
f"codex exec still running ({elapsed}s elapsed, json_events={valid_json}, idle={idle_for}s)"
|
|
1766
|
+
)
|
|
1767
|
+
else:
|
|
1768
|
+
log.info(
|
|
1769
|
+
f"codex exec still running ({elapsed}s elapsed, stdout_lines={total_lines}, idle={idle_for}s)"
|
|
1770
|
+
)
|
|
1771
|
+
next_progress_at = now + float(progress_interval_s)
|
|
1685
1772
|
|
|
1686
|
-
|
|
1773
|
+
time.sleep(1.0)
|
|
1687
1774
|
|
|
1688
|
-
try:
|
|
1689
|
-
proc.wait(timeout=5)
|
|
1690
|
-
except Exception:
|
|
1691
1775
|
try:
|
|
1692
|
-
proc.kill()
|
|
1693
1776
|
proc.wait(timeout=5)
|
|
1694
1777
|
except Exception:
|
|
1695
|
-
|
|
1778
|
+
try:
|
|
1779
|
+
proc.kill()
|
|
1780
|
+
proc.wait(timeout=5)
|
|
1781
|
+
except Exception:
|
|
1782
|
+
pass
|
|
1696
1783
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1784
|
+
stdout_thread.join(timeout=2)
|
|
1785
|
+
stderr_thread.join(timeout=2)
|
|
1786
|
+
finally:
|
|
1787
|
+
_restore_repo_local_codex_files(codex_project_mask)
|
|
1699
1788
|
|
|
1700
1789
|
return_code = proc.returncode
|
|
1701
1790
|
_ACTIVE_CHILD = None
|
|
@@ -33,7 +33,9 @@ from openai_codex_executor import (
|
|
|
33
33
|
_detect_codex_workaround_signal,
|
|
34
34
|
_extract_usage_counts,
|
|
35
35
|
_load_prompt_template,
|
|
36
|
+
_mask_repo_local_codex_files,
|
|
36
37
|
_repo_root_for_prompt_loading,
|
|
38
|
+
_restore_repo_local_codex_files,
|
|
37
39
|
_resolve_codex_command_prefix,
|
|
38
40
|
_unwrap_shell_wrapper_command,
|
|
39
41
|
_usage_from_trace_or_estimate,
|
|
@@ -80,6 +82,61 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
80
82
|
self.assertEqual(cfg.reasoning_effort, "xhigh")
|
|
81
83
|
self.assertFalse(cfg.json_output)
|
|
82
84
|
|
|
85
|
+
def test_masks_and_restores_repo_local_codex_file(self) -> None:
|
|
86
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-") as root:
|
|
87
|
+
repo = Path(root) / "repo"
|
|
88
|
+
repo.mkdir()
|
|
89
|
+
codex_file = repo / ".codex"
|
|
90
|
+
codex_file.write_text("tracked repo sentinel\n", encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
masked = _mask_repo_local_codex_files(str(repo), {})
|
|
93
|
+
try:
|
|
94
|
+
self.assertFalse(codex_file.exists())
|
|
95
|
+
self.assertEqual(len(masked), 1)
|
|
96
|
+
self.assertTrue(masked[0][1].exists())
|
|
97
|
+
finally:
|
|
98
|
+
_restore_repo_local_codex_files(masked)
|
|
99
|
+
|
|
100
|
+
self.assertEqual(codex_file.read_text(encoding="utf-8"), "tracked repo sentinel\n")
|
|
101
|
+
|
|
102
|
+
def test_masks_project_root_override_codex_file_for_worktree_runs(self) -> None:
|
|
103
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-root-") as root:
|
|
104
|
+
project_root = Path(root) / "project"
|
|
105
|
+
worktree = project_root / ".worktrees" / "job-123"
|
|
106
|
+
worktree.mkdir(parents=True)
|
|
107
|
+
root_codex_file = project_root / ".codex"
|
|
108
|
+
worktree_codex_file = worktree / ".codex"
|
|
109
|
+
root_codex_file.write_text("root sentinel\n", encoding="utf-8")
|
|
110
|
+
worktree_codex_file.write_text("worktree sentinel\n", encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
masked = _mask_repo_local_codex_files(
|
|
113
|
+
str(worktree),
|
|
114
|
+
{"PUSHPALS_REPO_ROOT_OVERRIDE": str(project_root)},
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
self.assertFalse(root_codex_file.exists())
|
|
118
|
+
self.assertFalse(worktree_codex_file.exists())
|
|
119
|
+
self.assertEqual(len(masked), 2)
|
|
120
|
+
finally:
|
|
121
|
+
_restore_repo_local_codex_files(masked)
|
|
122
|
+
|
|
123
|
+
self.assertEqual(root_codex_file.read_text(encoding="utf-8"), "root sentinel\n")
|
|
124
|
+
self.assertEqual(worktree_codex_file.read_text(encoding="utf-8"), "worktree sentinel\n")
|
|
125
|
+
|
|
126
|
+
def test_does_not_mask_repo_local_codex_directory(self) -> None:
|
|
127
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-dir-") as root:
|
|
128
|
+
repo = Path(root) / "repo"
|
|
129
|
+
codex_dir = repo / ".codex"
|
|
130
|
+
codex_dir.mkdir(parents=True)
|
|
131
|
+
(codex_dir / "config.toml").write_text("[hooks]\n", encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
masked = _mask_repo_local_codex_files(str(repo), {})
|
|
134
|
+
try:
|
|
135
|
+
self.assertEqual(masked, [])
|
|
136
|
+
self.assertTrue((codex_dir / "config.toml").exists())
|
|
137
|
+
finally:
|
|
138
|
+
_restore_repo_local_codex_files(masked)
|
|
139
|
+
|
|
83
140
|
def test_reasoning_effort_defaults_to_extra_high_for_default_gpt_5_5(self) -> None:
|
|
84
141
|
cfg = OpenAICodexRuntimeConfig.from_sources(
|
|
85
142
|
SettingsResolver(env={}, config_loader=lambda: {}),
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
|
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
|
|
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
|
]
|