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.
@@ -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(
@@ -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 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, "");
@@ -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 violations = rawDiffFiles.filter((file) => {
329
- const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
330
- 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
+ }
331
394
  if (allowedPatterns.length > 0) {
332
395
  const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
333
- return !isAllowed;
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 (violations.length > 0) {
339
- const err = new Error("RESTRICTED FILE VIOLATION DETECTED: " + violations.join(", "));
340
- err.code = 3;
341
- 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
+ }
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 maxDiffBytes = 75 * 1024; // 75 KB
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 ${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.`);
355
428
  err.code = 5;
356
429
  throw err;
357
430
  }
358
431
 
359
- 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
+ }
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
- 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 });
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 here
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 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.`;
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
- BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
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
- execSync(`git archive HEAD -o "${tempArchive}"`);
532
- execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
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 };
@@ -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
  }