jules-orchestrator-kit 0.2.9 → 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.
@@ -1,23 +1,28 @@
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
  }
17
20
 
18
21
  export function matchGlob(filepath, globPattern) {
19
22
  const cleanPath = filepath.replace(/\\/g, "/").replace(/^\.\//, "");
20
- const segments = globPattern.split("/");
23
+ // Normalize consecutive glob wildcards (e.g., "**/**" -> "**")
24
+ const normalizedGlob = globPattern.replace(/\\/g, "/").replace(/(?:\*\*\/)+/g, "**/");
25
+ const segments = normalizedGlob.split("/");
21
26
  const regexParts = [];
22
27
  for (let i = 0; i < segments.length; i++) {
23
28
  const seg = segments[i];
@@ -33,12 +38,14 @@ export function matchGlob(filepath, globPattern) {
33
38
  }
34
39
  }
35
40
  let patternStr = regexParts.join("/");
36
- patternStr = patternStr.replace(/(^|\/)\*\*(\/|$)/g, (m, p1, p2) => {
37
- if (p1 === "/" && p2 === "/") return "(?:/|/.*/)";
38
- if (p1 === "/") return "(?:/.*)?";
39
- if (p2 === "/") return "(?:.*/)?";
41
+ patternStr = patternStr.replace(/(?:^|\/)\*\*(?:\/|$)/g, (m) => {
42
+ if (m === "**") return ".*";
43
+ if (m === "/**/") return "(?:/|/.*/)";
44
+ if (m === "/**") return "(?:/.*)?";
45
+ if (m === "**/") return "(?:.*/)?";
40
46
  return ".*";
41
47
  });
48
+ patternStr = patternStr.replace(/\*\*/g, ".*");
42
49
  return new RegExp(`^${patternStr}$`, "i").test(cleanPath);
43
50
  }
44
51
 
@@ -141,45 +148,58 @@ export function logAuditMetrics(metrics) {
141
148
  }
142
149
 
143
150
  export function runSelfAudit() {
144
- console.log("šŸ” Running Jules PR Self-Audit Gatekeeper...\n");
151
+ log.header("Running Jules PR Self-Audit Gatekeeper...");
145
152
 
146
153
  const targetBranch = process.env.BASE_BRANCH || "main";
147
154
  const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
148
155
  if (!SAFE_BRANCH.test(targetBranch)) {
149
- console.error(`āŒ FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
150
- 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);
151
166
  }
152
- console.log(`šŸŽÆ Target Branch: ${targetBranch}`);
153
167
 
154
168
  if (process.env.CI) {
155
- console.log("ā˜ļø CI environment detected. Fetching merge-base history...");
156
- 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
+ }
157
177
  }
158
178
 
159
- const mainRef = runCommand(`git rev-parse --verify origin/${targetBranch}`, true) ? `origin/${targetBranch}` : targetBranch;
160
- 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);
161
181
 
162
182
  if (!mergeBase) {
163
- console.error(`āŒ FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
164
- process.exit(1);
183
+ log.error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
184
+ process.exit(2);
165
185
  }
166
186
 
167
- console.log(`šŸ”— Merge-Base Hash: ${mergeBase}`);
187
+ log.info(`Merge-Base Hash: ${mergeBase}`);
168
188
 
169
- const rawDiffFiles = runCommand(`git diff --name-only ${mergeBase}...HEAD`)
170
- .split("\n")
171
- .map((f) => f.trim())
189
+ const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
190
+ .split("\0")
191
+ .map((f) => f.trim().replace(/^["']|["']$/g, ""))
172
192
  .filter(Boolean);
173
193
 
174
194
  const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
175
195
  const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
176
196
 
177
- console.log(`\nšŸ“„ Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
178
- 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}`));
179
199
 
180
200
  // Fail closed: Load security config exclusively from base branch (mainRef).
181
201
  // Never fall back to working-tree config which an untrusted PR could craft.
182
- const trustedConfig = runCommand(`git show ${mainRef}:.agent/jules.yml`, true);
202
+ const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
183
203
  const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
184
204
  const allowedPatterns = loadAllowedPatterns(trustedConfig);
185
205
 
@@ -191,16 +211,16 @@ export function runSelfAudit() {
191
211
  });
192
212
 
193
213
  if (violations.length > 0) {
194
- console.error("\nāŒ RESTRICTED FILE VIOLATION DETECTED!");
195
- console.error("Jules PR attempted to modify forbidden system files:");
196
- violations.forEach((v) => console.error(` - ${v}`));
197
- 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);
198
218
  }
199
- console.log("\nāœ… Restricted File Boundary Check: PASSED");
219
+ log.success("Restricted File Boundary Check: PASSED");
200
220
 
201
- console.log("\nšŸ› ļø Resolving Dynamic Verification Suite...");
221
+ log.info("Resolving Dynamic Verification Suite...");
202
222
  const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
203
- console.log(`šŸ“‹ Discovered Execution Scope: ${resolvedCmds.source}`);
223
+ log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
204
224
 
205
225
  const startTime = Date.now();
206
226
  let testPassed = true;
@@ -217,23 +237,23 @@ export function runSelfAudit() {
217
237
  };
218
238
 
219
239
  if (resolvedCmds.testCmd) {
220
- console.log(`ā–¶ Running Test Verification: ${resolvedCmds.testCmd}`);
240
+ log.step("ā–¶", `Running Test Verification: ${resolvedCmds.testCmd}`);
221
241
  if (!runVerification(resolvedCmds.testCmd)) {
222
242
  testPassed = false;
223
- console.error(`āŒ Test verification failed: ${resolvedCmds.testCmd}`);
243
+ log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
224
244
  }
225
245
  }
226
246
 
227
247
  if (testPassed && resolvedCmds.buildCmd) {
228
- console.log(`ā–¶ Running Build Verification: ${resolvedCmds.buildCmd}`);
248
+ log.step("ā–¶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
229
249
  if (!runVerification(resolvedCmds.buildCmd)) {
230
250
  testPassed = false;
231
- console.error(`āŒ Build verification failed: ${resolvedCmds.buildCmd}`);
251
+ log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
232
252
  }
233
253
  }
234
254
 
235
255
  if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
236
- 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.");
237
257
  runCommand("git status");
238
258
  }
239
259
 
@@ -247,17 +267,150 @@ export function runSelfAudit() {
247
267
  });
248
268
 
249
269
  if (!testPassed) {
250
- console.error("\nšŸ”„ OODA SELF-HEALING FEEDBACK LOG:");
251
- console.error(failureLog.slice(-1500));
252
- 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);
253
347
  }
254
348
 
255
- 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
+ }
256
405
  }
257
406
 
258
407
  // Execute when invoked directly from CLI
259
408
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
260
- runSelfAudit();
409
+ if (process.argv.includes("--preflight")) {
410
+ runPreflightSandbox();
411
+ } else {
412
+ runSelfAudit();
413
+ }
261
414
  }
262
415
 
263
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
+ }
@@ -2,26 +2,27 @@ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { log, sleep } from "./utils.mjs";
5
6
 
6
7
  const execFileAsync = promisify(execFile);
7
8
 
8
9
  const tasksFile = process.argv[2];
9
10
 
10
11
  if (!tasksFile) {
11
- console.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
12
- console.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
12
+ log.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
13
+ log.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
13
14
  process.exit(1);
14
15
  }
15
16
 
16
17
  const resolvedPath = path.resolve(process.cwd(), tasksFile);
17
18
  if (!fs.existsSync(resolvedPath)) {
18
- console.error(`āŒ Tasks file not found: ${resolvedPath}`);
19
+ log.error(`Tasks file not found: ${resolvedPath}`);
19
20
  process.exit(1);
20
21
  }
21
22
 
22
23
  const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
23
24
  if (!Array.isArray(tasks)) {
24
- console.error("āŒ tasks.json must contain a JSON array of task objects.");
25
+ log.error("tasks.json must contain a JSON array of task objects.");
25
26
  process.exit(1);
26
27
  }
27
28
 
@@ -36,48 +37,119 @@ const USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
36
37
  // Ensure root project path is preserved for history logs even when running in worktrees
37
38
  process.env.JULES_PROJECT_ROOT = process.cwd();
38
39
 
39
- console.log(`šŸ Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
40
+ log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
40
41
 
41
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ const activeWorktrees = new Set();
43
+
44
+ function cleanupAllWorktreesSync() {
45
+ for (const item of Array.from(activeWorktrees)) {
46
+ if (item.wtDir && fs.existsSync(item.wtDir)) {
47
+ try {
48
+ fs.chmodSync(item.wtDir, 0o755);
49
+ } catch (_) {}
50
+ }
51
+ try {
52
+ execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
53
+ } catch (_) {}
54
+ if (item.branchName) {
55
+ try {
56
+ execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
57
+ } catch (_) {}
58
+ }
59
+ }
60
+ activeWorktrees.clear();
61
+ }
62
+
63
+ function reapOrphanedWorktrees() {
64
+ const worktreesBase = path.resolve(process.cwd(), ".agent/worktrees");
65
+ if (!fs.existsSync(worktreesBase)) return;
66
+ try {
67
+ const entries = fs.readdirSync(worktreesBase);
68
+ for (const entry of entries) {
69
+ const wtPath = path.join(worktreesBase, entry);
70
+ try {
71
+ execFileSync("git", ["worktree", "remove", "--force", wtPath], { stdio: "ignore" });
72
+ } catch (_) {
73
+ try {
74
+ fs.rmSync(wtPath, { recursive: true, force: true });
75
+ } catch (_) {}
76
+ }
77
+ }
78
+ } catch (_) {}
79
+ try {
80
+ execFileSync("git", ["worktree", "prune"], { stdio: "ignore" });
81
+ } catch (_) {}
82
+ }
83
+
84
+ process.on("SIGINT", () => {
85
+ log.dim("SIGINT received. Cleaning up active Git worktrees...");
86
+ cleanupAllWorktreesSync();
87
+ process.exit(130);
88
+ });
89
+
90
+ process.on("SIGTERM", () => {
91
+ log.dim("SIGTERM received. Cleaning up active Git worktrees...");
92
+ cleanupAllWorktreesSync();
93
+ process.exit(143);
94
+ });
42
95
 
43
96
  async function createWorktree(taskId) {
44
97
  const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
45
98
  const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
46
99
  const branchName = `jules/${slug}`;
47
- try {
48
- if (!fs.existsSync(path.dirname(wtDir))) {
49
- fs.mkdirSync(path.dirname(wtDir), { recursive: true });
50
- }
51
- await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
52
- return { wtDir, branchName };
53
- } catch (err) {
100
+ let item = null;
101
+ await fs.promises.mkdir(path.dirname(wtDir), { recursive: true });
102
+
103
+ const maxRetries = 3;
104
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
54
105
  try {
55
- await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
56
- return { wtDir, branchName: null };
57
- } catch (fallbackErr) {
58
- console.warn(`āš ļø Failed to create worktree for ${taskId}:`, fallbackErr.message);
59
- return null;
106
+ await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
107
+ item = { wtDir, branchName };
108
+ break;
109
+ } catch (err) {
110
+ if (attempt < maxRetries) {
111
+ const jitter = Math.floor(Math.random() * 200) + 100 * Math.pow(2, attempt);
112
+ await sleep(jitter);
113
+ continue;
114
+ }
115
+ try {
116
+ await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
117
+ item = { wtDir, branchName: null };
118
+ } catch (fallbackErr) {
119
+ log.error(`Failed to create worktree for ${taskId}: ${fallbackErr.message}`);
120
+ return null;
121
+ }
60
122
  }
61
123
  }
124
+ if (item) activeWorktrees.add(item);
125
+ return item;
62
126
  }
63
127
 
64
128
  async function removeWorktree(wtDir, branchName) {
65
129
  if (!wtDir) return;
130
+ for (const item of Array.from(activeWorktrees)) {
131
+ if (item.wtDir === wtDir) {
132
+ activeWorktrees.delete(item);
133
+ }
134
+ }
66
135
  try {
67
136
  await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
68
137
  } catch (err) {
69
- console.warn(`āš ļø Failed to remove worktree at ${wtDir}:`, err.message);
138
+ log.error(`Failed to remove worktree at ${wtDir}: ${err.message}`);
70
139
  }
71
140
  if (branchName) {
72
141
  try {
73
142
  await execFileAsync("git", ["branch", "-D", branchName]);
74
143
  } catch (err) {
75
- console.warn(`āš ļø Failed to delete swarm branch ${branchName}:`, err.message);
144
+ log.error(`Failed to delete swarm branch ${branchName}: ${err.message}`);
76
145
  }
77
146
  }
78
147
  }
79
148
 
80
149
  async function runSwarm() {
150
+ if (USE_WORKTREES) {
151
+ reapOrphanedWorktrees();
152
+ }
81
153
  const results = [];
82
154
  let hasFailure = false;
83
155
 
@@ -92,8 +164,7 @@ async function runSwarm() {
92
164
  await sleep(STAGGER_MS * bIdx);
93
165
  }
94
166
 
95
- console.log(`\n----------------------------------------`);
96
- console.log(`[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
167
+ log.step("======", `[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
97
168
 
98
169
  let wtInfo = null;
99
170
  let execCwd = process.cwd();
@@ -102,7 +173,7 @@ async function runSwarm() {
102
173
  wtInfo = await createWorktree(taskId);
103
174
  if (wtInfo?.wtDir) {
104
175
  execCwd = wtInfo.wtDir;
105
- console.log(`🌲 Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
176
+ log.info(`Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
106
177
  }
107
178
  }
108
179
 
@@ -117,12 +188,17 @@ async function runSwarm() {
117
188
  const { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
118
189
  cwd: execCwd,
119
190
  timeout: 15 * 60 * 1000,
120
- env: { ...process.env, JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT },
191
+ env: {
192
+ ...process.env,
193
+ JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
194
+ JULES_SLOT_INDEX: String(bIdx + 1),
195
+ JULES_SLOT_TOTAL: String(batch.length)
196
+ },
121
197
  });
122
- if (stdout) console.log(stdout.trim());
198
+ if (stdout) log.dim(stdout.trim());
123
199
  return { taskId, title: task.title, status: "SUCCESS" };
124
200
  } catch (error) {
125
- console.error(`āš ļø Failed task [${task.title}]:`, error.message);
201
+ log.error(`Failed task [${task.title}]: ${error.message}`);
126
202
  return { taskId, title: task.title, status: "FAILED", error: error.message };
127
203
  } finally {
128
204
  if (wtInfo?.wtDir) {
@@ -143,15 +219,14 @@ async function runSwarm() {
143
219
  }
144
220
 
145
221
  if (i + MAX_CONCURRENT < tasks.length) {
146
- console.log(`\nā³ Batch finished. Cooling down for 2s before next batch...`);
222
+ log.info(`Batch finished. Cooling down for 2s before next batch...`);
147
223
  await sleep(2000);
148
224
  }
149
225
  }
150
226
 
151
- console.log(`\n========================================`);
152
- console.log(`šŸŽ‰ Swarm Dispatch Summary (${results.length} tasks processed):`);
227
+ log.header(`Swarm Dispatch Summary (${results.length} tasks processed):`);
153
228
  results.forEach((res) => {
154
- console.log(` - [${res.status}] ${res.title}`);
229
+ log.info(`[${res.status}] ${res.title}`);
155
230
  });
156
231
 
157
232
  if (hasFailure) {
@@ -160,6 +235,6 @@ async function runSwarm() {
160
235
  }
161
236
 
162
237
  runSwarm().catch((err) => {
163
- console.error("āŒ Unhandled swarm rejection:", err);
238
+ log.error(`Unhandled swarm rejection: ${err.message}`);
164
239
  process.exit(1);
165
240
  });