jules-orchestrator-kit 0.5.2 → 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/.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 +2 -2
- package/README.md +99 -204
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/scripts/command-resolver.mjs +2 -2
- package/scripts/jules-create.mjs +22 -2
- package/scripts/jules-dispatch.mjs +36 -39
- package/scripts/jules-queue-runner.mjs +48 -3
- package/scripts/jules-scan-todos.mjs +7 -7
- package/scripts/jules-self-audit.mjs +127 -18
- package/scripts/jules-swarm.mjs +4 -1
- package/scripts/lock-manager.mjs +51 -17
- package/scripts/utils.mjs +139 -0
|
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveProjectCommands } from "./command-resolver.mjs";
|
|
8
|
-
import { log, logToHistory } from "./utils.mjs";
|
|
8
|
+
import { log, logToHistory, redactSecrets, appendLedger, checkDailyBudget, reserveDailyBudget } from "./utils.mjs";
|
|
9
9
|
|
|
10
10
|
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
@@ -45,40 +45,14 @@ if (isMainModule) {
|
|
|
45
45
|
loadEnv();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
55
|
-
if (
|
|
56
|
-
envVal &&
|
|
57
|
-
envVal.length >= 20 &&
|
|
58
|
-
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH/i.test(envKey)
|
|
59
|
-
) {
|
|
60
|
-
if (sanitized.includes(envVal)) {
|
|
61
|
-
sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
48
|
+
function parseRetryAfter(res) {
|
|
49
|
+
const raw = res.headers.get("Retry-After");
|
|
50
|
+
let waitSec = Number.parseInt(raw ?? "", 10);
|
|
51
|
+
if (!Number.isFinite(waitSec)) {
|
|
52
|
+
const asDate = raw ? Date.parse(raw) : NaN;
|
|
53
|
+
waitSec = Number.isFinite(asDate) ? Math.ceil((asDate - Date.now()) / 1000) : 5;
|
|
64
54
|
}
|
|
65
|
-
|
|
66
|
-
const patterns = [
|
|
67
|
-
/gh[pusr]_[a-zA-Z0-9]{36}/g,
|
|
68
|
-
/github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
|
|
69
|
-
/AKIA[0-9A-Z]{16}/g,
|
|
70
|
-
/AIzaSy[a-zA-Z0-9_\-]{33}/g,
|
|
71
|
-
/ya29\.[a-zA-Z0-9_\-]{20,}/g,
|
|
72
|
-
/Bearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
|
|
73
|
-
/sk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}/g,
|
|
74
|
-
/xox[baprs]-[a-zA-Z0-9\-]{10,}/g,
|
|
75
|
-
/eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g,
|
|
76
|
-
/-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY-----/g,
|
|
77
|
-
];
|
|
78
|
-
for (const pat of patterns) {
|
|
79
|
-
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
80
|
-
}
|
|
81
|
-
return sanitized;
|
|
55
|
+
return Math.min(Math.max(waitSec, 0), 60);
|
|
82
56
|
}
|
|
83
57
|
|
|
84
58
|
const ASTRO_TRIGGER_RE = /\b(?:astro|components|pages|src\/.*\.astro)\b/i;
|
|
@@ -129,6 +103,7 @@ let fullPrompt = "";
|
|
|
129
103
|
let historyFile = "";
|
|
130
104
|
let tmpPayloadFile = "";
|
|
131
105
|
let tmpDir = "";
|
|
106
|
+
let taskKey = "";
|
|
132
107
|
|
|
133
108
|
const ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
134
109
|
export function getAlphaRange(index, total) {
|
|
@@ -214,10 +189,13 @@ if (isMainModule) {
|
|
|
214
189
|
log.info(`Dispatching task to Google Jules: "${taskTitle}"...`);
|
|
215
190
|
log.success(`Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
|
|
216
191
|
|
|
217
|
-
const
|
|
192
|
+
const hashInput = [repo || "repoless", taskTitle, rawPrompt, process.env.BASE_BRANCH || "main", dateStr].join("\u0000");
|
|
193
|
+
taskKey = crypto.createHash("sha256").update(hashInput).digest("hex").slice(0, 12);
|
|
194
|
+
|
|
218
195
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
|
|
219
|
-
tmpPayloadFile = path.join(tmpDir, `payload_${
|
|
196
|
+
tmpPayloadFile = path.join(tmpDir, `payload_${taskKey}.json`);
|
|
220
197
|
fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
|
|
198
|
+
|
|
221
199
|
}
|
|
222
200
|
|
|
223
201
|
function cleanupTmp() {
|
|
@@ -239,6 +217,14 @@ async function executeDispatch() {
|
|
|
239
217
|
return;
|
|
240
218
|
}
|
|
241
219
|
|
|
220
|
+
const budgetLimit = process.env.JULES_DAILY_BUDGET ? parseInt(process.env.JULES_DAILY_BUDGET, 10) : 300;
|
|
221
|
+
const budgetCheck = reserveDailyBudget(budgetLimit, taskKey);
|
|
222
|
+
|
|
223
|
+
if (!budgetCheck.ok) {
|
|
224
|
+
log.warn(`Daily budget exhausted or locked (${budgetCheck.used}/${budgetCheck.budget} sessions). Aborting dispatch.`);
|
|
225
|
+
process.exit(7); // Budget exhausted exit code
|
|
226
|
+
}
|
|
227
|
+
|
|
242
228
|
if (apiKey && (repo || isRepoless)) {
|
|
243
229
|
const payload = {
|
|
244
230
|
title: taskTitle,
|
|
@@ -275,6 +261,15 @@ async function executeDispatch() {
|
|
|
275
261
|
signal: AbortSignal.timeout(30000)
|
|
276
262
|
});
|
|
277
263
|
|
|
264
|
+
if (res.status === 429) {
|
|
265
|
+
const waitSec = parseRetryAfter(res);
|
|
266
|
+
if (attempts < maxAttempts) {
|
|
267
|
+
log.warn(`⚠️ REST API Rate Limit Exceeded (HTTP 429). Retrying after ${waitSec}s...`);
|
|
268
|
+
await new Promise((r) => setTimeout(r, waitSec * 1000));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
278
273
|
if (res.ok || res.status === 429 || res.status === 400 || res.status === 401) {
|
|
279
274
|
break;
|
|
280
275
|
}
|
|
@@ -303,8 +298,8 @@ async function executeDispatch() {
|
|
|
303
298
|
}
|
|
304
299
|
|
|
305
300
|
if (res.status === 429) {
|
|
306
|
-
const
|
|
307
|
-
const err = new Error(`❌ Jules REST API Rate Limit Exceeded (HTTP 429). Retry after: ${
|
|
301
|
+
const waitSec = parseRetryAfter(res);
|
|
302
|
+
const err = new Error(`❌ Jules REST API Rate Limit Exceeded (HTTP 429). Retry after: ${waitSec}s`);
|
|
308
303
|
err.code = 1;
|
|
309
304
|
throw err;
|
|
310
305
|
}
|
|
@@ -330,6 +325,7 @@ async function executeDispatch() {
|
|
|
330
325
|
sessionId = sId || data.name || data.id || sessionId;
|
|
331
326
|
log.success(`REST API Dispatch response: Session ${sessionId} created successfully.`);
|
|
332
327
|
fs.appendFileSync(historyFile, `\n## Session\n- ID: \`${sessionId}\`\n`);
|
|
328
|
+
appendLedger("sessions", { event: "session_dispatched", task_key: taskKey, session_id: sessionId, status: "dispatched", task_title: taskTitle, mode: "api" });
|
|
333
329
|
} catch (jsonErr) {
|
|
334
330
|
log.warn("⚠️ Failed to parse REST API JSON response:", jsonErr.message);
|
|
335
331
|
}
|
|
@@ -361,6 +357,7 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
|
361
357
|
log.step("💻", `Executing: jules ${args.join(" ")} (prompt passed via stdin)...`);
|
|
362
358
|
execFileSync("jules", args, { input: promptToSend, stdio: ["pipe", "inherit", "inherit"] });
|
|
363
359
|
log.success(`Successfully dispatched task "${title}" to Jules CLI.`);
|
|
360
|
+
appendLedger("sessions", { event: "session_dispatched", task_key: taskKey, session_id: "CLI", status: "dispatched", task_title: title, mode: "cli" });
|
|
364
361
|
} catch (error) {
|
|
365
362
|
const err = new Error(`Failed to dispatch task to Jules CLI: ${error.message}`);
|
|
366
363
|
err.code = 1;
|
|
@@ -371,6 +368,6 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
|
371
368
|
if (isMainModule) {
|
|
372
369
|
executeDispatch().catch((err) => {
|
|
373
370
|
log.error(`❌ Fatal unhandled rejection in dispatch: ${err.message}`);
|
|
374
|
-
process.exit(1);
|
|
371
|
+
process.exit(err.code || 1);
|
|
375
372
|
});
|
|
376
373
|
}
|
|
@@ -13,6 +13,7 @@ const dispatchScript = path.resolve(__dirname, "jules-dispatch.mjs");
|
|
|
13
13
|
|
|
14
14
|
const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
15
15
|
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
16
|
+
const failedDir = path.resolve(process.cwd(), ".agent/jules-queue/failed");
|
|
16
17
|
const queueLogFile = path.join(queueDir, "queue.jsonl");
|
|
17
18
|
|
|
18
19
|
const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
|
|
@@ -23,8 +24,22 @@ if (!fs.existsSync(queueDir)) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
ensureDir(completedDir);
|
|
27
|
+
ensureDir(failedDir);
|
|
26
28
|
ensureDir(processingDir);
|
|
27
29
|
|
|
30
|
+
// Recover any tasks left in .processing from a previous run or crash
|
|
31
|
+
const STALE_MS = 15 * 60 * 1000;
|
|
32
|
+
const processingFiles = fs.readdirSync(processingDir).filter(f => f.endsWith(".md"));
|
|
33
|
+
for (const f of processingFiles) {
|
|
34
|
+
const procPath = path.join(processingDir, f);
|
|
35
|
+
try {
|
|
36
|
+
if (Date.now() - fs.statSync(procPath).mtimeMs < STALE_MS) continue;
|
|
37
|
+
const backToQueue = path.join(queueDir, f);
|
|
38
|
+
fs.renameSync(procPath, backToQueue);
|
|
39
|
+
log.info(`Recovered stale task ${f} from .processing/`);
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
function logQueueState(file, status, error = null) {
|
|
29
44
|
const entry = JSON.stringify({
|
|
30
45
|
timestamp: new Date().toISOString(),
|
|
@@ -97,14 +112,21 @@ async function runQueue() {
|
|
|
97
112
|
// Atomic claim: move from queueDir to processingDir before dispatching
|
|
98
113
|
try {
|
|
99
114
|
safeMoveSync(filePath, processingPath);
|
|
115
|
+
const now = new Date();
|
|
116
|
+
fs.utimesSync(processingPath, now, now);
|
|
100
117
|
} catch (claimErr) {
|
|
101
118
|
// Another runner already claimed this task file or file missing
|
|
102
119
|
return;
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
let title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
106
123
|
|
|
107
124
|
const content = fs.readFileSync(processingPath, "utf-8").trim();
|
|
125
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
126
|
+
if (h1Match && h1Match[1].trim()) {
|
|
127
|
+
title = h1Match[1].trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
if (!content || content.length < 10) {
|
|
109
131
|
log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
|
|
110
132
|
const destPath = path.join(completedDir, file);
|
|
@@ -117,7 +139,7 @@ async function runQueue() {
|
|
|
117
139
|
logQueueState(file, "RUNNING");
|
|
118
140
|
|
|
119
141
|
try {
|
|
120
|
-
await execFileAsync("node", [dispatchScript, title, processingPath]);
|
|
142
|
+
await execFileAsync("node", [dispatchScript, title, processingPath], { timeout: 10 * 60 * 1000 });
|
|
121
143
|
|
|
122
144
|
const destPath = path.join(completedDir, file);
|
|
123
145
|
await safeMoveAsync(processingPath, destPath);
|
|
@@ -125,7 +147,30 @@ async function runQueue() {
|
|
|
125
147
|
log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
|
|
126
148
|
} catch (error) {
|
|
127
149
|
logQueueState(file, "FAILED", error);
|
|
128
|
-
|
|
150
|
+
|
|
151
|
+
let attempts = 0;
|
|
152
|
+
const attemptMatch = content.match(/^attempts:\s*(\d+)/m);
|
|
153
|
+
if (attemptMatch) attempts = parseInt(attemptMatch[1], 10);
|
|
154
|
+
|
|
155
|
+
if (attempts >= 2) {
|
|
156
|
+
const destPath = path.join(failedDir, file);
|
|
157
|
+
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
158
|
+
log.error(`Task [${title}] failed 3 times. Moved to failed/. Error: ${error.message}`);
|
|
159
|
+
} else {
|
|
160
|
+
let newContent = content;
|
|
161
|
+
if (attemptMatch) {
|
|
162
|
+
newContent = content.replace(/^attempts:\s*\d+/m, `attempts: ${attempts + 1}`);
|
|
163
|
+
} else if (content.startsWith("---\n")) {
|
|
164
|
+
newContent = content.replace(/^---\n/, `---\nattempts: 1\n`);
|
|
165
|
+
} else {
|
|
166
|
+
newContent = `---\nattempts: 1\n---\n\n` + content;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
fs.writeFileSync(processingPath, newContent);
|
|
170
|
+
await safeMoveAsync(processingPath, filePath);
|
|
171
|
+
} catch (_) {}
|
|
172
|
+
log.warn(`Task [${title}] failed. Re-queued (Attempt ${attempts + 1}). Error: ${error.message}`);
|
|
173
|
+
}
|
|
129
174
|
}
|
|
130
175
|
})
|
|
131
176
|
);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { log, ensureDir } from "./utils.mjs";
|
|
12
13
|
|
|
13
14
|
const IGNORE_DIRS = new Set([
|
|
@@ -22,9 +23,8 @@ const IGNORE_DIRS = new Set([
|
|
|
22
23
|
"coverage",
|
|
23
24
|
".next",
|
|
24
25
|
".turbo",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"tmp_repos3"
|
|
26
|
+
"test",
|
|
27
|
+
"scripts/jules-scan-todos.mjs"
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
30
|
const IGNORE_EXTS = new Set([
|
|
@@ -61,15 +61,15 @@ export function scanCodebaseForTodos(rootDir = process.cwd(), options = {}) {
|
|
|
61
61
|
walk(fullPath);
|
|
62
62
|
} else if (entry.isFile()) {
|
|
63
63
|
const ext = path.extname(entry.name).toLowerCase();
|
|
64
|
-
if (IGNORE_EXTS.has(ext)) continue;
|
|
64
|
+
if (IGNORE_EXTS.has(ext) || IGNORE_DIRS.has(relPath)) continue;
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
67
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
68
68
|
const lines = content.split("\n");
|
|
69
69
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
70
70
|
const line = lines[lineIdx];
|
|
71
|
-
// Ignore false positives from regex definitions
|
|
72
|
-
if (/TAG_REGEX|
|
|
71
|
+
// Ignore false positives from regex definitions or scanner string literals
|
|
72
|
+
if (/TAG_REGEX|tasks\.some|log\.info\("Zero TODO|\/FIXME comments/i.test(line)) {
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
const match = line.match(TAG_REGEX);
|
|
@@ -137,6 +137,6 @@ export function runScanner() {
|
|
|
137
137
|
log.info(`Run \`npx jules-swarm ${path.relative(process.cwd(), outputPath)}\` to execute all tasks in parallel.`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.url
|
|
140
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
141
141
|
runScanner();
|
|
142
142
|
}
|
|
@@ -5,12 +5,13 @@ 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
|
+
if (ignoreError) return "";
|
|
14
15
|
const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
|
|
15
16
|
err.code = 1;
|
|
16
17
|
throw err;
|
|
@@ -32,7 +33,7 @@ export function matchGlob(filepath, globPattern) {
|
|
|
32
33
|
seg
|
|
33
34
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
34
35
|
.replace(/\*/g, "[^/]*")
|
|
35
|
-
.replace(/\?/g, "
|
|
36
|
+
.replace(/\?/g, "[^/]")
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -49,6 +50,32 @@ export function matchGlob(filepath, globPattern) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
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
|
+
|
|
52
79
|
/**
|
|
53
80
|
* Lightweight Zero-Dependency YAML Pattern Extractor
|
|
54
81
|
* Extracts forbidden_paths and allow_paths arrays from jules.yml.
|
|
@@ -193,6 +220,18 @@ export function runSelfAudit() {
|
|
|
193
220
|
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
194
221
|
.filter(Boolean);
|
|
195
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
|
+
|
|
196
235
|
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
|
197
236
|
const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
|
|
198
237
|
|
|
@@ -207,20 +246,72 @@ export function runSelfAudit() {
|
|
|
207
246
|
|
|
208
247
|
const violations = rawDiffFiles.filter((file) => {
|
|
209
248
|
const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
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;
|
|
213
255
|
});
|
|
214
256
|
|
|
215
257
|
if (violations.length > 0) {
|
|
216
|
-
const err = new Error("RESTRICTED FILE VIOLATION DETECTED
|
|
258
|
+
const err = new Error("RESTRICTED FILE VIOLATION DETECTED: " + violations.join(", "));
|
|
217
259
|
err.code = 3;
|
|
218
260
|
throw err;
|
|
219
261
|
}
|
|
220
262
|
log.success("Restricted File Boundary Check: PASSED");
|
|
221
263
|
|
|
222
|
-
log.info("
|
|
223
|
-
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
|
+
|
|
224
315
|
log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
|
|
225
316
|
|
|
226
317
|
const startTime = Date.now();
|
|
@@ -278,18 +369,29 @@ export function runSelfAudit() {
|
|
|
278
369
|
}
|
|
279
370
|
|
|
280
371
|
let oodaRetries = 0;
|
|
372
|
+
const oodaStateFile = path.resolve(process.cwd(), ".agent/state/ooda.json");
|
|
281
373
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
383
|
}
|
|
287
384
|
} catch (e) {
|
|
288
|
-
// Ignorera
|
|
385
|
+
// Ignorera fel här
|
|
289
386
|
}
|
|
290
387
|
|
|
291
388
|
if (oodaRetries < 3) {
|
|
292
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) {}
|
|
293
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.`;
|
|
294
396
|
|
|
295
397
|
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
@@ -339,14 +441,21 @@ export function runPreflightSandbox() {
|
|
|
339
441
|
}
|
|
340
442
|
}
|
|
341
443
|
|
|
342
|
-
log.step("[3/5]", "
|
|
343
|
-
|
|
344
|
-
if (fs.existsSync(
|
|
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) {
|
|
345
452
|
try {
|
|
346
|
-
execSync(
|
|
453
|
+
execSync(installCmd, { cwd: sandboxDir, stdio: "pipe", encoding: "utf-8" });
|
|
347
454
|
} catch (e) {
|
|
348
|
-
log.warn(
|
|
455
|
+
log.warn(`${installCmd} failed or wasn't needed. Output: ${e.stdout}\nError: ${e.stderr}\nProceeding...`);
|
|
349
456
|
}
|
|
457
|
+
} else {
|
|
458
|
+
log.info("Skipping dependency installation (no lockfile detected).");
|
|
350
459
|
}
|
|
351
460
|
|
|
352
461
|
log.step("[4/5]", "Resolving execution boundary in sandbox...");
|
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() {
|