jules-orchestrator-kit 0.8.0 → 0.8.5
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/rules/dynamic-guardrails.json +0 -4
- package/.env.example +3 -0
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +1 -1
- package/README.md +95 -14
- package/bin/init.js +4 -1
- package/index.mjs +5 -4
- package/package.json +12 -5
- package/scripts/command-resolver.mjs +17 -17
- package/scripts/jules-cleanup.mjs +15 -25
- package/scripts/jules-create.mjs +10 -5
- package/scripts/jules-dispatch.mjs +163 -63
- package/scripts/jules-merge-swarm.mjs +184 -0
- package/scripts/jules-nightly.mjs +11 -7
- package/scripts/jules-patch.mjs +111 -0
- package/scripts/jules-queue-runner.mjs +39 -15
- package/scripts/jules-scan-todos.mjs +1 -1
- package/scripts/jules-self-audit.mjs +137 -48
- package/scripts/jules-status.mjs +52 -47
- package/scripts/jules-swarm.mjs +138 -39
- package/scripts/lock-manager.mjs +17 -10
- package/scripts/utils.mjs +154 -8
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -2,46 +2,75 @@ 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";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { log, sleep } from "./utils.mjs";
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
12
|
+
let tasks = [];
|
|
13
|
+
let MAX_CONCURRENT = 3;
|
|
14
|
+
let STAGGER_MS = 1500;
|
|
15
|
+
let USE_WORKTREES = false;
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
19
|
-
log.error(`Tasks file not found: ${resolvedPath}`);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
17
|
+
const activeChildProcesses = new Set();
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
if (isMainModule) {
|
|
20
|
+
process.on("SIGINT", () => {
|
|
21
|
+
log.dim("SIGINT received. Cleaning up active processes and Git worktrees...");
|
|
22
|
+
cleanupAllWorktreesSync();
|
|
23
|
+
process.exit(130);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
process.on("SIGTERM", () => {
|
|
27
|
+
log.dim("SIGTERM received. Cleaning up active processes and Git worktrees...");
|
|
28
|
+
cleanupAllWorktreesSync();
|
|
29
|
+
process.exit(143);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const tasksFile = process.argv[2];
|
|
33
|
+
|
|
34
|
+
if (!tasksFile) {
|
|
35
|
+
log.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
|
|
36
|
+
log.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resolvedPath = path.resolve(process.cwd(), tasksFile);
|
|
41
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
42
|
+
log.error(`Tasks file not found: ${resolvedPath}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
|
|
47
|
+
if (!Array.isArray(tasks)) {
|
|
48
|
+
log.error("tasks.json must contain a JSON array of task objects.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
31
51
|
|
|
32
|
-
const
|
|
33
|
-
|
|
52
|
+
const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
|
|
53
|
+
MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
|
|
34
54
|
|
|
35
|
-
const
|
|
55
|
+
const parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
|
|
56
|
+
STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
58
|
+
USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
|
|
39
59
|
|
|
40
|
-
|
|
60
|
+
process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
61
|
+
log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
|
|
62
|
+
}
|
|
41
63
|
|
|
42
64
|
const activeWorktrees = new Set();
|
|
43
65
|
|
|
44
66
|
function cleanupAllWorktreesSync() {
|
|
67
|
+
for (const child of Array.from(activeChildProcesses)) {
|
|
68
|
+
try {
|
|
69
|
+
child.kill("SIGTERM");
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
activeChildProcesses.clear();
|
|
73
|
+
|
|
45
74
|
for (const item of Array.from(activeWorktrees)) {
|
|
46
75
|
if (item.wtDir && fs.existsSync(item.wtDir)) {
|
|
47
76
|
try {
|
|
@@ -84,17 +113,7 @@ function reapOrphanedWorktrees() {
|
|
|
84
113
|
} catch (_) {}
|
|
85
114
|
}
|
|
86
115
|
|
|
87
|
-
process.on("SIGINT", () => {
|
|
88
|
-
log.dim("SIGINT received. Cleaning up active Git worktrees...");
|
|
89
|
-
cleanupAllWorktreesSync();
|
|
90
|
-
process.exit(130);
|
|
91
|
-
});
|
|
92
116
|
|
|
93
|
-
process.on("SIGTERM", () => {
|
|
94
|
-
log.dim("SIGTERM received. Cleaning up active Git worktrees...");
|
|
95
|
-
cleanupAllWorktreesSync();
|
|
96
|
-
process.exit(143);
|
|
97
|
-
});
|
|
98
117
|
|
|
99
118
|
async function createWorktree(taskId) {
|
|
100
119
|
const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
@@ -149,7 +168,73 @@ async function removeWorktree(wtDir, branchName) {
|
|
|
149
168
|
}
|
|
150
169
|
}
|
|
151
170
|
|
|
171
|
+
export function buildSyncManifest(tasksList = []) {
|
|
172
|
+
const reservations = tasksList.map((t, idx) => ({
|
|
173
|
+
id: t.id || `task-${idx + 1}`,
|
|
174
|
+
title: t.title || "",
|
|
175
|
+
scope: t.scope || null,
|
|
176
|
+
reservedAt: new Date().toISOString()
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
version: 1,
|
|
181
|
+
generatedAt: new Date().toISOString(),
|
|
182
|
+
totalTasks: tasksList.length,
|
|
183
|
+
reservations
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
|
|
188
|
+
const agentDir = path.resolve(projectRoot, ".agent");
|
|
189
|
+
const manifestPath = path.join(agentDir, "sync-manifest.json");
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await fs.promises.mkdir(agentDir, { recursive: true });
|
|
193
|
+
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
194
|
+
|
|
195
|
+
const allowRemotePush = process.env.JULES_SWARM_REMOTE_PUSH === "true" || process.env.JULES_SWARM_REMOTE_PUSH === "1";
|
|
196
|
+
const isDryRun = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
197
|
+
|
|
198
|
+
if (isDryRun) {
|
|
199
|
+
log.dim("[DRY RUN] Generated .agent/sync-manifest.json reservation manifest (git push skipped).");
|
|
200
|
+
return { status: "DRY_RUN", path: manifestPath };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!allowRemotePush) {
|
|
204
|
+
log.info(`Generated local swarm reservation manifest: ${manifestPath}`);
|
|
205
|
+
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
|
|
210
|
+
} catch {
|
|
211
|
+
log.dim("Not in a git repository. Skipped git push for sync manifest.");
|
|
212
|
+
return { status: "SKIPPED_NOT_GIT", path: manifestPath };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const manifestBranch = `jules/swarm-manifest-${Date.now()}`;
|
|
217
|
+
await execFileAsync("git", ["add", manifestPath], { cwd: projectRoot });
|
|
218
|
+
await execFileAsync("git", ["commit", "-m", "chore(jules): reservation sync manifest [skip ci]"], { cwd: projectRoot });
|
|
219
|
+
log.info(`Committed reservation manifest to Git. Pushing to origin/${manifestBranch}...`);
|
|
220
|
+
|
|
221
|
+
await execFileAsync("git", ["push", "origin", `HEAD:${manifestBranch}`], { cwd: projectRoot });
|
|
222
|
+
log.success(`Pushed reservation manifest to origin/${manifestBranch}.`);
|
|
223
|
+
return { status: "PUSHED", branch: manifestBranch, path: manifestPath };
|
|
224
|
+
} catch (gitErr) {
|
|
225
|
+
log.warn(`⚠️ Git push reservation manifest failed (${gitErr.message}). Continuing local dispatch...`);
|
|
226
|
+
return { status: "FAILED_GIT", error: gitErr.message, path: manifestPath };
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
log.warn(`⚠️ Failed to build or save sync manifest: ${err.message}`);
|
|
230
|
+
return { status: "ERROR", error: err.message };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
152
234
|
async function runSwarm() {
|
|
235
|
+
const manifest = buildSyncManifest(tasks);
|
|
236
|
+
await pushReservationManifest(manifest);
|
|
237
|
+
|
|
153
238
|
if (USE_WORKTREES) {
|
|
154
239
|
reapOrphanedWorktrees();
|
|
155
240
|
}
|
|
@@ -188,7 +273,7 @@ async function runSwarm() {
|
|
|
188
273
|
|
|
189
274
|
try {
|
|
190
275
|
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
191
|
-
const
|
|
276
|
+
const child = execFile("node", [dispatchScript, task.title, effectivePrompt], {
|
|
192
277
|
cwd: execCwd,
|
|
193
278
|
timeout: 15 * 60 * 1000,
|
|
194
279
|
env: {
|
|
@@ -198,6 +283,18 @@ async function runSwarm() {
|
|
|
198
283
|
JULES_SLOT_TOTAL: String(tasks.length)
|
|
199
284
|
},
|
|
200
285
|
});
|
|
286
|
+
activeChildProcesses.add(child);
|
|
287
|
+
|
|
288
|
+
const { stdout } = await new Promise((resolve, reject) => {
|
|
289
|
+
let out = "";
|
|
290
|
+
child.stdout?.on("data", (data) => { out += data; });
|
|
291
|
+
child.on("error", reject);
|
|
292
|
+
child.on("close", (code) => {
|
|
293
|
+
activeChildProcesses.delete(child);
|
|
294
|
+
if (code === 0) resolve({ stdout: out });
|
|
295
|
+
else reject(new Error(`Process exited with code ${code}`));
|
|
296
|
+
});
|
|
297
|
+
});
|
|
201
298
|
if (stdout) log.dim(stdout.trim());
|
|
202
299
|
return { taskId, title: task.title, status: "SUCCESS" };
|
|
203
300
|
} catch (error) {
|
|
@@ -237,7 +334,9 @@ async function runSwarm() {
|
|
|
237
334
|
}
|
|
238
335
|
}
|
|
239
336
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
337
|
+
if (isMainModule) {
|
|
338
|
+
runSwarm().catch((err) => {
|
|
339
|
+
log.error(`Unhandled swarm rejection: ${err.message}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
}
|
package/scripts/lock-manager.mjs
CHANGED
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import fs from 'node:fs/promises';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
16
17
|
|
|
17
|
-
const MANIFEST_PATH = path.join(process.cwd(), '.agent', '
|
|
18
|
+
const MANIFEST_PATH = path.join(process.cwd(), '.agent', 'file-locks.json');
|
|
18
19
|
const TTL_MS = 20 * 60 * 1000; // 20 minutes
|
|
20
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
19
21
|
|
|
20
22
|
async function readManifest() {
|
|
21
23
|
try {
|
|
@@ -76,8 +78,11 @@ function cleanExpiredLocks(manifest) {
|
|
|
76
78
|
return changed;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
async function acquireLocks(agentName, taskId, files) {
|
|
81
|
+
async function acquireLocks(agentName, taskId, files, isUnattended = false) {
|
|
80
82
|
await withManifestLock(async () => {
|
|
83
|
+
if (isUnattended) {
|
|
84
|
+
console.log(`[lock-manager] Operating in unattended mode for task ${taskId}`);
|
|
85
|
+
}
|
|
81
86
|
const manifest = await readManifest();
|
|
82
87
|
cleanExpiredLocks(manifest);
|
|
83
88
|
|
|
@@ -148,20 +153,20 @@ async function main() {
|
|
|
148
153
|
const agentName = args[1];
|
|
149
154
|
const taskId = args[2];
|
|
150
155
|
const unattendedIndex = args.indexOf('--unattended');
|
|
151
|
-
|
|
156
|
+
const isUnattended = unattendedIndex !== -1;
|
|
152
157
|
let files = [];
|
|
153
|
-
if (
|
|
158
|
+
if (isUnattended) {
|
|
154
159
|
files = args.slice(3, unattendedIndex).concat(args.slice(unattendedIndex + 1));
|
|
155
160
|
} else {
|
|
156
161
|
files = args.slice(3);
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
if (!agentName || !taskId || files.length === 0) {
|
|
160
|
-
console.error('Usage: acquire <agent_name> <task_id> <file_paths...>');
|
|
165
|
+
console.error('Usage: acquire <agent_name> <task_id> <file_paths...> [--unattended]');
|
|
161
166
|
process.exit(1);
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
await acquireLocks(agentName, taskId, files);
|
|
169
|
+
await acquireLocks(agentName, taskId, files, isUnattended);
|
|
165
170
|
} else if (command === 'release') {
|
|
166
171
|
const taskId = args[1];
|
|
167
172
|
if (!taskId) {
|
|
@@ -175,7 +180,9 @@ async function main() {
|
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
if (isMainModule) {
|
|
184
|
+
main().catch(err => {
|
|
185
|
+
console.error('Unhandled error:', err);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
});
|
|
188
|
+
}
|
package/scripts/utils.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
|
|
6
7
|
// ANSI colors for standardizing CLI DX
|
|
@@ -29,6 +30,28 @@ export const log = {
|
|
|
29
30
|
|
|
30
31
|
export const sleep = promisify(setTimeout);
|
|
31
32
|
|
|
33
|
+
export function loadEnv(customCwd = process.cwd()) {
|
|
34
|
+
const envPath = path.resolve(customCwd, ".env");
|
|
35
|
+
if (fs.existsSync(envPath)) {
|
|
36
|
+
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
40
|
+
const eqIdx = trimmed.indexOf("=");
|
|
41
|
+
if (eqIdx > 0) {
|
|
42
|
+
const key = trimmed.slice(0, eqIdx).trim().replace(/^export\s+/, "");
|
|
43
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
44
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
45
|
+
val = val.slice(1, -1);
|
|
46
|
+
}
|
|
47
|
+
if (!process.env[key]) {
|
|
48
|
+
process.env[key] = val;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
export function ensureDir(dirPath) {
|
|
33
56
|
try {
|
|
34
57
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -37,11 +60,13 @@ export function ensureDir(dirPath) {
|
|
|
37
60
|
} catch (error) {
|
|
38
61
|
log.error(`EACCES: Insufficient permissions or failed to create directory: ${dirPath}`);
|
|
39
62
|
log.error(error.message);
|
|
40
|
-
|
|
63
|
+
const err = new Error(`Failed to create directory ${dirPath}: ${error.message}`);
|
|
64
|
+
err.code = "EACCES";
|
|
65
|
+
throw err;
|
|
41
66
|
}
|
|
42
67
|
}
|
|
43
68
|
|
|
44
|
-
export function logToHistory(filename, content
|
|
69
|
+
export function logToHistory(filename, content) {
|
|
45
70
|
const dateStr = new Date().toISOString().split("T")[0];
|
|
46
71
|
const historyDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/history");
|
|
47
72
|
ensureDir(historyDir);
|
|
@@ -110,9 +135,19 @@ export const HIGH_CONFIDENCE_PATTERNS = [
|
|
|
110
135
|
/\bgh[pousr]_[a-zA-Z0-9]{36,}\b/g,
|
|
111
136
|
/\bgithub_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}\b/g,
|
|
112
137
|
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
138
|
+
/\bASIA[0-9A-Z]{16}\b/g,
|
|
113
139
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
140
|
+
/\bPuTTY-User-Key-File-\d+:/g,
|
|
114
141
|
/\bnpm_[a-zA-Z0-9]{36}\b/g,
|
|
115
142
|
/\bsk_live_[0-9a-zA-Z]{24,}\b/g,
|
|
143
|
+
/\bsbp_[a-zA-Z0-9]{40,}\b/g,
|
|
144
|
+
/\bhf_[a-zA-Z0-9]{34,}\b/g,
|
|
145
|
+
/\bGOCSPX-[a-zA-Z0-9_-]{28}\b/g,
|
|
146
|
+
/\bglpat-[a-zA-Z0-9_-]{20,}\b/g,
|
|
147
|
+
/\bdop_v1_[a-f0-9]{64}\b/g,
|
|
148
|
+
/\bSG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}\b/g,
|
|
149
|
+
/\bwhsec_[a-zA-Z0-9]{24,}\b/g,
|
|
150
|
+
/\bshpat_[a-f0-9]{32}\b/g,
|
|
116
151
|
];
|
|
117
152
|
|
|
118
153
|
export const LOW_CONFIDENCE_PATTERNS = [
|
|
@@ -121,16 +156,23 @@ export const LOW_CONFIDENCE_PATTERNS = [
|
|
|
121
156
|
/\bBearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
|
|
122
157
|
/\bsk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}\b/g,
|
|
123
158
|
/\bxox[baprs]-[a-zA-Z0-9\-]{10,}\b/g,
|
|
159
|
+
/\bxapp-\d+-[a-zA-Z0-9_]+-\d+-[a-zA-Z0-9_]+\b/g,
|
|
124
160
|
/\beyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\b/g,
|
|
125
161
|
/\b(?:rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}\b/g,
|
|
126
162
|
/\bsk_test_[0-9a-zA-Z]{24,}\b/g,
|
|
163
|
+
/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/g,
|
|
164
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^:\s]+:[^@\s]+@[^:\s\/]+/gi,
|
|
165
|
+
/\bAuthorization:\s*Basic\s+[A-Za-z0-9+/=]{16,}/gi,
|
|
166
|
+
/[?&]sig=[A-Za-z0-9%+/=]{20,}/g,
|
|
127
167
|
];
|
|
128
168
|
|
|
129
169
|
export function hasHighConfidenceSecret(text) {
|
|
130
170
|
if (!text) return false;
|
|
131
171
|
return HIGH_CONFIDENCE_PATTERNS.some((pat) => {
|
|
132
172
|
pat.lastIndex = 0;
|
|
133
|
-
|
|
173
|
+
const res = pat.test(text);
|
|
174
|
+
pat.lastIndex = 0;
|
|
175
|
+
return res;
|
|
134
176
|
});
|
|
135
177
|
}
|
|
136
178
|
|
|
@@ -138,7 +180,9 @@ export function hasLowConfidenceSecret(text) {
|
|
|
138
180
|
if (!text) return false;
|
|
139
181
|
return LOW_CONFIDENCE_PATTERNS.some((pat) => {
|
|
140
182
|
pat.lastIndex = 0;
|
|
141
|
-
|
|
183
|
+
const res = pat.test(text);
|
|
184
|
+
pat.lastIndex = 0;
|
|
185
|
+
return res;
|
|
142
186
|
});
|
|
143
187
|
}
|
|
144
188
|
|
|
@@ -150,7 +194,7 @@ export function redactSecrets(text) {
|
|
|
150
194
|
if (
|
|
151
195
|
envVal &&
|
|
152
196
|
(envVal.length >= 20 || calculateShannonEntropy(envVal) > 3.6) &&
|
|
153
|
-
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH/i.test(envKey)
|
|
197
|
+
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH|PASSPHRASE|URL|URI|DSN|CONNECTION|ACCOUNT/i.test(envKey)
|
|
154
198
|
) {
|
|
155
199
|
if (sanitized.includes(envVal)) {
|
|
156
200
|
sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
|
|
@@ -160,15 +204,103 @@ export function redactSecrets(text) {
|
|
|
160
204
|
|
|
161
205
|
const allPatterns = [...HIGH_CONFIDENCE_PATTERNS, ...LOW_CONFIDENCE_PATTERNS];
|
|
162
206
|
for (const pat of allPatterns) {
|
|
207
|
+
pat.lastIndex = 0;
|
|
163
208
|
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
209
|
+
pat.lastIndex = 0;
|
|
164
210
|
}
|
|
165
211
|
return sanitized;
|
|
166
212
|
}
|
|
167
213
|
|
|
214
|
+
export function anonymizePii(text) {
|
|
215
|
+
if (!text) return "";
|
|
216
|
+
let sanitized = text;
|
|
217
|
+
|
|
218
|
+
// Mask email addresses
|
|
219
|
+
sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]");
|
|
220
|
+
|
|
221
|
+
// Mask IPv4 addresses (excluding 0.0.0.0, 127.0.0.1, 10.x, 192.168.x if needed, or mask all)
|
|
222
|
+
sanitized = sanitized.replace(/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g, (ip) => {
|
|
223
|
+
if (ip === "127.0.0.1" || ip === "0.0.0.0") return ip;
|
|
224
|
+
return "[REDACTED_IP]";
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Mask phone numbers (formatted with international prefix or standard delimiters)
|
|
228
|
+
sanitized = sanitized.replace(/(?:(?:\+\d{1,3}[\s-]?)|\b)\(?\d{2,4}\)?(?:[\s-]?\d{2,4}){2,4}\b/g, (phone) => {
|
|
229
|
+
const digitsOnly = phone.replace(/\D/g, "");
|
|
230
|
+
if (digitsOnly.length >= 7 && digitsOnly.length <= 15) {
|
|
231
|
+
return "[REDACTED_PHONE]";
|
|
232
|
+
}
|
|
233
|
+
return phone;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return sanitized;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function verifyLedgerIntegrity(filePath) {
|
|
240
|
+
if (!fs.existsSync(filePath)) {
|
|
241
|
+
return { ok: false, count: 0, reason: "File not found" };
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
245
|
+
const content = rawContent.replace(/\r\n/g, "\n").trim();
|
|
246
|
+
if (!content) return { ok: true, count: 0 };
|
|
247
|
+
|
|
248
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
249
|
+
let previousHash = "";
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
const line = lines[i];
|
|
253
|
+
let record;
|
|
254
|
+
try {
|
|
255
|
+
record = JSON.parse(line);
|
|
256
|
+
} catch (jsonErr) {
|
|
257
|
+
return { ok: false, count: i, reason: `Corrupted JSON record at line ${i + 1}: ${jsonErr.message}` };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!record.event || !record.timestamp) {
|
|
261
|
+
return { ok: false, count: i, reason: `Missing mandatory event/timestamp fields at line ${i + 1}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const lineHash = crypto.createHash("sha256").update(previousHash + line).digest("hex");
|
|
265
|
+
previousHash = lineHash;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { ok: true, count: lines.length, lastHash: previousHash };
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return { ok: false, count: 0, reason: err.message };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function pruneOldLedgers(stateDir, retentionDays = 30) {
|
|
275
|
+
try {
|
|
276
|
+
if (!fs.existsSync(stateDir)) return;
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const maxAgeMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
279
|
+
const files = fs.readdirSync(stateDir);
|
|
280
|
+
files.forEach((file) => {
|
|
281
|
+
if (file.endsWith(".jsonl")) {
|
|
282
|
+
const filePath = path.join(stateDir, file);
|
|
283
|
+
const stat = fs.statSync(filePath);
|
|
284
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
285
|
+
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
} catch (_) {}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getLocalDateString(date = new Date()) {
|
|
293
|
+
const year = date.getFullYear();
|
|
294
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
295
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
296
|
+
return `${year}-${month}-${day}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
168
299
|
export function getDailyLedgerPath(ledgerName = "sessions") {
|
|
169
|
-
const dateStr =
|
|
300
|
+
const dateStr = getLocalDateString();
|
|
170
301
|
const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state", ledgerName);
|
|
171
302
|
ensureDir(stateDir);
|
|
303
|
+
pruneOldLedgers(stateDir, 30);
|
|
172
304
|
return path.join(stateDir, `${dateStr}.jsonl`);
|
|
173
305
|
}
|
|
174
306
|
|
|
@@ -183,7 +315,6 @@ export function appendLedger(ledgerName, payload) {
|
|
|
183
315
|
}
|
|
184
316
|
|
|
185
317
|
export function checkDailyBudget(maxSessions = 300) {
|
|
186
|
-
const dateStr = new Date().toISOString().split("T")[0];
|
|
187
318
|
const ledgerPath = getDailyLedgerPath("sessions");
|
|
188
319
|
|
|
189
320
|
if (!fs.existsSync(ledgerPath)) {
|
|
@@ -275,3 +406,18 @@ export function reserveDailyBudget(maxSessions = 300, taskKey = "") {
|
|
|
275
406
|
}
|
|
276
407
|
}
|
|
277
408
|
}
|
|
409
|
+
|
|
410
|
+
export function getIsolatedCacheDir() {
|
|
411
|
+
const customCache = process.env.JULES_CACHE_DIR;
|
|
412
|
+
if (customCache) {
|
|
413
|
+
return path.resolve(customCache);
|
|
414
|
+
}
|
|
415
|
+
return path.join(os.homedir(), ".cache", "jules-orchestrator-kit");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function ensureSdkCacheIsolation() {
|
|
419
|
+
const cacheDir = getIsolatedCacheDir();
|
|
420
|
+
ensureDir(cacheDir);
|
|
421
|
+
process.env.JULES_CACHE_DIR = cacheDir;
|
|
422
|
+
return cacheDir;
|
|
423
|
+
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
name: Bug Report
|
|
2
|
-
description: Create a report to help us improve Jules Orchestrator Kit
|
|
3
|
-
title: "[BUG] "
|
|
4
|
-
labels: ["bug"]
|
|
5
|
-
assignees: []
|
|
6
|
-
body:
|
|
7
|
-
- type: markdown
|
|
8
|
-
attributes:
|
|
9
|
-
value: |
|
|
10
|
-
Thanks for taking the time to fill out this bug report!
|
|
11
|
-
- type: input
|
|
12
|
-
id: version
|
|
13
|
-
attributes:
|
|
14
|
-
label: "Node.js Version"
|
|
15
|
-
description: "What version of Node.js are you using? (e.g. v20.11.0)"
|
|
16
|
-
placeholder: "v20.x"
|
|
17
|
-
validations:
|
|
18
|
-
required: true
|
|
19
|
-
- type: dropdown
|
|
20
|
-
id: package_manager
|
|
21
|
-
attributes:
|
|
22
|
-
label: "Package Manager"
|
|
23
|
-
description: "Which package manager are you using?"
|
|
24
|
-
options:
|
|
25
|
-
- npm
|
|
26
|
-
- pnpm
|
|
27
|
-
- yarn
|
|
28
|
-
- bun
|
|
29
|
-
validations:
|
|
30
|
-
required: true
|
|
31
|
-
- type: dropdown
|
|
32
|
-
id: execution_mode
|
|
33
|
-
attributes:
|
|
34
|
-
label: "Execution Mode"
|
|
35
|
-
description: "How are you running Jules?"
|
|
36
|
-
options:
|
|
37
|
-
- API Mode (JULES_API_KEY set)
|
|
38
|
-
- CLI Mode (Local jules binary)
|
|
39
|
-
validations:
|
|
40
|
-
required: true
|
|
41
|
-
- type: textarea
|
|
42
|
-
id: description
|
|
43
|
-
attributes:
|
|
44
|
-
label: "Bug Description"
|
|
45
|
-
description: "A clear and concise description of what the bug is."
|
|
46
|
-
placeholder: "When I run npm run jules:queue..."
|
|
47
|
-
validations:
|
|
48
|
-
required: true
|
|
49
|
-
- type: textarea
|
|
50
|
-
id: reproduction
|
|
51
|
-
attributes:
|
|
52
|
-
label: "Steps to Reproduce"
|
|
53
|
-
description: "Steps to reproduce the behavior."
|
|
54
|
-
placeholder: "1. Go to...\n2. Click on...\n3. See error"
|
|
55
|
-
validations:
|
|
56
|
-
required: true
|
|
57
|
-
- type: textarea
|
|
58
|
-
id: expected
|
|
59
|
-
attributes:
|
|
60
|
-
label: "Expected Behavior"
|
|
61
|
-
description: "A clear and concise description of what you expected to happen."
|
|
62
|
-
validations:
|
|
63
|
-
required: true
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Feature Request
|
|
2
|
-
description: Suggest an idea for Jules Orchestrator Kit
|
|
3
|
-
title: "[FEATURE] "
|
|
4
|
-
labels: ["enhancement"]
|
|
5
|
-
assignees: []
|
|
6
|
-
body:
|
|
7
|
-
- type: markdown
|
|
8
|
-
attributes:
|
|
9
|
-
value: |
|
|
10
|
-
Thanks for suggesting a new feature!
|
|
11
|
-
- type: textarea
|
|
12
|
-
id: description
|
|
13
|
-
attributes:
|
|
14
|
-
label: "Is your feature request related to a problem? Please describe."
|
|
15
|
-
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
|
|
16
|
-
placeholder: "..."
|
|
17
|
-
validations:
|
|
18
|
-
required: true
|
|
19
|
-
- type: textarea
|
|
20
|
-
id: solution
|
|
21
|
-
attributes:
|
|
22
|
-
label: "Describe the solution you'd like"
|
|
23
|
-
description: "A clear and concise description of what you want to happen."
|
|
24
|
-
placeholder: "..."
|
|
25
|
-
validations:
|
|
26
|
-
required: true
|
|
27
|
-
- type: textarea
|
|
28
|
-
id: alternatives
|
|
29
|
-
attributes:
|
|
30
|
-
label: "Describe alternatives you've considered"
|
|
31
|
-
description: "A clear and concise description of any alternative solutions or features you've considered."
|
|
32
|
-
placeholder: "..."
|
|
33
|
-
validations:
|
|
34
|
-
required: false
|
|
Binary file
|