jules-orchestrator-kit 0.8.1 → 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.
@@ -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 tasksFile = process.argv[2];
10
+ const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
10
11
 
11
- if (!tasksFile) {
12
- log.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
13
- log.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
14
- process.exit(1);
15
- }
12
+ let tasks = [];
13
+ let MAX_CONCURRENT = 3;
14
+ let STAGGER_MS = 1500;
15
+ let USE_WORKTREES = false;
16
16
 
17
- const resolvedPath = path.resolve(process.cwd(), tasksFile);
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
- const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
24
- if (!Array.isArray(tasks)) {
25
- log.error("tasks.json must contain a JSON array of task objects.");
26
- process.exit(1);
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
- const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
30
- const MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
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 parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
33
- const STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
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 USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
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
- // Ensure root project path is preserved for history logs even when running in worktrees
38
- process.env.JULES_PROJECT_ROOT = process.cwd();
58
+ USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
39
59
 
40
- log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
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 { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
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
- runSwarm().catch((err) => {
241
- log.error(`Unhandled swarm rejection: ${err.message}`);
242
- process.exit(1);
243
- });
337
+ if (isMainModule) {
338
+ runSwarm().catch((err) => {
339
+ log.error(`Unhandled swarm rejection: ${err.message}`);
340
+ process.exit(1);
341
+ });
342
+ }
@@ -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', 'sync-manifest.json');
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 (unattendedIndex !== -1) {
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
- main().catch(err => {
179
- console.error('Unhandled error:', err);
180
- process.exit(1);
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 { execFileSync } from "node:child_process";
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
- process.exit(1);
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, type = "audit") {
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
- return pat.test(text);
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
- return pat.test(text);
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,11 +204,73 @@ 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
+
168
274
  export function pruneOldLedgers(stateDir, retentionDays = 30) {
169
275
  try {
170
276
  if (!fs.existsSync(stateDir)) return;
@@ -183,8 +289,15 @@ export function pruneOldLedgers(stateDir, retentionDays = 30) {
183
289
  } catch (_) {}
184
290
  }
185
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
+
186
299
  export function getDailyLedgerPath(ledgerName = "sessions") {
187
- const dateStr = new Date().toISOString().split("T")[0];
300
+ const dateStr = getLocalDateString();
188
301
  const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state", ledgerName);
189
302
  ensureDir(stateDir);
190
303
  pruneOldLedgers(stateDir, 30);
@@ -293,3 +406,18 @@ export function reserveDailyBudget(maxSessions = 300, taskKey = "") {
293
406
  }
294
407
  }
295
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
@@ -1,57 +0,0 @@
1
- name: Agent Scope Guard
2
-
3
- on:
4
- pull_request:
5
- types: [opened, synchronize, reopened]
6
-
7
- jobs:
8
- scope-guard:
9
- runs-on: ubuntu-latest
10
- if: startsWith(github.head_ref, 'jules/')
11
- steps:
12
- - name: Checkout repository
13
- uses: actions/checkout@v4
14
- with:
15
- fetch-depth: 0
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v4
19
- with:
20
- node-version: '20'
21
-
22
- - name: Verify Scope
23
- env:
24
- BASE_SHA: ${{ github.event.pull_request.base.sha }}
25
- HEAD_SHA: ${{ github.event.pull_request.head.sha }}
26
- BASE_REF: ${{ github.base_ref }}
27
- run: |
28
- echo "Checking files modified in PR..."
29
- MODIFIED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)
30
-
31
- # Read protected paths from origin/${{ github.base_ref }}
32
- PROTECTED_PATHS=$(node -e "const paths = JSON.parse(require('child_process').execSync('git show origin/' + process.env.BASE_REF + ':.agent/protected-paths.json', {encoding:'utf8'})).protected; console.log(paths.join(' '));")
33
-
34
- echo "Protected patterns: $PROTECTED_PATHS"
35
-
36
- VIOLATION_FOUND=0
37
-
38
- # Use micromatch or similar simple matching via bash for CI simplicity.
39
- # Here we just use a simple regex grep for patterns.
40
- for FILE in $MODIFIED_FILES; do
41
- echo "Checking: $FILE"
42
- for PATTERN in $PROTECTED_PATHS; do
43
- # Convert simple glob to regex for bash
44
- REGEX=$(echo "$PATTERN" | sed -e 's/\./\\./g' -e 's/\*\*/.*/g' -e 's/\*/[^/]*/g')
45
- if [[ "$FILE" =~ ^$REGEX$ ]] || [[ "$FILE" =~ ^$REGEX/ ]]; then
46
- echo "::error file=$FILE::File violates protected paths constraint: Matches pattern '$PATTERN'"
47
- VIOLATION_FOUND=1
48
- fi
49
- done
50
- done
51
-
52
- if [ $VIOLATION_FOUND -eq 1 ]; then
53
- echo "::error::PR modifies protected files. Jules agent is not allowed to modify these files without explicit manual review bypass."
54
- exit 1
55
- fi
56
-
57
- echo "Scope check passed. No protected files were modified."