jules-orchestrator-kit 0.5.1 → 0.6.0

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.
@@ -5,16 +5,16 @@ 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 } from "./utils.mjs";
8
+ import { log, logToHistory, redactSecrets } from "./utils.mjs";
9
9
 
10
10
  function runGitCommand(args, ignoreError = false) {
11
11
  try {
12
12
  return execFileSync("git", args, { encoding: "utf-8" }).trim();
13
13
  } catch (error) {
14
14
  if (ignoreError) return "";
15
- log.error(`Git command failed: git ${args.join(" ")}`);
16
- log.error(error.message);
17
- process.exit(1);
15
+ const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
16
+ err.code = 1;
17
+ throw err;
18
18
  }
19
19
  }
20
20
 
@@ -33,7 +33,7 @@ export function matchGlob(filepath, globPattern) {
33
33
  seg
34
34
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
35
35
  .replace(/\*/g, "[^/]*")
36
- .replace(/\?/g, ".")
36
+ .replace(/\?/g, "[^/]")
37
37
  );
38
38
  }
39
39
  }
@@ -50,6 +50,32 @@ export function matchGlob(filepath, globPattern) {
50
50
  }
51
51
 
52
52
 
53
+ export const COMMAND_DEFINING_FILES = [
54
+ "package.json",
55
+ "package-lock.json",
56
+ "pnpm-lock.yaml",
57
+ "pnpm-workspace.yaml",
58
+ "yarn.lock",
59
+ "bun.lockb",
60
+ "turbo.json",
61
+ "nx.json",
62
+ "lerna.json",
63
+ "rush.json",
64
+ "Cargo.toml",
65
+ "Cargo.lock",
66
+ "go.mod",
67
+ "go.sum",
68
+ "pyproject.toml",
69
+ "setup.py",
70
+ "requirements.txt",
71
+ "Makefile",
72
+ "build.gradle",
73
+ "build.gradle.kts",
74
+ "pom.xml",
75
+ ".agent/jules.yml",
76
+ "jules.config.json"
77
+ ];
78
+
53
79
  /**
54
80
  * Lightweight Zero-Dependency YAML Pattern Extractor
55
81
  * Extracts forbidden_paths and allow_paths arrays from jules.yml.
@@ -153,16 +179,18 @@ export function runSelfAudit() {
153
179
  const targetBranch = process.env.BASE_BRANCH || "main";
154
180
  const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
155
181
  if (!SAFE_BRANCH.test(targetBranch)) {
156
- log.error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
157
- process.exit(2);
182
+ const err = new Error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
183
+ err.code = 2;
184
+ throw err;
158
185
  }
159
186
  log.info(`Target Branch: ${targetBranch}`);
160
187
 
161
188
  try {
162
189
  execFileSync("git", ["--version"], { stdio: "ignore" });
163
190
  } catch {
164
- log.error("FATAL: git is not installed or not in PATH.");
165
- process.exit(2);
191
+ const err = new Error("FATAL: git is not installed or not in PATH.");
192
+ err.code = 2;
193
+ throw err;
166
194
  }
167
195
 
168
196
  if (process.env.CI) {
@@ -180,8 +208,9 @@ export function runSelfAudit() {
180
208
  const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
181
209
 
182
210
  if (!mergeBase) {
183
- log.error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
184
- process.exit(2);
211
+ const err = new Error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
212
+ err.code = 2;
213
+ throw err;
185
214
  }
186
215
 
187
216
  log.info(`Merge-Base Hash: ${mergeBase}`);
@@ -191,6 +220,18 @@ export function runSelfAudit() {
191
220
  .map((f) => f.trim().replace(/^["']|["']$/g, ""))
192
221
  .filter(Boolean);
193
222
 
223
+ const commandFileChanges = rawDiffFiles.filter((file) =>
224
+ COMMAND_DEFINING_FILES.some((cmdFile) => file === cmdFile || file.endsWith(`/${cmdFile}`))
225
+ );
226
+
227
+ if (commandFileChanges.length > 0 && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "true" && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "1") {
228
+ const err = new Error(
229
+ "COMMAND FILE CHANGE DETECTED: " + commandFileChanges.join(", ") + ". Refusing to execute verification scripts from untrusted branch."
230
+ );
231
+ err.code = 3;
232
+ throw err;
233
+ }
234
+
194
235
  const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
195
236
  const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
196
237
 
@@ -205,21 +246,72 @@ export function runSelfAudit() {
205
246
 
206
247
  const violations = rawDiffFiles.filter((file) => {
207
248
  const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
208
- if (!isForbidden) return false;
209
- const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
210
- return !isAllowed;
249
+ if (isForbidden) return true; // Immutable forbidden path — CANNOT be overridden by allow_paths
250
+ if (allowedPatterns.length > 0) {
251
+ const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
252
+ return !isAllowed;
253
+ }
254
+ return false;
211
255
  });
212
256
 
213
257
  if (violations.length > 0) {
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);
258
+ const err = new Error("RESTRICTED FILE VIOLATION DETECTED: " + violations.join(", "));
259
+ err.code = 3;
260
+ throw err;
218
261
  }
219
262
  log.success("Restricted File Boundary Check: PASSED");
220
263
 
221
- log.info("Resolving Dynamic Verification Suite...");
222
- const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
264
+ log.info("Analyzing Diff Payload Size & Content...");
265
+ const diffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], true);
266
+ const diffBytes = Buffer.byteLength(diffPayload, "utf8");
267
+ const maxDiffBytes = 75 * 1024; // 75 KB
268
+
269
+ if (diffBytes > maxDiffBytes) {
270
+ const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${maxDiffBytes} bytes. Split the task.`);
271
+ err.code = 5;
272
+ throw err;
273
+ }
274
+
275
+ const redactedDiff = redactSecrets(diffPayload);
276
+ if (redactedDiff !== diffPayload) {
277
+ const err = new Error("SECRET LEAK PREVENTED: Secret-like content detected in diff. Aborting.");
278
+ err.code = 6;
279
+ throw err;
280
+ }
281
+ log.success(`Diff Inspection Check: PASSED (${diffBytes} bytes, No Secrets Detected)`);
282
+
283
+ log.info("Resolving Dynamic Verification Suite from Trusted Base Branch...");
284
+
285
+ const runId = crypto.randomBytes(4).toString("hex");
286
+ const trustedDir = path.join(os.tmpdir(), `jules-trusted-tree-${runId}`);
287
+ const tempArchive = path.join(os.tmpdir(), `jules-trusted-archive-${runId}.tar`);
288
+
289
+ let resolutionRoot;
290
+ try {
291
+ fs.mkdirSync(trustedDir, { recursive: true });
292
+ execFileSync("git", ["archive", mainRef, "-o", tempArchive]);
293
+ execFileSync("tar", ["-xf", tempArchive, "-C", trustedDir]);
294
+ resolutionRoot = trustedDir;
295
+ } catch (err) {
296
+ const error = new Error("FATAL: Failed to extract trusted base branch for command resolution. Refusing to fall back to working tree.\n" + err.message);
297
+ error.code = 2;
298
+ throw error;
299
+ }
300
+
301
+ let resolvedCmds;
302
+ try {
303
+ resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, resolutionRoot);
304
+ } finally {
305
+ try {
306
+ if (trustedDir && fs.existsSync(trustedDir)) {
307
+ fs.rmSync(trustedDir, { recursive: true, force: true });
308
+ }
309
+ if (tempArchive && fs.existsSync(tempArchive)) {
310
+ fs.rmSync(tempArchive, { force: true });
311
+ }
312
+ } catch (e) {}
313
+ }
314
+
223
315
  log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
224
316
 
225
317
  const startTime = Date.now();
@@ -254,7 +346,7 @@ export function runSelfAudit() {
254
346
 
255
347
  if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
256
348
  log.info("No build or test scripts found. Running git status check.");
257
- runCommand("git status");
349
+ execSync("git status", { stdio: "inherit" });
258
350
  }
259
351
 
260
352
  const durationMs = Date.now() - startTime;
@@ -270,60 +362,36 @@ export function runSelfAudit() {
270
362
  log.error("OODA SELF-HEALING FEEDBACK LOG:");
271
363
  log.dim(failureLog.slice(-1500));
272
364
 
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
365
  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);
366
+ const err = new Error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
367
+ err.code = 4;
368
+ throw err;
312
369
  }
313
370
 
314
371
  let oodaRetries = 0;
372
+ const oodaStateFile = path.resolve(process.cwd(), ".agent/state/ooda.json");
315
373
  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;
374
+ if (fs.existsSync(oodaStateFile)) {
375
+ const oodaData = JSON.parse(fs.readFileSync(oodaStateFile, "utf-8"));
376
+ oodaRetries = oodaData.attempts || 0;
377
+ } else {
378
+ const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
379
+ const matches = history.match(/OODA Auto-Repair/gi);
380
+ if (matches) {
381
+ oodaRetries = matches.length;
382
+ }
320
383
  }
321
384
  } catch (e) {
322
- // Ignorera git-fel här
385
+ // Ignorera fel här
323
386
  }
324
387
 
325
388
  if (oodaRetries < 3) {
326
389
  log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
390
+ try {
391
+ const stateDir = path.resolve(process.cwd(), ".agent/state");
392
+ if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
393
+ fs.writeFileSync(oodaStateFile, JSON.stringify({ attempts: oodaRetries + 1, lastAttempt: new Date().toISOString() }), "utf-8");
394
+ } catch (e) {}
327
395
  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
396
 
329
397
  const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
@@ -342,28 +410,29 @@ export function runSelfAudit() {
342
410
  } else {
343
411
  log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
344
412
  }
345
-
346
- process.exit(4);
413
+ const err = new Error("OODA Auto-Repair failed or exhausted.");
414
+ err.code = 4;
415
+ throw err;
347
416
  }
348
417
 
349
418
  log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
350
419
  }
351
420
 
352
421
  export function runPreflightSandbox() {
353
- log.header("Initializing Pre-Flight Sandbox (Steril Simulering)...");
422
+ log.header("Initializing Pre-Flight Sandbox (Sterile Simulation)...");
354
423
 
355
424
  const runId = crypto.randomBytes(4).toString("hex");
356
425
  const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
357
426
  const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
358
427
 
359
428
  try {
360
- log.step("[1/4]", `Skapar isolerad miljö i ${sandboxDir}...`);
429
+ log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
361
430
  fs.mkdirSync(sandboxDir, { recursive: true });
362
431
 
363
432
  execSync(`git archive HEAD -o "${tempArchive}"`);
364
433
  execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
365
434
 
366
- log.step("[2/4]", "Steriliserar miljövariabler...");
435
+ log.step("[2/5]", "Sterilizing environment variables...");
367
436
  const sterileEnv = { ...process.env };
368
437
  const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
369
438
  for (const key of stripKeys) {
@@ -372,11 +441,28 @@ export function runPreflightSandbox() {
372
441
  }
373
442
  }
374
443
 
375
- log.step("[3/4]", "Löser beroenden i sandlådan...");
444
+ log.step("[3/5]", "Resolving package manager and installing dependencies...");
445
+ let installCmd = "";
446
+ if (fs.existsSync(path.join(sandboxDir, "pnpm-lock.yaml"))) installCmd = "pnpm install --ignore-scripts";
447
+ else if (fs.existsSync(path.join(sandboxDir, "bun.lockb"))) installCmd = "bun install --ignore-scripts";
448
+ else if (fs.existsSync(path.join(sandboxDir, "yarn.lock"))) installCmd = "yarn install --ignore-scripts";
449
+ else if (fs.existsSync(path.join(sandboxDir, "package-lock.json"))) installCmd = "npm ci --ignore-scripts";
450
+
451
+ if (installCmd) {
452
+ try {
453
+ execSync(installCmd, { cwd: sandboxDir, stdio: "pipe", encoding: "utf-8" });
454
+ } catch (e) {
455
+ log.warn(`${installCmd} failed or wasn't needed. Output: ${e.stdout}\nError: ${e.stderr}\nProceeding...`);
456
+ }
457
+ } else {
458
+ log.info("Skipping dependency installation (no lockfile detected).");
459
+ }
460
+
461
+ log.step("[4/5]", "Resolving execution boundary in sandbox...");
376
462
  const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
377
463
 
378
464
  if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
379
- log.step("[4/4]", `Kör verifiering: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
465
+ log.step("[5/5]", `Running verification: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
380
466
  execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
381
467
  cwd: sandboxDir,
382
468
  env: sterileEnv,
@@ -384,14 +470,15 @@ export function runPreflightSandbox() {
384
470
  });
385
471
  log.success("Pre-Flight Passed. Zero epistemic drift detected.");
386
472
  } else {
387
- log.info("Inga test/bygg-kommandon hittades för verifiering.");
473
+ log.info("No test/build commands found for verification.");
388
474
  log.success("Pre-Flight Passed (No-Op).");
389
475
  }
390
476
  } 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);
477
+ log.error("\nPRE-FLIGHT FAILED!");
478
+ log.error("The agent would likely crash (The 5-Minute Drop-off) in the Web UI due to missing dependencies.");
479
+ const wrappedErr = new Error(err.message);
480
+ wrappedErr.code = 1;
481
+ throw wrappedErr;
395
482
  } finally {
396
483
  try {
397
484
  fs.rmSync(sandboxDir, { recursive: true, force: true });
@@ -406,10 +493,15 @@ export function runPreflightSandbox() {
406
493
 
407
494
  // Execute when invoked directly from CLI
408
495
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
409
- if (process.argv.includes("--preflight")) {
410
- runPreflightSandbox();
411
- } else {
412
- runSelfAudit();
496
+ try {
497
+ if (process.argv.includes("--preflight")) {
498
+ runPreflightSandbox();
499
+ } else {
500
+ runSelfAudit();
501
+ }
502
+ } catch (err) {
503
+ if (err.message) log.error(err.message);
504
+ process.exit(err.code || 1);
413
505
  }
414
506
  }
415
507
 
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, execFileSync } 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";
@@ -74,6 +74,9 @@ function reapOrphanedWorktrees() {
74
74
  fs.rmSync(wtPath, { recursive: true, force: true });
75
75
  } catch (_) {}
76
76
  }
77
+ try {
78
+ execFileSync("git", ["branch", "-D", `jules/${entry}`], { stdio: "ignore" });
79
+ } catch (_) {}
77
80
  }
78
81
  } catch (_) {}
79
82
  try {
@@ -31,7 +31,37 @@ async function readManifest() {
31
31
 
32
32
  async function writeManifest(manifest) {
33
33
  await fs.mkdir(path.dirname(MANIFEST_PATH), { recursive: true });
34
- await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf-8');
34
+ const tmp = MANIFEST_PATH + `.tmp-${process.pid}`;
35
+ await fs.writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
36
+ await fs.rename(tmp, MANIFEST_PATH);
37
+ }
38
+
39
+ async function withManifestLock(fn) {
40
+ const lockPath = MANIFEST_PATH + ".lock";
41
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
42
+ for (let i = 0; i < 50; i++) {
43
+ try {
44
+ const stats = await fs.stat(lockPath);
45
+ if (Date.now() - stats.mtimeMs > 5 * 60 * 1000) {
46
+ await fs.rm(lockPath, { force: true });
47
+ }
48
+ } catch (_) {}
49
+
50
+ let handle;
51
+ try {
52
+ handle = await fs.open(lockPath, "wx");
53
+ } catch (err) {
54
+ if (err.code !== "EEXIST") throw err;
55
+ await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
56
+ continue;
57
+ }
58
+ try { return await fn(); }
59
+ finally {
60
+ try { await handle.close(); } catch (_) {}
61
+ await fs.rm(lockPath, { force: true });
62
+ }
63
+ }
64
+ throw new Error("Could not acquire manifest lock");
35
65
  }
36
66
 
37
67
  function cleanExpiredLocks(manifest) {
@@ -47,7 +77,8 @@ function cleanExpiredLocks(manifest) {
47
77
  }
48
78
 
49
79
  async function acquireLocks(agentName, taskId, files) {
50
- const manifest = await readManifest();
80
+ await withManifestLock(async () => {
81
+ const manifest = await readManifest();
51
82
  cleanExpiredLocks(manifest);
52
83
 
53
84
  const now = Date.now();
@@ -65,13 +96,13 @@ async function acquireLocks(agentName, taskId, files) {
65
96
  }
66
97
  }
67
98
 
68
- if (conflicts.length > 0) {
69
- console.error(`ERROR: Cannot acquire locks for task ${taskId}. Conflicts detected:`);
70
- for (const conflict of conflicts) {
71
- console.error(` - ${conflict.file} is locked by ${conflict.lockedBy} (Task: ${conflict.task})`);
99
+ if (conflicts.length > 0) {
100
+ let msg = `Cannot acquire locks for task ${taskId}. Conflicts detected:\n`;
101
+ for (const conflict of conflicts) {
102
+ msg += ` - ${conflict.file} is locked by ${conflict.lockedBy} (Task: ${conflict.task})\n`;
103
+ }
104
+ throw new Error(msg.trim());
72
105
  }
73
- process.exit(1);
74
- }
75
106
 
76
107
  for (const file of files) {
77
108
  const normalizedFile = path.normalize(file);
@@ -83,12 +114,14 @@ async function acquireLocks(agentName, taskId, files) {
83
114
  };
84
115
  }
85
116
 
86
- await writeManifest(manifest);
87
- console.log(`SUCCESS: Acquired locks for task ${taskId} on ${files.length} files.`);
117
+ await writeManifest(manifest);
118
+ console.log(`SUCCESS: Acquired locks for task ${taskId} on ${files.length} files.`);
119
+ });
88
120
  }
89
121
 
90
122
  async function releaseLocks(taskId) {
91
- const manifest = await readManifest();
123
+ await withManifestLock(async () => {
124
+ const manifest = await readManifest();
92
125
  let releasedCount = 0;
93
126
 
94
127
  for (const [file, lock] of Object.entries(manifest)) {
@@ -98,12 +131,13 @@ async function releaseLocks(taskId) {
98
131
  }
99
132
  }
100
133
 
101
- if (releasedCount > 0) {
102
- await writeManifest(manifest);
103
- console.log(`SUCCESS: Released ${releasedCount} locks for task ${taskId}.`);
104
- } else {
105
- console.log(`INFO: No locks found for task ${taskId}.`);
106
- }
134
+ if (releasedCount > 0) {
135
+ await writeManifest(manifest);
136
+ console.log(`SUCCESS: Released ${releasedCount} locks for task ${taskId}.`);
137
+ } else {
138
+ console.log(`INFO: No locks found for task ${taskId}.`);
139
+ }
140
+ });
107
141
  }
108
142
 
109
143
  async function main() {
package/scripts/utils.mjs CHANGED
@@ -92,3 +92,142 @@ export function resolveMarkdownConflict(content) {
92
92
 
93
93
  return result.join("\n");
94
94
  }
95
+
96
+ export function calculateShannonEntropy(str) {
97
+ const len = str.length;
98
+ if (len === 0) return 0;
99
+ const frequencies = {};
100
+ for (let i = 0; i < len; i++) {
101
+ frequencies[str[i]] = (frequencies[str[i]] || 0) + 1;
102
+ }
103
+ return Object.values(frequencies).reduce((sum, count) => {
104
+ const p = count / len;
105
+ return sum - p * Math.log2(p);
106
+ }, 0);
107
+ }
108
+
109
+ export function redactSecrets(text) {
110
+ if (!text) return "";
111
+ let sanitized = text;
112
+
113
+ for (const [envKey, envVal] of Object.entries(process.env)) {
114
+ if (
115
+ envVal &&
116
+ (envVal.length >= 20 || calculateShannonEntropy(envVal) > 3.6) &&
117
+ /KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH/i.test(envKey)
118
+ ) {
119
+ if (sanitized.includes(envVal)) {
120
+ sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
121
+ }
122
+ }
123
+ }
124
+
125
+ const patterns = [
126
+ /\bgh[pousr]_[a-zA-Z0-9]{36,}\b/g,
127
+ /\bgithub_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}\b/g,
128
+ /\bAKIA[0-9A-Z]{16}\b/g,
129
+ /\bAIza[0-9A-Za-z_-]{30,40}\b/g,
130
+ /\bya29\.[a-zA-Z0-9_\-]{20,}\b/g,
131
+ /\bBearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
132
+ /\bsk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}\b/g,
133
+ /\bxox[baprs]-[a-zA-Z0-9\-]{10,}\b/g,
134
+ /\beyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\b/g,
135
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
136
+ /\bnpm_[a-zA-Z0-9]{36}\b/g,
137
+ /\b(?:sk|rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}\b/g,
138
+ ];
139
+ for (const pat of patterns) {
140
+ sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
141
+ }
142
+ return sanitized;
143
+ }
144
+
145
+ export function appendLedger(ledgerName, payload) {
146
+ const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state");
147
+ ensureDir(stateDir);
148
+ const ledgerPath = path.join(stateDir, `${ledgerName}.jsonl`);
149
+ const entry = JSON.stringify({
150
+ timestamp: new Date().toISOString(),
151
+ ...payload
152
+ }) + "\n";
153
+ fs.appendFileSync(ledgerPath, entry, "utf-8");
154
+ return ledgerPath;
155
+ }
156
+
157
+ export function checkDailyBudget(maxSessions = 300) {
158
+ const dateStr = new Date().toISOString().split("T")[0];
159
+ const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state");
160
+ const sessionsLedger = path.join(stateDir, "sessions.jsonl");
161
+
162
+ if (!fs.existsSync(sessionsLedger)) {
163
+ return { ok: true, used: 0, budget: maxSessions };
164
+ }
165
+
166
+ const content = fs.readFileSync(sessionsLedger, "utf-8");
167
+ const lines = content.split("\n").filter(Boolean);
168
+
169
+ let usedToday = 0;
170
+ for (const line of lines) {
171
+ try {
172
+ const entry = JSON.parse(line);
173
+ if (
174
+ entry.timestamp &&
175
+ entry.timestamp.startsWith(dateStr) &&
176
+ (entry.event === "session_dispatched" || entry.event === "budget_reserved" || !entry.event)
177
+ ) {
178
+ usedToday++;
179
+ }
180
+ } catch (e) {
181
+ // Ignore
182
+ }
183
+ }
184
+
185
+ return { ok: usedToday < maxSessions, used: usedToday, budget: maxSessions };
186
+ }
187
+
188
+ export function reserveDailyBudget(maxSessions = 300, taskKey = "") {
189
+ const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state");
190
+ ensureDir(stateDir);
191
+
192
+ const lockFile = path.join(stateDir, "budget.lock");
193
+ let fd;
194
+
195
+ try {
196
+ fd = fs.openSync(lockFile, "wx");
197
+ } catch {
198
+ // If lock fails, wait 50ms and retry once before returning locked state
199
+ try {
200
+ fd = fs.openSync(lockFile, "w");
201
+ } catch {
202
+ return { ok: false, reason: "locked", used: null, budget: maxSessions };
203
+ }
204
+ }
205
+
206
+ try {
207
+ const check = checkDailyBudget(maxSessions);
208
+ if (!check.ok) {
209
+ return check;
210
+ }
211
+
212
+ appendLedger("sessions", {
213
+ event: "budget_reserved",
214
+ task_key: taskKey,
215
+ status: "reserved"
216
+ });
217
+
218
+ return {
219
+ ok: true,
220
+ used: check.used + 1,
221
+ budget: maxSessions
222
+ };
223
+ } finally {
224
+ if (fd !== undefined) {
225
+ try {
226
+ fs.closeSync(fd);
227
+ } catch {}
228
+ try {
229
+ fs.rmSync(lockFile, { force: true });
230
+ } catch {}
231
+ }
232
+ }
233
+ }