jules-orchestrator-kit 0.8.0 → 0.8.5
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/.agent/rules/dynamic-guardrails.json +0 -4
- package/.env.example +3 -0
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +1 -1
- package/README.md +95 -14
- package/bin/init.js +4 -1
- package/index.mjs +5 -4
- package/package.json +12 -5
- package/scripts/command-resolver.mjs +17 -17
- package/scripts/jules-cleanup.mjs +15 -25
- package/scripts/jules-create.mjs +10 -5
- package/scripts/jules-dispatch.mjs +163 -63
- package/scripts/jules-merge-swarm.mjs +184 -0
- package/scripts/jules-nightly.mjs +11 -7
- package/scripts/jules-patch.mjs +111 -0
- package/scripts/jules-queue-runner.mjs +39 -15
- package/scripts/jules-scan-todos.mjs +1 -1
- package/scripts/jules-self-audit.mjs +137 -48
- package/scripts/jules-status.mjs +52 -47
- package/scripts/jules-swarm.mjs +138 -39
- package/scripts/lock-manager.mjs +17 -10
- package/scripts/utils.mjs +154 -8
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { loadEnv, ensureSdkCacheIsolation, log } from "./utils.mjs";
|
|
4
|
+
|
|
5
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export async function fetchSessionPatch(sessionId, options = {}) {
|
|
8
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
9
|
+
throw new Error("Session ID is required.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const apiKey = options.apiKey || process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
|
|
13
|
+
const apiUrlBase = options.apiUrl || process.env.JULES_API_URL || "https://jules.googleapis.com/v1alpha/sessions";
|
|
14
|
+
|
|
15
|
+
// Normalize session ID (handles full resource name "sessions/123" or short ID "123")
|
|
16
|
+
const sessionPath = sessionId.startsWith("sessions/") ? sessionId : `sessions/${sessionId}`;
|
|
17
|
+
|
|
18
|
+
if (apiKey) {
|
|
19
|
+
const targetUrl = apiUrlBase.endsWith(`/${sessionPath}`)
|
|
20
|
+
? apiUrlBase
|
|
21
|
+
: `${apiUrlBase.replace(/\/sessions\/?$/, "")}/${sessionPath}`;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(targetUrl, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
"X-Goog-Api-Key": apiKey,
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
signal: AbortSignal.timeout(15000)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
const outputs = data.outputs || [];
|
|
36
|
+
const changeSetOutput = outputs.find((o) => o.type === "changeSet" || o.changeSet);
|
|
37
|
+
const changeSet = changeSetOutput?.changeSet;
|
|
38
|
+
const diff = changeSet?.gitPatch?.unidiffPatch || "";
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
sessionId: data.sessionId || sessionId,
|
|
42
|
+
state: data.state || "UNKNOWN",
|
|
43
|
+
title: data.title || "",
|
|
44
|
+
hasPatch: !!diff,
|
|
45
|
+
diff
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log.warn(`⚠️ REST API fetch failed (${err.message}), attempting SDK fallback...`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback to @google/jules-sdk if available
|
|
54
|
+
try {
|
|
55
|
+
const { jules } = await import("@google/jules-sdk");
|
|
56
|
+
const rawId = sessionId.replace(/^sessions\//, "");
|
|
57
|
+
const info = await jules.session(rawId).info();
|
|
58
|
+
const outputs = info.outputs || [];
|
|
59
|
+
const changeSet = outputs.find((o) => o.type === "changeSet" || o.changeSet)?.changeSet;
|
|
60
|
+
const diff = changeSet?.gitPatch?.unidiffPatch || "";
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
sessionId: info.id || rawId,
|
|
64
|
+
state: info.state || "UNKNOWN",
|
|
65
|
+
title: info.title || "",
|
|
66
|
+
hasPatch: !!diff,
|
|
67
|
+
diff
|
|
68
|
+
};
|
|
69
|
+
} catch (sdkErr) {
|
|
70
|
+
if (sdkErr.code === "ERR_MODULE_NOT_FOUND" || sdkErr.message?.includes("Cannot find package")) {
|
|
71
|
+
throw new Error("No API key configured and @google/jules-sdk is not installed. Set JULES_API_KEY or install @google/jules-sdk.");
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Failed to retrieve session patch: ${sdkErr.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isMainModule) {
|
|
78
|
+
loadEnv();
|
|
79
|
+
ensureSdkCacheIsolation();
|
|
80
|
+
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
const isJson = args.includes("--json");
|
|
83
|
+
const filteredArgs = args.filter((arg) => !arg.startsWith("--"));
|
|
84
|
+
const sessionId = filteredArgs[0];
|
|
85
|
+
|
|
86
|
+
if (!sessionId || args.includes("--help") || args.includes("-h")) {
|
|
87
|
+
console.error("Usage: node scripts/jules-patch.mjs <session_id> [--json]");
|
|
88
|
+
console.error("Extracts raw git unidiff patch from a Jules session for headless CI (git apply).");
|
|
89
|
+
process.exit(args.includes("--help") || args.includes("-h") ? 0 : 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fetchSessionPatch(sessionId)
|
|
93
|
+
.then((result) => {
|
|
94
|
+
if (isJson) {
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!result.hasPatch || !result.diff) {
|
|
100
|
+
console.error(`::warning::No git patch found in session outputs for session: ${sessionId}`);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.stdout.write(result.diff);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
})
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
console.error(`::error::Failed to extract patch for session ${sessionId}: ${err.message}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -97,14 +97,17 @@ async function safeMoveAsync(src, dest) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function classifyQueueFailure(error) {
|
|
100
|
+
export function classifyQueueFailure(error) {
|
|
101
101
|
const message = String(error?.stderr || error?.stdout || error?.message || "");
|
|
102
|
+
const code = error?.code;
|
|
102
103
|
|
|
103
|
-
if (/RESTRICTED FILE VIOLATION|COMMAND FILE CHANGE DETECTED|SECRET LEAK PREVENTED|AGENT RULE FILE CHANGE DETECTED/i.test(message)) return "security_violation";
|
|
104
|
-
if (/DIFF PAYLOAD TOO LARGE/i.test(message)) return "diff_too_large";
|
|
105
|
-
if (/
|
|
106
|
-
if (/
|
|
104
|
+
if (code === 6 || /RESTRICTED FILE VIOLATION|COMMAND FILE CHANGE DETECTED|SECRET LEAK PREVENTED|AGENT RULE FILE CHANGE DETECTED|Security violation/i.test(message)) return "security_violation";
|
|
105
|
+
if (code === 5 || /DIFF PAYLOAD TOO LARGE|DIFF TOO LARGE/i.test(message)) return "diff_too_large";
|
|
106
|
+
if (code === 8 || /lock contention|lock busy/i.test(message)) return "budget_locked";
|
|
107
|
+
if (code === 7 || /Daily budget exhausted|budget exhausted/i.test(message)) return "budget_exhausted";
|
|
108
|
+
if (/HTTP 429|Rate Limit Exceeded|FAILED_PRECONDITION|Active Session Limit/i.test(message)) return "concurrency_limit";
|
|
107
109
|
if (/HTTP 5\d\d/i.test(message)) return "api_error";
|
|
110
|
+
if (/command not found|ENOENT|CLI|execFile|spawn|exit code/i.test(message)) return "cli_error";
|
|
108
111
|
return "unknown";
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -131,6 +134,7 @@ function setTaskState(filename, stateObj) {
|
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
async function runQueue() {
|
|
137
|
+
let permanentFailuresCount = 0;
|
|
134
138
|
for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
|
|
135
139
|
const batch = files.slice(i, i + MAX_CONCURRENT);
|
|
136
140
|
await Promise.allSettled(
|
|
@@ -183,25 +187,45 @@ async function runQueue() {
|
|
|
183
187
|
logQueueState(file, "FAILED", error);
|
|
184
188
|
|
|
185
189
|
const failureClass = classifyQueueFailure(error);
|
|
186
|
-
const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"
|
|
190
|
+
const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"]);
|
|
187
191
|
|
|
188
192
|
const taskState = getTaskState(file);
|
|
189
|
-
const attempts = (taskState.attempts || 0) + 1;
|
|
190
193
|
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
if (failureClass === "budget_exhausted") {
|
|
195
|
+
setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
196
|
+
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
197
|
+
log.warn(`Task [${title}] deferred due to daily budget limit. Left in queue for next budget reset.`);
|
|
198
|
+
} else if (failureClass === "budget_locked") {
|
|
199
|
+
setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_LOCK_BUSY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
200
|
+
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
201
|
+
log.warn(`Task [${title}] lock contention (busy). Re-queued for retry.`);
|
|
202
|
+
} else if (failureClass === "concurrency_limit") {
|
|
203
|
+
setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_CONCURRENCY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
198
204
|
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
199
|
-
log.warn(`Task [${title}]
|
|
205
|
+
log.warn(`Task [${title}] active session quota (~30) reached. Pausing 15s before re-queuing.`);
|
|
206
|
+
await sleep(15000);
|
|
207
|
+
} else {
|
|
208
|
+
const attempts = (taskState.attempts || 0) + 1;
|
|
209
|
+
if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
|
|
210
|
+
permanentFailuresCount++;
|
|
211
|
+
const destPath = path.join(failedDir, file);
|
|
212
|
+
setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
213
|
+
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
214
|
+
log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
|
|
215
|
+
} else {
|
|
216
|
+
setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
217
|
+
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
218
|
+
log.warn(`Task [${title}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
|
|
219
|
+
}
|
|
200
220
|
}
|
|
201
221
|
}
|
|
202
222
|
})
|
|
203
223
|
);
|
|
204
224
|
}
|
|
225
|
+
if (permanentFailuresCount > 0) {
|
|
226
|
+
log.error(`Queue processing complete with ${permanentFailuresCount} failed task(s).`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
205
229
|
log.header("Queue processing complete!");
|
|
206
230
|
}
|
|
207
231
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Suggested Tasks Scanner for Google Jules
|
|
4
4
|
* Scans codebase for TODO, FIXME, HACK, and OPTIMIZE comments, ranks them by priority,
|
|
5
5
|
* and generates a tasks JSON file formatted for jules-swarm or jules-dispatch.
|
|
6
|
-
* Zero
|
|
6
|
+
* Zero runtime dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from "node:fs";
|
|
@@ -5,11 +5,15 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
|
|
8
|
-
import { log,
|
|
8
|
+
import { log, hasHighConfidenceSecret, hasLowConfidenceSecret, redactSecrets, anonymizePii } from "./utils.mjs";
|
|
9
9
|
|
|
10
10
|
function runGitCommand(args, ignoreError = false) {
|
|
11
11
|
try {
|
|
12
|
-
return execFileSync("git", args, {
|
|
12
|
+
return execFileSync("git", args, {
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
15
|
+
stdio: ["pipe", "pipe", ignoreError ? "ignore" : "pipe"],
|
|
16
|
+
}).trim();
|
|
13
17
|
} catch (error) {
|
|
14
18
|
if (ignoreError) return "";
|
|
15
19
|
const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
|
|
@@ -137,7 +141,8 @@ export function loadForbiddenPatterns(configContent = "") {
|
|
|
137
141
|
".github/**",
|
|
138
142
|
"**/secrets/**",
|
|
139
143
|
"**/*.pem",
|
|
140
|
-
"
|
|
144
|
+
"**/.env*",
|
|
145
|
+
"**/lock-manager*",
|
|
141
146
|
"scripts/jules-*",
|
|
142
147
|
".agent/jules.yml"
|
|
143
148
|
];
|
|
@@ -160,10 +165,12 @@ export function loadForbiddenPatterns(configContent = "") {
|
|
|
160
165
|
continue;
|
|
161
166
|
}
|
|
162
167
|
if (inForbidden) {
|
|
163
|
-
const
|
|
168
|
+
const lineWithoutComment = line.split("#")[0];
|
|
169
|
+
const trimmed = lineWithoutComment.trim();
|
|
164
170
|
if (trimmed.startsWith("-")) {
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
const val = trimmed.slice(1).trim().replace(/^["']|["']$/g, "");
|
|
172
|
+
if (val) blockParsed.push(val);
|
|
173
|
+
} else if (trimmed) {
|
|
167
174
|
break;
|
|
168
175
|
}
|
|
169
176
|
}
|
|
@@ -201,6 +208,50 @@ export function loadAllowedPatterns(configContent = "") {
|
|
|
201
208
|
return blockParsed;
|
|
202
209
|
}
|
|
203
210
|
|
|
211
|
+
export function validateJulesConfig(configContent = "", jsonGuardrailsContent = "") {
|
|
212
|
+
const errors = [];
|
|
213
|
+
|
|
214
|
+
if (configContent) {
|
|
215
|
+
const forbidden = loadForbiddenPatterns(configContent);
|
|
216
|
+
const allowed = loadAllowedPatterns(configContent);
|
|
217
|
+
const allPatterns = [...forbidden, ...allowed];
|
|
218
|
+
|
|
219
|
+
for (const pat of allPatterns) {
|
|
220
|
+
if (typeof pat !== "string" || !pat.trim()) {
|
|
221
|
+
errors.push("Empty or non-string glob pattern found in configuration.");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (jsonGuardrailsContent) {
|
|
227
|
+
try {
|
|
228
|
+
const data = JSON.parse(jsonGuardrailsContent);
|
|
229
|
+
if (Array.isArray(data.rules)) {
|
|
230
|
+
for (let i = 0; i < data.rules.length; i++) {
|
|
231
|
+
const rule = data.rules[i];
|
|
232
|
+
if (rule.trigger) {
|
|
233
|
+
try {
|
|
234
|
+
new RegExp(rule.trigger, "i");
|
|
235
|
+
} catch (reErr) {
|
|
236
|
+
errors.push(`Invalid RegExp trigger at rule index ${i} ('${rule.trigger}'): ${reErr.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (jsonErr) {
|
|
242
|
+
errors.push(`Invalid JSON syntax in dynamic-guardrails.json: ${jsonErr.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (errors.length > 0) {
|
|
247
|
+
const err = new Error(`CONFIG VALIDATION ERROR:\n` + errors.map((e) => ` - ${e}`).join("\n"));
|
|
248
|
+
err.code = 3;
|
|
249
|
+
return { ok: false, errors, err };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { ok: true, errors: [] };
|
|
253
|
+
}
|
|
254
|
+
|
|
204
255
|
export function parseAndCleanStderr(stderrStr) {
|
|
205
256
|
if (!stderrStr) return "";
|
|
206
257
|
const clean = stderrStr.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
@@ -208,6 +259,22 @@ export function parseAndCleanStderr(stderrStr) {
|
|
|
208
259
|
return lines.slice(-100).join("\n");
|
|
209
260
|
}
|
|
210
261
|
|
|
262
|
+
export function getOodaStateFile(mergeBase = "main") {
|
|
263
|
+
const oodaDir = path.resolve(process.cwd(), ".agent/state/ooda");
|
|
264
|
+
if (!fs.existsSync(oodaDir)) fs.mkdirSync(oodaDir, { recursive: true });
|
|
265
|
+
|
|
266
|
+
const keyInput = [
|
|
267
|
+
process.env.GITHUB_REPOSITORY || "",
|
|
268
|
+
process.env.GITHUB_REF || "",
|
|
269
|
+
process.env.GITHUB_HEAD_REF || "",
|
|
270
|
+
mergeBase,
|
|
271
|
+
process.cwd()
|
|
272
|
+
].join("\u0000");
|
|
273
|
+
|
|
274
|
+
const key = crypto.createHash("sha256").update(keyInput).digest("hex").slice(0, 16);
|
|
275
|
+
return path.join(oodaDir, `${key}.json`);
|
|
276
|
+
}
|
|
277
|
+
|
|
211
278
|
export function logAuditMetrics(metrics) {
|
|
212
279
|
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
213
280
|
if (!fs.existsSync(historyDir)) {
|
|
@@ -275,7 +342,9 @@ export function runSelfAudit() {
|
|
|
275
342
|
|
|
276
343
|
if (commandFileChanges.length > 0 && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "true" && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "1") {
|
|
277
344
|
const err = new Error(
|
|
278
|
-
"COMMAND FILE CHANGE DETECTED: " + commandFileChanges.join(", ") + ".
|
|
345
|
+
"COMMAND / BUILD CONFIG FILE CHANGE DETECTED: [" + commandFileChanges.join(", ") + "]. " +
|
|
346
|
+
"Refusing to execute verification scripts from untrusted branch. " +
|
|
347
|
+
"To override for legitimate dependency updates, set JULES_ALLOW_COMMAND_FILE_CHANGES=true."
|
|
279
348
|
);
|
|
280
349
|
err.code = 3;
|
|
281
350
|
throw err;
|
|
@@ -287,7 +356,9 @@ export function runSelfAudit() {
|
|
|
287
356
|
|
|
288
357
|
if (agentRuleChanges.length > 0 && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "true" && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "1") {
|
|
289
358
|
const err = new Error(
|
|
290
|
-
"AGENT RULE FILE CHANGE DETECTED: " + agentRuleChanges.join(", ") + ".
|
|
359
|
+
"AGENT RULE FILE CHANGE DETECTED: [" + agentRuleChanges.join(", ") + "]. " +
|
|
360
|
+
"Refusing to load agent rules from untrusted branch. " +
|
|
361
|
+
"To override for legitimate rule updates, set JULES_ALLOW_AGENT_RULE_CHANGES=true."
|
|
291
362
|
);
|
|
292
363
|
err.code = 3;
|
|
293
364
|
throw err;
|
|
@@ -302,23 +373,44 @@ export function runSelfAudit() {
|
|
|
302
373
|
// Fail closed: Load security config exclusively from base branch (mainRef).
|
|
303
374
|
// Never fall back to working-tree config which an untrusted PR could craft.
|
|
304
375
|
const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
|
|
376
|
+
const trustedJson = runGitCommand(["show", `${mainRef}:.agent/rules/dynamic-guardrails.json`], true);
|
|
377
|
+
|
|
378
|
+
const validationRes = validateJulesConfig(trustedConfig, trustedJson);
|
|
379
|
+
if (!validationRes.ok) {
|
|
380
|
+
log.error(`Config validation failed: ${validationRes.err.message}`);
|
|
381
|
+
throw validationRes.err;
|
|
382
|
+
}
|
|
383
|
+
|
|
305
384
|
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
306
385
|
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
307
386
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
387
|
+
const violationsWithDetails = [];
|
|
388
|
+
rawDiffFiles.forEach((file) => {
|
|
389
|
+
const matchedForbidden = forbiddenPatterns.find((pattern) => matchGlob(file, pattern));
|
|
390
|
+
if (matchedForbidden) {
|
|
391
|
+
violationsWithDetails.push(`${file} (matched forbidden pattern '${matchedForbidden}')`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
311
394
|
if (allowedPatterns.length > 0) {
|
|
312
395
|
const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
|
|
313
|
-
|
|
396
|
+
if (!isAllowed) {
|
|
397
|
+
violationsWithDetails.push(`${file} (not permitted in allow_paths [${allowedPatterns.join(", ")}])`);
|
|
398
|
+
}
|
|
314
399
|
}
|
|
315
|
-
return false;
|
|
316
400
|
});
|
|
317
401
|
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
402
|
+
if (violationsWithDetails.length > 0) {
|
|
403
|
+
if (process.env.JULES_ALLOW_RESTRICTED_FILES === "true") {
|
|
404
|
+
log.warn("JULES_ALLOW_RESTRICTED_FILES=true is set. Bypassing restricted file boundary check.");
|
|
405
|
+
} else {
|
|
406
|
+
const err = new Error(
|
|
407
|
+
`RESTRICTED FILE VIOLATION DETECTED (config read from ${mainRef}:.agent/jules.yml):\n` +
|
|
408
|
+
violationsWithDetails.map((v) => ` - ${v}`).join("\n") +
|
|
409
|
+
"\nTo override restricted file checks, set JULES_ALLOW_RESTRICTED_FILES=true."
|
|
410
|
+
);
|
|
411
|
+
err.code = 3;
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
322
414
|
}
|
|
323
415
|
log.success("Restricted File Boundary Check: PASSED");
|
|
324
416
|
|
|
@@ -328,15 +420,23 @@ export function runSelfAudit() {
|
|
|
328
420
|
: "";
|
|
329
421
|
|
|
330
422
|
const diffBytes = Buffer.byteLength(codeDiffPayload, "utf8");
|
|
331
|
-
const
|
|
423
|
+
const maxDiffKb = parseInt(process.env.JULES_MAX_DIFF_KB || "50", 10);
|
|
424
|
+
const maxDiffBytes = maxDiffKb * 1024;
|
|
332
425
|
|
|
333
426
|
if (diffBytes > maxDiffBytes) {
|
|
334
|
-
const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${
|
|
427
|
+
const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${maxDiffKb} KB limit for code files. Split the task.`);
|
|
335
428
|
err.code = 5;
|
|
336
429
|
throw err;
|
|
337
430
|
}
|
|
338
431
|
|
|
339
|
-
|
|
432
|
+
let fullDiffPayload = "";
|
|
433
|
+
try {
|
|
434
|
+
fullDiffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], false);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
const error = new Error("DIFF PAYLOAD TOO LARGE: The PR diff exceeds the maximum buffer size (25MB) and cannot be safely scanned for secrets. Please split the task into smaller PRs.");
|
|
437
|
+
error.code = 5;
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
340
440
|
const addedLines = fullDiffPayload
|
|
341
441
|
.split("\n")
|
|
342
442
|
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
@@ -393,8 +493,17 @@ export function runSelfAudit() {
|
|
|
393
493
|
let failureLog = "";
|
|
394
494
|
|
|
395
495
|
const runVerification = (cmd) => {
|
|
496
|
+
if (typeof cmd !== "string" || !cmd.trim()) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if (/[\r\n\0]/.test(cmd)) {
|
|
500
|
+
log.error(`Security violation: Invalid control characters in command: ${cmd}`);
|
|
501
|
+
failureLog = `Security violation: Invalid control characters in verification command`;
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
396
504
|
try {
|
|
397
|
-
|
|
505
|
+
const timeoutMs = parseInt(process.env.JULES_VERIFY_TIMEOUT_MS || "600000", 10);
|
|
506
|
+
execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
|
|
398
507
|
return true;
|
|
399
508
|
} catch (err) {
|
|
400
509
|
failureLog = parseAndCleanStderr((err.stderr || "") + "\n" + (err.stdout || "") + "\n" + (err.message || ""));
|
|
@@ -442,37 +551,15 @@ export function runSelfAudit() {
|
|
|
442
551
|
throw err;
|
|
443
552
|
}
|
|
444
553
|
|
|
445
|
-
function getOodaStateFile(mergeBase = "main") {
|
|
446
|
-
const oodaDir = path.resolve(process.cwd(), ".agent/state/ooda");
|
|
447
|
-
if (!fs.existsSync(oodaDir)) fs.mkdirSync(oodaDir, { recursive: true });
|
|
448
|
-
|
|
449
|
-
const keyInput = [
|
|
450
|
-
process.env.GITHUB_REPOSITORY || "",
|
|
451
|
-
process.env.GITHUB_REF || "",
|
|
452
|
-
process.env.GITHUB_HEAD_REF || "",
|
|
453
|
-
mergeBase,
|
|
454
|
-
process.cwd()
|
|
455
|
-
].join("\u0000");
|
|
456
|
-
|
|
457
|
-
const key = crypto.createHash("sha256").update(keyInput).digest("hex").slice(0, 16);
|
|
458
|
-
return path.join(oodaDir, `${key}.json`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
554
|
let oodaRetries = 0;
|
|
462
555
|
const oodaStateFile = getOodaStateFile(mergeBase);
|
|
463
556
|
try {
|
|
464
557
|
if (fs.existsSync(oodaStateFile)) {
|
|
465
558
|
const oodaData = JSON.parse(fs.readFileSync(oodaStateFile, "utf-8"));
|
|
466
559
|
oodaRetries = oodaData.attempts || 0;
|
|
467
|
-
} else {
|
|
468
|
-
const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
|
|
469
|
-
const matches = history.match(/OODA Auto-Repair/gi);
|
|
470
|
-
if (matches) {
|
|
471
|
-
oodaRetries = matches.length;
|
|
472
|
-
}
|
|
473
560
|
}
|
|
474
561
|
} catch (e) {
|
|
475
|
-
// Ignore errors
|
|
562
|
+
// Ignore reading errors
|
|
476
563
|
}
|
|
477
564
|
|
|
478
565
|
if (oodaRetries < 3) {
|
|
@@ -480,13 +567,15 @@ function getOodaStateFile(mergeBase = "main") {
|
|
|
480
567
|
try {
|
|
481
568
|
fs.writeFileSync(oodaStateFile, JSON.stringify({ attempts: oodaRetries + 1, lastAttempt: new Date().toISOString() }), "utf-8");
|
|
482
569
|
} catch (e) {}
|
|
483
|
-
const
|
|
570
|
+
const sanitizedFailureLog = redactSecrets(anonymizePii(failureLog.slice(-1500)));
|
|
571
|
+
const prompt = `OODA Auto-Repair Attempt ${oodaRetries + 1}\n\nThe verification suite failed with the following errors after the previous patch:\n\n\`\`\`\n${sanitizedFailureLog}\n\`\`\`\n\nPlease fix the errors so the verification passes.`;
|
|
484
572
|
|
|
485
573
|
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
486
574
|
try {
|
|
487
575
|
const env = {
|
|
488
576
|
...process.env,
|
|
489
|
-
|
|
577
|
+
START_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch,
|
|
578
|
+
BASE_BRANCH: targetBranch
|
|
490
579
|
};
|
|
491
580
|
execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
|
|
492
581
|
stdio: "inherit",
|
|
@@ -524,8 +613,8 @@ export function runPreflightSandbox() {
|
|
|
524
613
|
log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
|
|
525
614
|
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
526
615
|
|
|
527
|
-
|
|
528
|
-
|
|
616
|
+
execFileSync("git", ["archive", "HEAD", "-o", tempArchive]);
|
|
617
|
+
execFileSync("tar", ["-xf", tempArchive, "-C", sandboxDir]);
|
|
529
618
|
|
|
530
619
|
log.step("[2/5]", "Sterilizing environment variables...");
|
|
531
620
|
const sterileEnv = { ...process.env };
|
package/scripts/jules-status.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { log, checkDailyBudget } from "./utils.mjs";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
|
|
6
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
if (isMainModule) {
|
|
9
|
+
const isJson = process.argv.includes("--json");
|
|
10
|
+
const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
|
|
11
|
+
|
|
12
|
+
const budgetInfo = checkDailyBudget();
|
|
9
13
|
|
|
10
14
|
if (!fs.existsSync(queueLogPath)) {
|
|
11
15
|
if (isJson) {
|
|
@@ -16,59 +20,60 @@ if (!fs.existsSync(queueLogPath)) {
|
|
|
16
20
|
process.exit(0);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(queueLogPath, "utf-8");
|
|
25
|
+
const lines = content.split("\n").filter(Boolean);
|
|
26
|
+
|
|
27
|
+
if (lines.length === 0) {
|
|
28
|
+
if (isJson) {
|
|
29
|
+
console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
|
|
30
|
+
} else {
|
|
31
|
+
log.info("Queue log is empty.");
|
|
32
|
+
}
|
|
33
|
+
process.exit(0);
|
|
28
34
|
}
|
|
29
|
-
process.exit(0);
|
|
30
|
-
}
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// We only want the latest status for each task file
|
|
37
|
+
const statusMap = new Map();
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
try {
|
|
41
|
+
const entry = JSON.parse(line);
|
|
42
|
+
if (entry.file) {
|
|
43
|
+
statusMap.set(entry.file, {
|
|
44
|
+
file: entry.file,
|
|
45
|
+
Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
|
|
46
|
+
Status: entry.status,
|
|
47
|
+
Timestamp: entry.timestamp,
|
|
48
|
+
Error: entry.error || null
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Ignore malformed lines
|
|
46
53
|
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// Ignore malformed lines
|
|
49
54
|
}
|
|
50
|
-
}
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
const tasksList = Array.from(statusMap.values());
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
if (isJson) {
|
|
59
|
+
console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if (tasksList.length === 0) {
|
|
64
|
+
log.info("No parseable task events found in queue.jsonl");
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
log.header(`JULES QUEUE STATUS (Budget: ${budgetInfo.used}/${budgetInfo.budget} sessions used)`);
|
|
69
|
+
console.table(tasksList.map(t => ({ Task: t.Task, Status: t.Status, Timestamp: new Date(t.Timestamp).toLocaleString(), Error: t.Error || "-" })));
|
|
66
70
|
|
|
67
|
-
} catch (err) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (isJson) {
|
|
73
|
+
console.log(JSON.stringify({ error: err.message }, null, 2));
|
|
74
|
+
} else {
|
|
75
|
+
log.error(`Failed to read queue log: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
process.exit(1);
|
|
72
78
|
}
|
|
73
|
-
process.exit(1);
|
|
74
79
|
}
|