jules-orchestrator-kit 0.8.1 → 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/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +1 -1
- package/README.md +93 -13
- package/bin/init.js +1 -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 +23 -5
- package/scripts/jules-scan-todos.mjs +1 -1
- package/scripts/jules-self-audit.mjs +115 -30
- 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 +135 -7
- 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(
|
|
@@ -191,9 +195,19 @@ async function runQueue() {
|
|
|
191
195
|
setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
192
196
|
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
193
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() });
|
|
204
|
+
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
205
|
+
log.warn(`Task [${title}] active session quota (~30) reached. Pausing 15s before re-queuing.`);
|
|
206
|
+
await sleep(15000);
|
|
194
207
|
} else {
|
|
195
208
|
const attempts = (taskState.attempts || 0) + 1;
|
|
196
209
|
if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
|
|
210
|
+
permanentFailuresCount++;
|
|
197
211
|
const destPath = path.join(failedDir, file);
|
|
198
212
|
setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
199
213
|
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
@@ -208,6 +222,10 @@ async function runQueue() {
|
|
|
208
222
|
})
|
|
209
223
|
);
|
|
210
224
|
}
|
|
225
|
+
if (permanentFailuresCount > 0) {
|
|
226
|
+
log.error(`Queue processing complete with ${permanentFailuresCount} failed task(s).`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
211
229
|
log.header("Queue processing complete!");
|
|
212
230
|
}
|
|
213
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, "");
|
|
@@ -322,23 +373,44 @@ export function runSelfAudit() {
|
|
|
322
373
|
// Fail closed: Load security config exclusively from base branch (mainRef).
|
|
323
374
|
// Never fall back to working-tree config which an untrusted PR could craft.
|
|
324
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
|
+
|
|
325
384
|
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
326
385
|
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
327
386
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
394
|
if (allowedPatterns.length > 0) {
|
|
332
395
|
const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
|
|
333
|
-
|
|
396
|
+
if (!isAllowed) {
|
|
397
|
+
violationsWithDetails.push(`${file} (not permitted in allow_paths [${allowedPatterns.join(", ")}])`);
|
|
398
|
+
}
|
|
334
399
|
}
|
|
335
|
-
return false;
|
|
336
400
|
});
|
|
337
401
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
414
|
}
|
|
343
415
|
log.success("Restricted File Boundary Check: PASSED");
|
|
344
416
|
|
|
@@ -348,15 +420,23 @@ export function runSelfAudit() {
|
|
|
348
420
|
: "";
|
|
349
421
|
|
|
350
422
|
const diffBytes = Buffer.byteLength(codeDiffPayload, "utf8");
|
|
351
|
-
const
|
|
423
|
+
const maxDiffKb = parseInt(process.env.JULES_MAX_DIFF_KB || "50", 10);
|
|
424
|
+
const maxDiffBytes = maxDiffKb * 1024;
|
|
352
425
|
|
|
353
426
|
if (diffBytes > maxDiffBytes) {
|
|
354
|
-
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.`);
|
|
355
428
|
err.code = 5;
|
|
356
429
|
throw err;
|
|
357
430
|
}
|
|
358
431
|
|
|
359
|
-
|
|
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
|
+
}
|
|
360
440
|
const addedLines = fullDiffPayload
|
|
361
441
|
.split("\n")
|
|
362
442
|
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
@@ -413,8 +493,17 @@ export function runSelfAudit() {
|
|
|
413
493
|
let failureLog = "";
|
|
414
494
|
|
|
415
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
|
+
}
|
|
416
504
|
try {
|
|
417
|
-
|
|
505
|
+
const timeoutMs = parseInt(process.env.JULES_VERIFY_TIMEOUT_MS || "600000", 10);
|
|
506
|
+
execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
|
|
418
507
|
return true;
|
|
419
508
|
} catch (err) {
|
|
420
509
|
failureLog = parseAndCleanStderr((err.stderr || "") + "\n" + (err.stdout || "") + "\n" + (err.message || ""));
|
|
@@ -468,15 +557,9 @@ export function runSelfAudit() {
|
|
|
468
557
|
if (fs.existsSync(oodaStateFile)) {
|
|
469
558
|
const oodaData = JSON.parse(fs.readFileSync(oodaStateFile, "utf-8"));
|
|
470
559
|
oodaRetries = oodaData.attempts || 0;
|
|
471
|
-
} else {
|
|
472
|
-
const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
|
|
473
|
-
const matches = history.match(/OODA Auto-Repair/gi);
|
|
474
|
-
if (matches) {
|
|
475
|
-
oodaRetries = matches.length;
|
|
476
|
-
}
|
|
477
560
|
}
|
|
478
561
|
} catch (e) {
|
|
479
|
-
// Ignore errors
|
|
562
|
+
// Ignore reading errors
|
|
480
563
|
}
|
|
481
564
|
|
|
482
565
|
if (oodaRetries < 3) {
|
|
@@ -484,13 +567,15 @@ export function runSelfAudit() {
|
|
|
484
567
|
try {
|
|
485
568
|
fs.writeFileSync(oodaStateFile, JSON.stringify({ attempts: oodaRetries + 1, lastAttempt: new Date().toISOString() }), "utf-8");
|
|
486
569
|
} catch (e) {}
|
|
487
|
-
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.`;
|
|
488
572
|
|
|
489
573
|
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
490
574
|
try {
|
|
491
575
|
const env = {
|
|
492
576
|
...process.env,
|
|
493
|
-
|
|
577
|
+
START_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch,
|
|
578
|
+
BASE_BRANCH: targetBranch
|
|
494
579
|
};
|
|
495
580
|
execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
|
|
496
581
|
stdio: "inherit",
|
|
@@ -528,8 +613,8 @@ export function runPreflightSandbox() {
|
|
|
528
613
|
log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
|
|
529
614
|
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
530
615
|
|
|
531
|
-
|
|
532
|
-
|
|
616
|
+
execFileSync("git", ["archive", "HEAD", "-o", tempArchive]);
|
|
617
|
+
execFileSync("tar", ["-xf", tempArchive, "-C", sandboxDir]);
|
|
533
618
|
|
|
534
619
|
log.step("[2/5]", "Sterilizing environment variables...");
|
|
535
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
|
}
|