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.
@@ -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 (/Daily budget exhausted/i.test(message)) return "budget_exhausted";
106
- if (/HTTP 429|Rate Limit Exceeded/i.test(message)) return "rate_limited";
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", "budget_exhausted"]);
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 (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
192
- const destPath = path.join(failedDir, file);
193
- setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
194
- try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
195
- log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
196
- } else {
197
- setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
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}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
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 external dependencies.
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, logToHistory, redactSecrets, hasHighConfidenceSecret, hasLowConfidenceSecret } from "./utils.mjs";
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, { encoding: "utf-8" }).trim();
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
- "**/lock-manager/**",
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 trimmed = line.trim();
168
+ const lineWithoutComment = line.split("#")[0];
169
+ const trimmed = lineWithoutComment.trim();
164
170
  if (trimmed.startsWith("-")) {
165
- blockParsed.push(trimmed.slice(1).trim().replace(/^["']|["']$/g, ""));
166
- } else if (trimmed && !trimmed.startsWith("#")) {
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(", ") + ". Refusing to execute verification scripts from untrusted branch."
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(", ") + ". Refusing to load agent rules from untrusted branch."
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 violations = rawDiffFiles.filter((file) => {
309
- const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
310
- if (isForbidden) return true; // Immutable forbidden path — CANNOT be overridden by allow_paths
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
- return !isAllowed;
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 (violations.length > 0) {
319
- const err = new Error("RESTRICTED FILE VIOLATION DETECTED: " + violations.join(", "));
320
- err.code = 3;
321
- throw err;
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 maxDiffBytes = 75 * 1024; // 75 KB
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 ${maxDiffBytes} bytes limit for code files. Split the task.`);
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
- const fullDiffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], true);
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
- execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
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 here
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 prompt = `OODA Auto-Repair Attempt ${oodaRetries + 1}\n\nThe verification suite failed with the following errors after the previous patch:\n\n\`\`\`\n${failureLog.slice(-1500)}\n\`\`\`\n\nPlease fix the errors so the verification passes.`;
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
- BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
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
- execSync(`git archive HEAD -o "${tempArchive}"`);
528
- execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
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 };
@@ -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 isJson = process.argv.includes("--json");
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
- const budgetInfo = checkDailyBudget();
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
- const content = fs.readFileSync(queueLogPath, "utf-8");
21
- const lines = content.split("\n").filter(Boolean);
22
-
23
- if (lines.length === 0) {
24
- if (isJson) {
25
- console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
26
- } else {
27
- log.info("Queue log is empty.");
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
- // We only want the latest status for each task file
33
- const statusMap = new Map();
36
+ // We only want the latest status for each task file
37
+ const statusMap = new Map();
34
38
 
35
- for (const line of lines) {
36
- try {
37
- const entry = JSON.parse(line);
38
- if (entry.file) {
39
- statusMap.set(entry.file, {
40
- file: entry.file,
41
- Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
42
- Status: entry.status,
43
- Timestamp: entry.timestamp,
44
- Error: entry.error || null
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
- const tasksList = Array.from(statusMap.values());
56
+ const tasksList = Array.from(statusMap.values());
53
57
 
54
- if (isJson) {
55
- console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
56
- process.exit(0);
57
- }
58
+ if (isJson) {
59
+ console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
60
+ process.exit(0);
61
+ }
58
62
 
59
- if (tasksList.length === 0) {
60
- log.info("No parseable task events found in queue.jsonl");
61
- process.exit(0);
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
- log.header(`JULES QUEUE STATUS (Budget: ${budgetInfo.used}/${budgetInfo.budget} sessions used)`);
65
- console.table(tasksList.map(t => ({ Task: t.Task, Status: t.Status, Timestamp: new Date(t.Timestamp).toLocaleString(), Error: t.Error || "-" })));
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
- if (isJson) {
69
- console.log(JSON.stringify({ error: err.message }, null, 2));
70
- } else {
71
- log.error(`Failed to read queue log: ${err.message}`);
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
  }