jules-orchestrator-kit 0.1.9 → 0.2.8

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.
@@ -43,6 +43,14 @@ export function matchGlob(filepath, globPattern) {
43
43
  }
44
44
 
45
45
 
46
+ /**
47
+ * Lightweight Zero-Dependency YAML Pattern Extractor
48
+ * Extracts forbidden_paths and allow_paths arrays from jules.yml.
49
+ * Supported subset:
50
+ * - Flow style arrays: forbidden_paths: ["path1", "path2"]
51
+ * - Block style arrays: forbidden_paths:\n - path1\n - path2
52
+ * Limitations: Does not support multiline strings (|/>), nested dicts, or inline comments.
53
+ */
46
54
  export function loadForbiddenPatterns(configContent = "") {
47
55
  const defaultForbidden = [
48
56
  ".github/**",
@@ -112,10 +120,35 @@ export function loadAllowedPatterns(configContent = "") {
112
120
  return blockParsed;
113
121
  }
114
122
 
123
+ export function parseAndCleanStderr(stderrStr) {
124
+ if (!stderrStr) return "";
125
+ const clean = stderrStr.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
126
+ const lines = clean.split("\n").filter(Boolean);
127
+ return lines.slice(-100).join("\n");
128
+ }
129
+
130
+ export function logAuditMetrics(metrics) {
131
+ const historyDir = path.resolve(process.cwd(), ".agent/history");
132
+ if (!fs.existsSync(historyDir)) {
133
+ fs.mkdirSync(historyDir, { recursive: true });
134
+ }
135
+ const metricsFile = path.join(historyDir, "metrics.jsonl");
136
+ const logLine = JSON.stringify({
137
+ timestamp: new Date().toISOString(),
138
+ ...metrics,
139
+ }) + "\n";
140
+ fs.appendFileSync(metricsFile, logLine, "utf-8");
141
+ }
142
+
115
143
  export function runSelfAudit() {
116
144
  console.log("šŸ” Running Jules PR Self-Audit Gatekeeper...\n");
117
145
 
118
146
  const targetBranch = process.env.BASE_BRANCH || "main";
147
+ const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
148
+ if (!SAFE_BRANCH.test(targetBranch)) {
149
+ console.error(`āŒ FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
150
+ process.exit(1);
151
+ }
119
152
  console.log(`šŸŽÆ Target Branch: ${targetBranch}`);
120
153
 
121
154
  if (process.env.CI) {
@@ -150,7 +183,6 @@ export function runSelfAudit() {
150
183
  const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
151
184
  const allowedPatterns = loadAllowedPatterns(trustedConfig);
152
185
 
153
-
154
186
  const violations = rawDiffFiles.filter((file) => {
155
187
  const isForbidden = forbiddenPatterns.some((pattern) => matchGlob(file, pattern));
156
188
  if (!isForbidden) return false;
@@ -166,18 +198,38 @@ export function runSelfAudit() {
166
198
  }
167
199
  console.log("\nāœ… Restricted File Boundary Check: PASSED");
168
200
 
169
-
170
201
  console.log("\nšŸ› ļø Resolving Dynamic Verification Suite...");
171
202
  const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
172
203
  console.log(`šŸ“‹ Discovered Execution Scope: ${resolvedCmds.source}`);
173
204
 
205
+ const startTime = Date.now();
206
+ let testPassed = true;
207
+ let failureLog = "";
208
+
209
+ const runVerification = (cmd) => {
210
+ try {
211
+ execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
212
+ return true;
213
+ } catch (err) {
214
+ failureLog = parseAndCleanStderr((err.stderr || "") + "\n" + (err.stdout || "") + "\n" + (err.message || ""));
215
+ return false;
216
+ }
217
+ };
218
+
174
219
  if (resolvedCmds.testCmd) {
175
220
  console.log(`ā–¶ Running Test Verification: ${resolvedCmds.testCmd}`);
176
- runCommand(resolvedCmds.testCmd);
221
+ if (!runVerification(resolvedCmds.testCmd)) {
222
+ testPassed = false;
223
+ console.error(`āŒ Test verification failed: ${resolvedCmds.testCmd}`);
224
+ }
177
225
  }
178
- if (resolvedCmds.buildCmd) {
226
+
227
+ if (testPassed && resolvedCmds.buildCmd) {
179
228
  console.log(`ā–¶ Running Build Verification: ${resolvedCmds.buildCmd}`);
180
- runCommand(resolvedCmds.buildCmd);
229
+ if (!runVerification(resolvedCmds.buildCmd)) {
230
+ testPassed = false;
231
+ console.error(`āŒ Build verification failed: ${resolvedCmds.buildCmd}`);
232
+ }
181
233
  }
182
234
 
183
235
  if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
@@ -185,6 +237,21 @@ export function runSelfAudit() {
185
237
  runCommand("git status");
186
238
  }
187
239
 
240
+ const durationMs = Date.now() - startTime;
241
+ logAuditMetrics({
242
+ targetBranch,
243
+ changedFilesCount: changedCodeFiles.length,
244
+ testPassed,
245
+ durationMs,
246
+ scope: resolvedCmds.source,
247
+ });
248
+
249
+ if (!testPassed) {
250
+ console.error("\nšŸ”„ OODA SELF-HEALING FEEDBACK LOG:");
251
+ console.error(failureLog.slice(-1500));
252
+ process.exit(1);
253
+ }
254
+
188
255
  console.log("\nšŸŽ‰ JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
189
256
  }
190
257
 
@@ -9,7 +9,7 @@ const tasksFile = process.argv[2];
9
9
 
10
10
  if (!tasksFile) {
11
11
  console.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
12
- console.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1" } ]');
12
+ console.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
13
13
  process.exit(1);
14
14
  }
15
15
 
@@ -25,15 +25,61 @@ if (!Array.isArray(tasks)) {
25
25
  process.exit(1);
26
26
  }
27
27
 
28
- const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
29
- const STAGGER_MS = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
28
+ const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
29
+ const MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
30
30
 
31
- console.log(`šŸ Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT})...`);
31
+ const parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
32
+ const STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
33
+
34
+ const USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
35
+
36
+ // Ensure root project path is preserved for history logs even when running in worktrees
37
+ process.env.JULES_PROJECT_ROOT = process.cwd();
38
+
39
+ console.log(`šŸ Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
32
40
 
33
41
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
34
42
 
43
+ async function createWorktree(taskId) {
44
+ const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
45
+ const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
46
+ const branchName = `jules/${slug}`;
47
+ try {
48
+ if (!fs.existsSync(path.dirname(wtDir))) {
49
+ fs.mkdirSync(path.dirname(wtDir), { recursive: true });
50
+ }
51
+ await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
52
+ return { wtDir, branchName };
53
+ } catch (err) {
54
+ try {
55
+ await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
56
+ return { wtDir, branchName: null };
57
+ } catch (fallbackErr) {
58
+ console.warn(`āš ļø Failed to create worktree for ${taskId}:`, fallbackErr.message);
59
+ return null;
60
+ }
61
+ }
62
+ }
63
+
64
+ async function removeWorktree(wtDir, branchName) {
65
+ if (!wtDir) return;
66
+ try {
67
+ await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
68
+ } catch (err) {
69
+ console.warn(`āš ļø Failed to remove worktree at ${wtDir}:`, err.message);
70
+ }
71
+ if (branchName) {
72
+ try {
73
+ await execFileAsync("git", ["branch", "-D", branchName]);
74
+ } catch (err) {
75
+ console.warn(`āš ļø Failed to delete swarm branch ${branchName}:`, err.message);
76
+ }
77
+ }
78
+ }
79
+
35
80
  async function runSwarm() {
36
81
  const results = [];
82
+ let hasFailure = false;
37
83
 
38
84
  for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
39
85
  const batch = tasks.slice(i, i + MAX_CONCURRENT);
@@ -49,15 +95,39 @@ async function runSwarm() {
49
95
  console.log(`\n----------------------------------------`);
50
96
  console.log(`[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
51
97
 
98
+ let wtInfo = null;
99
+ let execCwd = process.cwd();
100
+
101
+ if (USE_WORKTREES) {
102
+ wtInfo = await createWorktree(taskId);
103
+ if (wtInfo?.wtDir) {
104
+ execCwd = wtInfo.wtDir;
105
+ console.log(`🌲 Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
106
+ }
107
+ }
108
+
109
+ let effectivePrompt = task.prompt || "";
110
+ if (task.scope) {
111
+ const scopeStr = typeof task.scope === "string" ? task.scope : JSON.stringify(task.scope);
112
+ effectivePrompt += `\n\n[SCOPE LOCK]\nStrictly limit changes to designated bounds: ${scopeStr}`;
113
+ }
114
+
52
115
  try {
53
- const { stdout } = await execFileAsync("node", ["scripts/jules-dispatch.mjs", task.title, task.prompt], {
116
+ const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
117
+ const { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
118
+ cwd: execCwd,
54
119
  timeout: 15 * 60 * 1000,
120
+ env: { ...process.env, JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT },
55
121
  });
56
122
  if (stdout) console.log(stdout.trim());
57
123
  return { taskId, title: task.title, status: "SUCCESS" };
58
124
  } catch (error) {
59
125
  console.error(`āš ļø Failed task [${task.title}]:`, error.message);
60
126
  return { taskId, title: task.title, status: "FAILED", error: error.message };
127
+ } finally {
128
+ if (wtInfo?.wtDir) {
129
+ await removeWorktree(wtInfo.wtDir, wtInfo.branchName);
130
+ }
61
131
  }
62
132
  })
63
133
  );
@@ -65,8 +135,10 @@ async function runSwarm() {
65
135
  for (const r of batchResults) {
66
136
  if (r.status === "fulfilled") {
67
137
  results.push(r.value);
138
+ if (r.value.status === "FAILED") hasFailure = true;
68
139
  } else {
69
140
  results.push({ title: "Unknown Task", status: "FAILED", error: r.reason });
141
+ hasFailure = true;
70
142
  }
71
143
  }
72
144
 
@@ -81,7 +153,13 @@ async function runSwarm() {
81
153
  results.forEach((res) => {
82
154
  console.log(` - [${res.status}] ${res.title}`);
83
155
  });
84
- }
85
156
 
86
- runSwarm();
157
+ if (hasFailure) {
158
+ process.exitCode = 1;
159
+ }
160
+ }
87
161
 
162
+ runSwarm().catch((err) => {
163
+ console.error("āŒ Unhandled swarm rejection:", err);
164
+ process.exit(1);
165
+ });