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.
- package/.agent/jules.yml +1 -1
- package/.env.example +49 -3
- package/.github/ISSUE_TEMPLATE/bug_report.md +63 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +34 -0
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +3 -2
- package/.github/workflows/jules-audit.yml +8 -2
- package/README.md +99 -204
- package/index.mjs +2 -2
- package/package.json +3 -2
- package/scripts/command-resolver.mjs +2 -2
- package/scripts/jules-create.mjs +22 -2
- package/scripts/jules-dispatch.mjs +55 -45
- package/scripts/jules-queue-runner.mjs +48 -3
- package/scripts/jules-scan-todos.mjs +7 -7
- package/scripts/jules-self-audit.mjs +172 -80
- package/scripts/jules-swarm.mjs +4 -1
- package/scripts/lock-manager.mjs +51 -17
- package/scripts/utils.mjs +139 -0
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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 (
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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("
|
|
222
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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/
|
|
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/
|
|
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/
|
|
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("[
|
|
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("
|
|
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
|
|
392
|
-
log.error("
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -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 {
|
package/scripts/lock-manager.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|