jules-orchestrator-kit 0.3.0 → 0.5.2

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.
@@ -1,7 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { execFileSync } from "node:child_process";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { log, ensureDir, sleep } from "./utils.mjs";
7
+
8
+ const execFileAsync = promisify(execFile);
5
9
 
6
10
  const __filename = fileURLToPath(import.meta.url);
7
11
  const __dirname = path.dirname(__filename);
@@ -18,13 +22,8 @@ if (!fs.existsSync(queueDir)) {
18
22
  process.exit(1);
19
23
  }
20
24
 
21
- if (!fs.existsSync(completedDir)) {
22
- fs.mkdirSync(completedDir, { recursive: true });
23
- }
24
-
25
- if (!fs.existsSync(processingDir)) {
26
- fs.mkdirSync(processingDir, { recursive: true });
27
- }
25
+ ensureDir(completedDir);
26
+ ensureDir(processingDir);
28
27
 
29
28
  function logQueueState(file, status, error = null) {
30
29
  const entry = JSON.stringify({
@@ -41,45 +40,100 @@ const files = fs.readdirSync(queueDir).filter(
41
40
  );
42
41
 
43
42
  if (files.length === 0) {
44
- console.log("ℹ️ No tasks found in the queue.");
43
+ log.info("No tasks found in the queue.");
45
44
  process.exit(0);
46
45
  }
47
46
 
48
- console.log(`🐝 Found ${files.length} tasks in the queue. Processing...`);
47
+ log.info(`🚀 Found ${files.length} tasks in the queue. Sending to Jules...`);
48
+
49
+ const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10) || 3;
49
50
 
50
- files.forEach((file, index) => {
51
- const filePath = path.join(queueDir, file);
52
- const processingPath = path.join(processingDir, file);
53
-
54
- // Atomic claim: move from queueDir to processingDir before dispatching
51
+ const args = process.argv.slice(2);
52
+ let paceMs = parseInt(process.env.JULES_PACE_MS || "500", 10);
53
+ const paceIdx = args.indexOf("--pace-ms");
54
+ if (paceIdx !== -1 && args[paceIdx + 1]) {
55
+ const parsed = parseInt(args[paceIdx + 1], 10);
56
+ if (Number.isFinite(parsed) && parsed >= 0) paceMs = parsed;
57
+ }
58
+
59
+ function safeMoveSync(src, dest) {
55
60
  try {
56
- fs.renameSync(filePath, processingPath);
57
- } catch (claimErr) {
58
- // Another runner already claimed this task file
59
- return;
61
+ fs.renameSync(src, dest);
62
+ } catch (err) {
63
+ if (err.code === "EXDEV") {
64
+ fs.copyFileSync(src, dest);
65
+ fs.unlinkSync(src);
66
+ } else {
67
+ throw err;
68
+ }
60
69
  }
70
+ }
61
71
 
62
- // Generate title from filename (e.g. "TASK-001-auth-spec.md" -> "TASK 001 auth spec")
63
- const title = file.replace(/\.md$/, "").replace(/-/g, " ");
64
-
65
- console.log(`\n----------------------------------------`);
66
- console.log(`[${index + 1}/${files.length}] Dispatching queued task: ${title}`);
67
- logQueueState(file, "RUNNING");
68
-
72
+ async function safeMoveAsync(src, dest) {
69
73
  try {
70
- execFileSync("node", [dispatchScript, title, processingPath], {
71
- stdio: "inherit",
72
- });
73
-
74
- // Move to completed
75
- const destPath = path.join(completedDir, file);
76
- fs.renameSync(processingPath, destPath);
77
- logQueueState(file, "COMPLETED");
78
- console.log(`✅ Moved ${file} to completed/`);
79
- } catch (error) {
80
- logQueueState(file, "FAILED", error);
81
- console.error(`⚠️ Failed to dispatch task [${title}]:`, error.message);
74
+ await fs.promises.rename(src, dest);
75
+ } catch (err) {
76
+ if (err.code === "EXDEV") {
77
+ await fs.promises.copyFile(src, dest);
78
+ await fs.promises.unlink(src);
79
+ } else {
80
+ throw err;
81
+ }
82
82
  }
83
- });
83
+ }
84
+
85
+ async function runQueue() {
86
+ for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
87
+ const batch = files.slice(i, i + MAX_CONCURRENT);
88
+ await Promise.allSettled(
89
+ batch.map(async (file, bIdx) => {
90
+ if (bIdx > 0 && paceMs > 0) {
91
+ await sleep(bIdx * paceMs);
92
+ }
93
+ const index = i + bIdx;
94
+ const filePath = path.join(queueDir, file);
95
+ const processingPath = path.join(processingDir, file);
96
+
97
+ // Atomic claim: move from queueDir to processingDir before dispatching
98
+ try {
99
+ safeMoveSync(filePath, processingPath);
100
+ } catch (claimErr) {
101
+ // Another runner already claimed this task file or file missing
102
+ return;
103
+ }
104
+
105
+ const title = file.replace(/\.md$/, "").replace(/-/g, " ");
106
+
107
+ const content = fs.readFileSync(processingPath, "utf-8").trim();
108
+ if (!content || content.length < 10) {
109
+ log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
110
+ const destPath = path.join(completedDir, file);
111
+ await safeMoveAsync(processingPath, destPath);
112
+ logQueueState(file, "SKIPPED_INVALID");
113
+ return;
114
+ }
115
+
116
+ log.step(`[${index + 1}/${files.length}]`, `🚀 Sending task to Jules: ${title}`);
117
+ logQueueState(file, "RUNNING");
118
+
119
+ try {
120
+ await execFileAsync("node", [dispatchScript, title, processingPath]);
121
+
122
+ const destPath = path.join(completedDir, file);
123
+ await safeMoveAsync(processingPath, destPath);
124
+ logQueueState(file, "DISPATCHED");
125
+ log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
126
+ } catch (error) {
127
+ logQueueState(file, "FAILED", error);
128
+ log.error(`Failed to dispatch task [${title}]: ${error.message}`);
129
+ }
130
+ })
131
+ );
132
+ }
133
+ log.header("Queue processing complete!");
134
+ }
84
135
 
85
- console.log(`\n🎉 Queue processing complete!`);
136
+ runQueue().catch(err => {
137
+ log.error(`Fatal queue error: ${err.message}`);
138
+ process.exit(1);
139
+ });
@@ -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,17 +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
- if (ignoreError) return "";
12
- console.error(`❌ Command failed: ${cmd}`);
13
- console.error(error.message);
14
- process.exit(1);
14
+ const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
15
+ err.code = 1;
16
+ throw err;
15
17
  }
16
18
  }
17
19
 
@@ -145,32 +147,48 @@ export function logAuditMetrics(metrics) {
145
147
  }
146
148
 
147
149
  export function runSelfAudit() {
148
- console.log("🔍 Running Jules PR Self-Audit Gatekeeper...\n");
150
+ log.header("Running Jules PR Self-Audit Gatekeeper...");
149
151
 
150
152
  const targetBranch = process.env.BASE_BRANCH || "main";
151
153
  const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
152
154
  if (!SAFE_BRANCH.test(targetBranch)) {
153
- console.error(`❌ FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
154
- process.exit(1);
155
+ const err = new Error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
156
+ err.code = 2;
157
+ throw err;
158
+ }
159
+ log.info(`Target Branch: ${targetBranch}`);
160
+
161
+ try {
162
+ execFileSync("git", ["--version"], { stdio: "ignore" });
163
+ } catch {
164
+ const err = new Error("FATAL: git is not installed or not in PATH.");
165
+ err.code = 2;
166
+ throw err;
155
167
  }
156
- console.log(`🎯 Target Branch: ${targetBranch}`);
157
168
 
158
169
  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);
170
+ log.info("☁️ CI environment detected. Fetching merge-base history...");
171
+ runGitCommand(["fetch", "origin", targetBranch, "--depth=100"], true);
172
+ const tmpRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
173
+ const tmpMerge = runGitCommand(["merge-base", "HEAD", tmpRef], true);
174
+ if (!tmpMerge) {
175
+ log.warn("Shallow fetch failed to find merge-base, executing full unshallow...");
176
+ runGitCommand(["fetch", "origin", targetBranch, "--unshallow"], true);
177
+ }
161
178
  }
162
179
 
163
- const mainRef = runCommand(`git rev-parse --verify origin/${targetBranch}`, true) ? `origin/${targetBranch}` : targetBranch;
164
- const mergeBase = runCommand(`git merge-base HEAD ${mainRef}`, true);
180
+ const mainRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
181
+ const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
165
182
 
166
183
  if (!mergeBase) {
167
- console.error(`❌ FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
168
- process.exit(1);
184
+ const err = new Error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
185
+ err.code = 2;
186
+ throw err;
169
187
  }
170
188
 
171
- console.log(`🔗 Merge-Base Hash: ${mergeBase}`);
189
+ log.info(`Merge-Base Hash: ${mergeBase}`);
172
190
 
173
- const rawDiffFiles = runCommand(`git diff -z --name-only ${mergeBase}...HEAD`)
191
+ const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
174
192
  .split("\0")
175
193
  .map((f) => f.trim().replace(/^["']|["']$/g, ""))
176
194
  .filter(Boolean);
@@ -178,12 +196,12 @@ export function runSelfAudit() {
178
196
  const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
179
197
  const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
180
198
 
181
- console.log(`\n📄 Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
182
- changedCodeFiles.forEach((file) => console.log(` - ${file}`));
199
+ log.info(`Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
200
+ changedCodeFiles.forEach((file) => log.dim(` - ${file}`));
183
201
 
184
202
  // Fail closed: Load security config exclusively from base branch (mainRef).
185
203
  // Never fall back to working-tree config which an untrusted PR could craft.
186
- const trustedConfig = runCommand(`git show ${mainRef}:.agent/jules.yml`, true);
204
+ const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
187
205
  const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
188
206
  const allowedPatterns = loadAllowedPatterns(trustedConfig);
189
207
 
@@ -195,16 +213,15 @@ export function runSelfAudit() {
195
213
  });
196
214
 
197
215
  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);
216
+ const err = new Error("RESTRICTED FILE VIOLATION DETECTED!");
217
+ err.code = 3;
218
+ throw err;
202
219
  }
203
- console.log("\n✅ Restricted File Boundary Check: PASSED");
220
+ log.success("Restricted File Boundary Check: PASSED");
204
221
 
205
- console.log("\n🛠️ Resolving Dynamic Verification Suite...");
222
+ log.info("Resolving Dynamic Verification Suite...");
206
223
  const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
207
- console.log(`📋 Discovered Execution Scope: ${resolvedCmds.source}`);
224
+ log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
208
225
 
209
226
  const startTime = Date.now();
210
227
  let testPassed = true;
@@ -221,24 +238,24 @@ export function runSelfAudit() {
221
238
  };
222
239
 
223
240
  if (resolvedCmds.testCmd) {
224
- console.log(`▶ Running Test Verification: ${resolvedCmds.testCmd}`);
241
+ log.step("▶", `Running Test Verification: ${resolvedCmds.testCmd}`);
225
242
  if (!runVerification(resolvedCmds.testCmd)) {
226
243
  testPassed = false;
227
- console.error(`❌ Test verification failed: ${resolvedCmds.testCmd}`);
244
+ log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
228
245
  }
229
246
  }
230
247
 
231
248
  if (testPassed && resolvedCmds.buildCmd) {
232
- console.log(`▶ Running Build Verification: ${resolvedCmds.buildCmd}`);
249
+ log.step("▶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
233
250
  if (!runVerification(resolvedCmds.buildCmd)) {
234
251
  testPassed = false;
235
- console.error(`❌ Build verification failed: ${resolvedCmds.buildCmd}`);
252
+ log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
236
253
  }
237
254
  }
238
255
 
239
256
  if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
240
- console.log("ℹ️ No build or test scripts found. Running git status check.");
241
- runCommand("git status");
257
+ log.info("No build or test scripts found. Running git status check.");
258
+ execSync("git status", { stdio: "inherit" });
242
259
  }
243
260
 
244
261
  const durationMs = Date.now() - startTime;
@@ -251,17 +268,132 @@ export function runSelfAudit() {
251
268
  });
252
269
 
253
270
  if (!testPassed) {
254
- console.error("\n🔄 OODA SELF-HEALING FEEDBACK LOG:");
255
- console.error(failureLog.slice(-1500));
256
- process.exit(1);
271
+ log.error("OODA SELF-HEALING FEEDBACK LOG:");
272
+ log.dim(failureLog.slice(-1500));
273
+
274
+ if (process.env.CI && !process.env.ALLOW_AUTO_REPAIR) {
275
+ const err = new Error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
276
+ err.code = 4;
277
+ throw err;
278
+ }
279
+
280
+ let oodaRetries = 0;
281
+ try {
282
+ const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
283
+ const matches = history.match(/OODA Auto-Repair/gi);
284
+ if (matches) {
285
+ oodaRetries = matches.length;
286
+ }
287
+ } catch (e) {
288
+ // Ignorera git-fel här
289
+ }
290
+
291
+ if (oodaRetries < 3) {
292
+ log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
293
+ 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.`;
294
+
295
+ const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
296
+ try {
297
+ const env = {
298
+ ...process.env,
299
+ BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
300
+ };
301
+ execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
302
+ stdio: "inherit",
303
+ env
304
+ });
305
+ } catch (dispatchErr) {
306
+ log.error(`Failed to trigger OODA repair dispatch: ${dispatchErr.message}`);
307
+ }
308
+ } else {
309
+ log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
310
+ }
311
+ const err = new Error("OODA Auto-Repair failed or exhausted.");
312
+ err.code = 4;
313
+ throw err;
257
314
  }
258
315
 
259
- console.log("\n🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
316
+ log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
317
+ }
318
+
319
+ export function runPreflightSandbox() {
320
+ log.header("Initializing Pre-Flight Sandbox (Sterile Simulation)...");
321
+
322
+ const runId = crypto.randomBytes(4).toString("hex");
323
+ const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
324
+ const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
325
+
326
+ try {
327
+ log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
328
+ fs.mkdirSync(sandboxDir, { recursive: true });
329
+
330
+ execSync(`git archive HEAD -o "${tempArchive}"`);
331
+ execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
332
+
333
+ log.step("[2/5]", "Sterilizing environment variables...");
334
+ const sterileEnv = { ...process.env };
335
+ const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
336
+ for (const key of stripKeys) {
337
+ if (sterileEnv[key]) {
338
+ sterileEnv[key] = `mock-${key.toLowerCase()}`;
339
+ }
340
+ }
341
+
342
+ log.step("[3/5]", "Installing dependencies in sandbox (npm ci)...");
343
+ const packageJsonPath = path.join(sandboxDir, "package.json");
344
+ if (fs.existsSync(packageJsonPath)) {
345
+ try {
346
+ execSync("npm ci --ignore-scripts", { cwd: sandboxDir, stdio: "ignore" });
347
+ } catch (e) {
348
+ log.warn("npm ci failed or wasn't needed. Proceeding...");
349
+ }
350
+ }
351
+
352
+ log.step("[4/5]", "Resolving execution boundary in sandbox...");
353
+ const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
354
+
355
+ if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
356
+ log.step("[5/5]", `Running verification: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
357
+ execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
358
+ cwd: sandboxDir,
359
+ env: sterileEnv,
360
+ stdio: "inherit"
361
+ });
362
+ log.success("Pre-Flight Passed. Zero epistemic drift detected.");
363
+ } else {
364
+ log.info("No test/build commands found for verification.");
365
+ log.success("Pre-Flight Passed (No-Op).");
366
+ }
367
+ } catch (err) {
368
+ log.error("\nPRE-FLIGHT FAILED!");
369
+ log.error("The agent would likely crash (The 5-Minute Drop-off) in the Web UI due to missing dependencies.");
370
+ const wrappedErr = new Error(err.message);
371
+ wrappedErr.code = 1;
372
+ throw wrappedErr;
373
+ } finally {
374
+ try {
375
+ fs.rmSync(sandboxDir, { recursive: true, force: true });
376
+ if (fs.existsSync(tempArchive)) {
377
+ fs.rmSync(tempArchive, { force: true });
378
+ }
379
+ } catch (e) {
380
+ // Ignorera fel vid cleanup
381
+ }
382
+ }
260
383
  }
261
384
 
262
385
  // Execute when invoked directly from CLI
263
386
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
264
- runSelfAudit();
387
+ try {
388
+ if (process.argv.includes("--preflight")) {
389
+ runPreflightSandbox();
390
+ } else {
391
+ runSelfAudit();
392
+ }
393
+ } catch (err) {
394
+ if (err.message) log.error(err.message);
395
+ process.exit(err.code || 1);
396
+ }
265
397
  }
266
398
 
267
399
 
@@ -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
+ }