jules-orchestrator-kit 0.3.0 → 0.5.1

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,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Suggested Tasks Scanner for Google Jules
4
+ * Scans codebase for TODO, FIXME, HACK, and OPTIMIZE comments, ranks them by priority,
5
+ * and generates a tasks JSON file formatted for jules-swarm or jules-dispatch.
6
+ * Zero external dependencies.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { log, ensureDir } from "./utils.mjs";
12
+
13
+ const IGNORE_DIRS = new Set([
14
+ "node_modules",
15
+ ".git",
16
+ ".agent/history",
17
+ ".agent/worktrees",
18
+ ".agent/jules-queue/.processing",
19
+ ".agent/jules-queue/completed",
20
+ "dist",
21
+ "build",
22
+ "coverage",
23
+ ".next",
24
+ ".turbo",
25
+ "tmp_repos",
26
+ "tmp_repos2",
27
+ "tmp_repos3"
28
+ ]);
29
+
30
+ const IGNORE_EXTS = new Set([
31
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".pdf",
32
+ ".zip", ".tar", ".gz", ".7z", ".mp4", ".webm", ".mp3", ".wav",
33
+ ".lock", ".map", ".bin", ".exe"
34
+ ]);
35
+
36
+ const TAG_REGEX = /(?:(?:\/\/|\/\*|\*|#|<!--)\s*|\b)(FIXME|HACK|TODO|OPTIMIZE):?\s*(.+)$/;
37
+
38
+ export function scanCodebaseForTodos(rootDir = process.cwd(), options = {}) {
39
+ const tasks = [];
40
+ const maxTasks = options.maxTasks || 100;
41
+
42
+ function walk(currentDir) {
43
+ if (tasks.length >= maxTasks) return;
44
+ let entries = [];
45
+ try {
46
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
47
+ } catch (_) {
48
+ return;
49
+ }
50
+
51
+ for (const entry of entries) {
52
+ if (tasks.length >= maxTasks) break;
53
+ const fullPath = path.join(currentDir, entry.name);
54
+ const relPath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
55
+
56
+ if (entry.isDirectory()) {
57
+ const baseName = entry.name;
58
+ if (IGNORE_DIRS.has(baseName) || IGNORE_DIRS.has(relPath) || baseName.startsWith(".")) {
59
+ continue;
60
+ }
61
+ walk(fullPath);
62
+ } else if (entry.isFile()) {
63
+ const ext = path.extname(entry.name).toLowerCase();
64
+ if (IGNORE_EXTS.has(ext)) continue;
65
+
66
+ try {
67
+ const content = fs.readFileSync(fullPath, "utf-8");
68
+ const lines = content.split("\n");
69
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
70
+ const line = lines[lineIdx];
71
+ // Ignore false positives from regex definitions, test assertions, or scanner string literals
72
+ if (/TAG_REGEX|assert\.|tasks\.some|log\.info\("Zero TODO|\/FIXME comments/i.test(line)) {
73
+ continue;
74
+ }
75
+ const match = line.match(TAG_REGEX);
76
+ if (match) {
77
+ const tag = match[1].toUpperCase();
78
+ const text = match[2].trim();
79
+ const priority = (tag === "FIXME" || tag === "HACK") ? "HIGH" : "MEDIUM";
80
+ const taskId = `task-${tag.toLowerCase()}-${tasks.length + 1}`;
81
+
82
+ tasks.push({
83
+ id: taskId,
84
+ title: `${tag} in ${path.basename(relPath)}: ${text.slice(0, 50)}`,
85
+ priority,
86
+ prompt: `Resolve ${tag} comment in \`${relPath}\` at line ${lineIdx + 1}.\n\nComment: ${text}\n\nFile Context:\n\`\`\`\nLine ${lineIdx + 1}: ${line.trim()}\n\`\`\``,
87
+ file: relPath,
88
+ line: lineIdx + 1,
89
+ tag
90
+ });
91
+
92
+ if (tasks.length >= maxTasks) break;
93
+ }
94
+ }
95
+ } catch (_) {
96
+ // Skip unreadable files
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ walk(rootDir);
103
+ return tasks;
104
+ }
105
+
106
+ export function runScanner() {
107
+ const args = process.argv.slice(2);
108
+ const isJson = args.includes("--json");
109
+
110
+ let outputPath = path.resolve(process.cwd(), ".agent/jules-queue/suggested-tasks.json");
111
+ const outIdx = args.indexOf("--output");
112
+ if (outIdx !== -1 && args[outIdx + 1]) {
113
+ outputPath = path.resolve(process.cwd(), args[outIdx + 1]);
114
+ }
115
+
116
+ log.header("Suggested Tasks Scanner");
117
+ log.info(`Scanning codebase for TODO, FIXME, HACK, and OPTIMIZE comments...`);
118
+
119
+ const tasks = scanCodebaseForTodos(process.cwd());
120
+
121
+ if (isJson) {
122
+ console.log(JSON.stringify(tasks, null, 2));
123
+ return;
124
+ }
125
+
126
+ log.success(`Scanned and found ${tasks.length} suggested task(s).`);
127
+
128
+ if (tasks.length === 0) {
129
+ log.info("Zero TODO/FIXME comments found in codebase!");
130
+ return;
131
+ }
132
+
133
+ ensureDir(path.dirname(outputPath));
134
+ fs.writeFileSync(outputPath, JSON.stringify(tasks, null, 2), "utf-8");
135
+
136
+ log.success(`Generated suggested tasks file: ${path.relative(process.cwd(), outputPath)}`);
137
+ log.info(`Run \`npx jules-swarm ${path.relative(process.cwd(), outputPath)}\` to execute all tasks in parallel.`);
138
+ }
139
+
140
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.url.slice(7))) {
141
+ runScanner();
142
+ }
@@ -1,16 +1,19 @@
1
- import { execSync } from "node:child_process";
1
+ import { execSync, execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import os from "node:os";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
8
+ import { log, logToHistory } from "./utils.mjs";
6
9
 
7
- function runCommand(cmd, ignoreError = false) {
10
+ function runGitCommand(args, ignoreError = false) {
8
11
  try {
9
- return execSync(cmd, { encoding: "utf-8" }).trim();
12
+ return execFileSync("git", args, { encoding: "utf-8" }).trim();
10
13
  } catch (error) {
11
14
  if (ignoreError) return "";
12
- console.error(`❌ Command failed: ${cmd}`);
13
- console.error(error.message);
15
+ log.error(`Git command failed: git ${args.join(" ")}`);
16
+ log.error(error.message);
14
17
  process.exit(1);
15
18
  }
16
19
  }
@@ -145,32 +148,45 @@ export function logAuditMetrics(metrics) {
145
148
  }
146
149
 
147
150
  export function runSelfAudit() {
148
- console.log("🔍 Running Jules PR Self-Audit Gatekeeper...\n");
151
+ log.header("Running Jules PR Self-Audit Gatekeeper...");
149
152
 
150
153
  const targetBranch = process.env.BASE_BRANCH || "main";
151
154
  const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
152
155
  if (!SAFE_BRANCH.test(targetBranch)) {
153
- console.error(`❌ FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
154
- process.exit(1);
156
+ log.error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
157
+ process.exit(2);
158
+ }
159
+ log.info(`Target Branch: ${targetBranch}`);
160
+
161
+ try {
162
+ execFileSync("git", ["--version"], { stdio: "ignore" });
163
+ } catch {
164
+ log.error("FATAL: git is not installed or not in PATH.");
165
+ process.exit(2);
155
166
  }
156
- console.log(`🎯 Target Branch: ${targetBranch}`);
157
167
 
158
168
  if (process.env.CI) {
159
- console.log("☁️ CI environment detected. Fetching merge-base history...");
160
- runCommand(`git fetch origin ${targetBranch} --depth=100 || git fetch origin ${targetBranch} --unshallow`, true);
169
+ log.info("☁️ CI environment detected. Fetching merge-base history...");
170
+ runGitCommand(["fetch", "origin", targetBranch, "--depth=100"], true);
171
+ const tmpRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
172
+ const tmpMerge = runGitCommand(["merge-base", "HEAD", tmpRef], true);
173
+ if (!tmpMerge) {
174
+ log.warn("Shallow fetch failed to find merge-base, executing full unshallow...");
175
+ runGitCommand(["fetch", "origin", targetBranch, "--unshallow"], true);
176
+ }
161
177
  }
162
178
 
163
- const mainRef = runCommand(`git rev-parse --verify origin/${targetBranch}`, true) ? `origin/${targetBranch}` : targetBranch;
164
- const mergeBase = runCommand(`git merge-base HEAD ${mainRef}`, true);
179
+ const mainRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
180
+ const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
165
181
 
166
182
  if (!mergeBase) {
167
- console.error(`❌ FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
168
- process.exit(1);
183
+ log.error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
184
+ process.exit(2);
169
185
  }
170
186
 
171
- console.log(`🔗 Merge-Base Hash: ${mergeBase}`);
187
+ log.info(`Merge-Base Hash: ${mergeBase}`);
172
188
 
173
- const rawDiffFiles = runCommand(`git diff -z --name-only ${mergeBase}...HEAD`)
189
+ const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
174
190
  .split("\0")
175
191
  .map((f) => f.trim().replace(/^["']|["']$/g, ""))
176
192
  .filter(Boolean);
@@ -178,12 +194,12 @@ export function runSelfAudit() {
178
194
  const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
179
195
  const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
180
196
 
181
- console.log(`\n📄 Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
182
- changedCodeFiles.forEach((file) => console.log(` - ${file}`));
197
+ log.info(`Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
198
+ changedCodeFiles.forEach((file) => log.dim(` - ${file}`));
183
199
 
184
200
  // Fail closed: Load security config exclusively from base branch (mainRef).
185
201
  // Never fall back to working-tree config which an untrusted PR could craft.
186
- const trustedConfig = runCommand(`git show ${mainRef}:.agent/jules.yml`, true);
202
+ const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
187
203
  const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
188
204
  const allowedPatterns = loadAllowedPatterns(trustedConfig);
189
205
 
@@ -195,16 +211,16 @@ export function runSelfAudit() {
195
211
  });
196
212
 
197
213
  if (violations.length > 0) {
198
- console.error("\n❌ RESTRICTED FILE VIOLATION DETECTED!");
199
- console.error("Jules PR attempted to modify forbidden system files:");
200
- violations.forEach((v) => console.error(` - ${v}`));
201
- process.exit(1);
214
+ log.error("RESTRICTED FILE VIOLATION DETECTED!");
215
+ log.error("Jules PR attempted to modify forbidden system files:");
216
+ violations.forEach((v) => log.error(` - ${v}`));
217
+ process.exit(3);
202
218
  }
203
- console.log("\n✅ Restricted File Boundary Check: PASSED");
219
+ log.success("Restricted File Boundary Check: PASSED");
204
220
 
205
- console.log("\n🛠️ Resolving Dynamic Verification Suite...");
221
+ log.info("Resolving Dynamic Verification Suite...");
206
222
  const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
207
- console.log(`📋 Discovered Execution Scope: ${resolvedCmds.source}`);
223
+ log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
208
224
 
209
225
  const startTime = Date.now();
210
226
  let testPassed = true;
@@ -221,23 +237,23 @@ export function runSelfAudit() {
221
237
  };
222
238
 
223
239
  if (resolvedCmds.testCmd) {
224
- console.log(`▶ Running Test Verification: ${resolvedCmds.testCmd}`);
240
+ log.step("▶", `Running Test Verification: ${resolvedCmds.testCmd}`);
225
241
  if (!runVerification(resolvedCmds.testCmd)) {
226
242
  testPassed = false;
227
- console.error(`❌ Test verification failed: ${resolvedCmds.testCmd}`);
243
+ log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
228
244
  }
229
245
  }
230
246
 
231
247
  if (testPassed && resolvedCmds.buildCmd) {
232
- console.log(`▶ Running Build Verification: ${resolvedCmds.buildCmd}`);
248
+ log.step("▶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
233
249
  if (!runVerification(resolvedCmds.buildCmd)) {
234
250
  testPassed = false;
235
- console.error(`❌ Build verification failed: ${resolvedCmds.buildCmd}`);
251
+ log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
236
252
  }
237
253
  }
238
254
 
239
255
  if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
240
- console.log("ℹ️ No build or test scripts found. Running git status check.");
256
+ log.info("No build or test scripts found. Running git status check.");
241
257
  runCommand("git status");
242
258
  }
243
259
 
@@ -251,17 +267,150 @@ export function runSelfAudit() {
251
267
  });
252
268
 
253
269
  if (!testPassed) {
254
- console.error("\n🔄 OODA SELF-HEALING FEEDBACK LOG:");
255
- console.error(failureLog.slice(-1500));
256
- process.exit(1);
270
+ log.error("OODA SELF-HEALING FEEDBACK LOG:");
271
+ log.dim(failureLog.slice(-1500));
272
+
273
+ // OODA Circuit Breaker: Fingerprint failure trace using SHA-256
274
+ const normalizedError = failureLog
275
+ .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/g, "TIMESTAMP")
276
+ .replace(/0x[0-9a-fA-F]+/g, "HEX")
277
+ .replace(/\d+/g, "NUM")
278
+ .trim();
279
+ const errorHash = crypto.createHash("sha256").update(normalizedError).digest("hex");
280
+
281
+ const circuitFile = path.resolve(process.cwd(), ".agent/history/ooda-circuit.json");
282
+ let prevCircuit = { hash: "", count: 0 };
283
+ if (fs.existsSync(circuitFile)) {
284
+ try {
285
+ prevCircuit = JSON.parse(fs.readFileSync(circuitFile, "utf-8"));
286
+ } catch (_) {}
287
+ }
288
+
289
+ if (prevCircuit.hash === errorHash && prevCircuit.count >= 1) {
290
+ log.error("❌ OODA CIRCUIT BREAKER TRIPPED: Consecutive identical failure detected.");
291
+ log.error("Agent generated code with identical error trace twice in a row. Aborting auto-repair.");
292
+ try {
293
+ fs.writeFileSync(circuitFile, JSON.stringify({ hash: "", count: 0 }));
294
+ } catch (_) {}
295
+ process.exit(4);
296
+ }
297
+
298
+ try {
299
+ fs.writeFileSync(
300
+ circuitFile,
301
+ JSON.stringify({
302
+ hash: errorHash,
303
+ count: prevCircuit.hash === errorHash ? prevCircuit.count + 1 : 1,
304
+ updatedAt: new Date().toISOString()
305
+ })
306
+ );
307
+ } catch (_) {}
308
+
309
+ if (process.env.CI && !process.env.ALLOW_AUTO_REPAIR) {
310
+ log.error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
311
+ process.exit(4);
312
+ }
313
+
314
+ let oodaRetries = 0;
315
+ try {
316
+ const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
317
+ const matches = history.match(/OODA Auto-Repair/gi);
318
+ if (matches) {
319
+ oodaRetries = matches.length;
320
+ }
321
+ } catch (e) {
322
+ // Ignorera git-fel här
323
+ }
324
+
325
+ if (oodaRetries < 3) {
326
+ log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
327
+ 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.`;
328
+
329
+ const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
330
+ try {
331
+ const env = {
332
+ ...process.env,
333
+ BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
334
+ };
335
+ execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
336
+ stdio: "inherit",
337
+ env
338
+ });
339
+ } catch (dispatchErr) {
340
+ log.error(`Failed to trigger OODA repair dispatch: ${dispatchErr.message}`);
341
+ }
342
+ } else {
343
+ log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
344
+ }
345
+
346
+ process.exit(4);
257
347
  }
258
348
 
259
- console.log("\n🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
349
+ log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
350
+ }
351
+
352
+ export function runPreflightSandbox() {
353
+ log.header("Initializing Pre-Flight Sandbox (Steril Simulering)...");
354
+
355
+ const runId = crypto.randomBytes(4).toString("hex");
356
+ const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
357
+ const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
358
+
359
+ try {
360
+ log.step("[1/4]", `Skapar isolerad miljö i ${sandboxDir}...`);
361
+ fs.mkdirSync(sandboxDir, { recursive: true });
362
+
363
+ execSync(`git archive HEAD -o "${tempArchive}"`);
364
+ execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
365
+
366
+ log.step("[2/4]", "Steriliserar miljövariabler...");
367
+ const sterileEnv = { ...process.env };
368
+ const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
369
+ for (const key of stripKeys) {
370
+ if (sterileEnv[key]) {
371
+ sterileEnv[key] = `mock-${key.toLowerCase()}`;
372
+ }
373
+ }
374
+
375
+ log.step("[3/4]", "Löser beroenden i sandlådan...");
376
+ const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
377
+
378
+ if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
379
+ log.step("[4/4]", `Kör verifiering: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
380
+ execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
381
+ cwd: sandboxDir,
382
+ env: sterileEnv,
383
+ stdio: "inherit"
384
+ });
385
+ log.success("Pre-Flight Passed. Zero epistemic drift detected.");
386
+ } else {
387
+ log.info("Inga test/bygg-kommandon hittades för verifiering.");
388
+ log.success("Pre-Flight Passed (No-Op).");
389
+ }
390
+ } catch (err) {
391
+ log.error("\nPRE-FLIGHT MISSLYCKADES!");
392
+ log.error("Agenten skulle förmodligen krascha (The 5-Minute Drop-off) i Web UI på grund av saknade beroenden.");
393
+ log.error(err.message);
394
+ process.exit(1);
395
+ } finally {
396
+ try {
397
+ fs.rmSync(sandboxDir, { recursive: true, force: true });
398
+ if (fs.existsSync(tempArchive)) {
399
+ fs.rmSync(tempArchive, { force: true });
400
+ }
401
+ } catch (e) {
402
+ // Ignorera fel vid cleanup
403
+ }
404
+ }
260
405
  }
261
406
 
262
407
  // Execute when invoked directly from CLI
263
408
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
264
- runSelfAudit();
409
+ if (process.argv.includes("--preflight")) {
410
+ runPreflightSandbox();
411
+ } else {
412
+ runSelfAudit();
413
+ }
265
414
  }
266
415
 
267
416
 
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Jules Task Status Utility
4
+ * Parses the queue.jsonl file and renders a clean console.table
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { log } from "./utils.mjs";
10
+
11
+ const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
12
+
13
+ if (!fs.existsSync(queueLogPath)) {
14
+ log.info("Queue log is empty. No tasks have been dispatched yet.");
15
+ process.exit(0);
16
+ }
17
+
18
+ try {
19
+ const content = fs.readFileSync(queueLogPath, "utf-8");
20
+ const lines = content.split("\n").filter(Boolean);
21
+
22
+ if (lines.length === 0) {
23
+ log.info("Queue log is empty.");
24
+ process.exit(0);
25
+ }
26
+
27
+ // We only want the latest status for each task file
28
+ const statusMap = new Map();
29
+
30
+ for (const line of lines) {
31
+ try {
32
+ const entry = JSON.parse(line);
33
+ if (entry.file) {
34
+ statusMap.set(entry.file, {
35
+ Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
36
+ Status: entry.status,
37
+ Timestamp: new Date(entry.timestamp).toLocaleString(),
38
+ Error: entry.error || "-"
39
+ });
40
+ }
41
+ } catch (e) {
42
+ // Ignore malformed lines
43
+ }
44
+ }
45
+
46
+ if (statusMap.size === 0) {
47
+ log.info("No parseable task events found in queue.jsonl");
48
+ process.exit(0);
49
+ }
50
+
51
+ log.header("Jules Queue Status");
52
+ console.table(Array.from(statusMap.values()));
53
+
54
+ } catch (err) {
55
+ log.error(`Failed to read queue log: ${err.message}`);
56
+ process.exit(1);
57
+ }