jules-orchestrator-kit 0.1.9 → 0.2.9

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.
@@ -3,12 +3,15 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import crypto from "node:crypto";
5
5
  import os from "node:os";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { resolveProjectCommands } from "./command-resolver.mjs";
7
8
 
8
- const taskTitle = process.argv[2];
9
- const taskPromptArg = process.argv[3];
9
+ const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
10
10
 
11
- if (!taskTitle || !taskPromptArg) {
11
+ const taskTitle = isMainModule ? process.argv[2] : "";
12
+ const taskPromptArg = isMainModule ? process.argv[3] : "";
13
+
14
+ if (isMainModule && (!taskTitle || !taskPromptArg)) {
12
15
  console.error(
13
16
  'Usage: node scripts/jules-dispatch.mjs <task-title> <path-to-prompt.md | "raw prompt string">'
14
17
  );
@@ -39,9 +42,41 @@ function loadEnv() {
39
42
  }
40
43
  loadEnv();
41
44
 
42
- // 0.1 Pre-Flight Secret Redaction Gate
43
- function redactSecrets(text) {
45
+ // 0.1 Shannon Entropy Calculator
46
+ export function calculateEntropy(str) {
47
+ if (!str) return 0;
48
+ const len = str.length;
49
+ const frequencies = {};
50
+ for (let i = 0; i < len; i++) {
51
+ const char = str[i];
52
+ frequencies[char] = (frequencies[char] || 0) + 1;
53
+ }
54
+ let entropy = 0;
55
+ for (const char in frequencies) {
56
+ const p = frequencies[char] / len;
57
+ entropy -= p * Math.log2(p);
58
+ }
59
+ return entropy;
60
+ }
61
+
62
+ // 0.2 Pre-Flight Secret Redaction Gate (RegEx + Shannon Entropy + Env Denylist)
63
+ export function redactSecrets(text) {
44
64
  if (!text) return "";
65
+ let sanitized = text;
66
+
67
+ // Redact active env secrets from denylist keys (*_KEY, *_SECRET, *_TOKEN, etc.)
68
+ for (const [envKey, envVal] of Object.entries(process.env)) {
69
+ if (
70
+ envVal &&
71
+ envVal.length >= 6 &&
72
+ /KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH/i.test(envKey)
73
+ ) {
74
+ if (sanitized.includes(envVal)) {
75
+ sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
76
+ }
77
+ }
78
+ }
79
+
45
80
  const patterns = [
46
81
  /gh[pusr]_[a-zA-Z0-9]{36}/g,
47
82
  /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
@@ -52,110 +87,143 @@ function redactSecrets(text) {
52
87
  /sk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}/g,
53
88
  /xox[baprs]-[a-zA-Z0-9\-]{10,}/g,
54
89
  /eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g,
55
- /-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY-----/,
90
+ /-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY-----/g,
56
91
  ];
57
- let sanitized = text;
58
92
  for (const pat of patterns) {
59
93
  sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
60
94
  }
95
+ // Exclude file paths (tokens with slashes or common extensions) from entropy redaction
96
+ sanitized = sanitized.replace(/[a-zA-Z0-9_\-\.]{20,}/g, (token) => {
97
+ if (
98
+ token.startsWith("[REDACTED_") ||
99
+ token.includes("/") ||
100
+ /\.(?:ts|js|jsx|tsx|json|md|py|rs|go|yml|yaml|toml|html|css|sh|config)$/i.test(token)
101
+ ) {
102
+ return token;
103
+ }
104
+ const entropy = calculateEntropy(token);
105
+ if (entropy > 3.6) {
106
+ return "[REDACTED_ENTROPY_KEY]";
107
+ }
108
+ return token;
109
+ });
61
110
  return sanitized;
62
111
  }
63
112
 
64
- // 1. Resolve prompt content (file path or inline string)
65
- let rawPrompt = "";
66
- const possiblePath = path.resolve(process.cwd(), taskPromptArg);
67
- if (fs.existsSync(possiblePath) && fs.statSync(possiblePath).isFile()) {
68
- rawPrompt = fs.readFileSync(possiblePath, "utf-8");
69
- } else {
70
- rawPrompt = taskPromptArg;
113
+ // 0.3 Hardened Path Traversal & Symlink Defense
114
+ export function assertPathWithinWorkspace(targetPath, workspaceRoot = process.cwd()) {
115
+ const rootReal = fs.existsSync(workspaceRoot) ? fs.realpathSync(path.resolve(workspaceRoot)) : path.resolve(workspaceRoot);
116
+ const resolvedTarget = path.resolve(rootReal, targetPath);
117
+ const targetReal = fs.existsSync(resolvedTarget) ? fs.realpathSync(resolvedTarget) : resolvedTarget;
118
+ const normalizedRoot = rootReal.replace(/\\/g, "/");
119
+ const normalizedTarget = targetReal.replace(/\\/g, "/");
120
+ if (!normalizedTarget.startsWith(normalizedRoot)) {
121
+ throw new Error(`FATAL: Sandboxed directory traversal breach blocked for path: ${targetPath}`);
122
+ }
123
+ return targetReal;
71
124
  }
72
- rawPrompt = redactSecrets(rawPrompt);
73
-
74
- // 2. Dynamic Guardrail Composition (DGC) from .agent/rules/dynamic-guardrails.json
75
- function getDynamicGuardrails(promptText) {
76
- const guardrailsPath = path.resolve(process.cwd(), ".agent/rules/dynamic-guardrails.json");
77
- if (!fs.existsSync(guardrailsPath)) return "";
78
125
 
79
- try {
80
- const data = JSON.parse(fs.readFileSync(guardrailsPath, "utf-8"));
81
- if (!data.rules || !Array.isArray(data.rules)) return "";
82
-
83
- const active = [];
84
- for (const item of data.rules) {
85
- if (new RegExp(item.trigger, "i").test(promptText)) {
86
- active.push(item.guardrail);
87
- }
88
- }
89
- return active.join("\n\n");
90
- } catch (e) {
91
- return "";
126
+ // 0.4 Dynamic Guardrails & Directive Definitions
127
+ export function getDynamicGuardrails(prompt = "") {
128
+ const guardrails = [];
129
+ if (/\b(?:astro|components|pages|src\/.*\.astro)\b/i.test(prompt)) {
130
+ guardrails.push("- Astro Guidance: Ensure zero client JS shipped by default. Use server islands or nano stores if state is required.");
131
+ }
132
+ if (/\b(?:db|database|d1|postgres|drizzle|migration|schema)\b/i.test(prompt)) {
133
+ guardrails.push("- Database Guidance: Do not modify migrations directly without inspecting current schema constraints.");
92
134
  }
135
+ return guardrails.length > 0 ? `## Context-Specific Guardrails\n${guardrails.join("\n")}` : "";
93
136
  }
94
137
 
95
- // 3. Dynamic Command Resolution (Language & Framework Detection)
96
- const resolvedCmds = resolveProjectCommands(process.cwd());
97
- const verifyDirective = resolvedCmds.testCmd || resolvedCmds.buildCmd
98
- ? `\n\n## šŸ› ļø MANDATORY VERIFICATION COMMANDS\nBefore submitting PR, you MUST execute: \`${[resolvedCmds.testCmd, resolvedCmds.buildCmd].filter(Boolean).join(" && ")}\``
99
- : "";
100
-
101
- // 4. Load Base Rules / Guardrails
102
- const rulesPath = fs.existsSync(path.resolve(process.cwd(), "AGENTS.md"))
103
- ? path.resolve(process.cwd(), "AGENTS.md")
104
- : path.resolve(process.cwd(), "JULES_RULES_TEMPLATE.md");
105
-
106
- const baseRules = fs.existsSync(rulesPath)
107
- ? fs.readFileSync(rulesPath, "utf-8")
108
- : "";
109
-
110
- const dynamicRules = getDynamicGuardrails(rawPrompt);
111
-
112
- // Machine XML Directives
113
- const envelope = `
114
- <MCP_DIRECTIVE>
115
- <system_state>HEADLESS_CI_MODE</system_state>
116
- <strict_invariants>
117
- <rule>1. READ-BEFORE-WRITE (ZERO HALLUCINATION): You are FORBIDDEN from guessing internal API signatures. Before editing, use search tools or MCP doc tools to verify exact signatures.</rule>
118
- <rule>2. VERIFICATION LOOP: After patching code, you MUST execute the verification commands and ensure 0 errors.</rule>
119
- <rule>3. ABORT CONDITION: On repeated unresolvable test failures, output <status>ABORT_UNRESOLVABLE</status> and terminate.</rule>
120
- </strict_invariants>
121
- </MCP_DIRECTIVE>
122
- `.trim();
123
-
124
- const fullPrompt = redactSecrets(`MCP DIRECTIVE: ${rawPrompt.trim()}${verifyDirective}\n\n---\n\n${envelope}\n\n---\n\n${dynamicRules ? `${dynamicRules}\n\n---\n\n` : ""}${baseRules.trim()}`);
125
-
126
- // 5. Log Dispatch History
127
- const dateStr = new Date().toISOString().split("T")[0];
128
- const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
129
-
130
- const historyDir = path.resolve(process.cwd(), ".agent/history");
131
- if (!fs.existsSync(historyDir)) {
132
- fs.mkdirSync(historyDir, { recursive: true });
138
+ function getBaseRules(projectRoot = process.cwd()) {
139
+ const templatePath = path.join(projectRoot, "JULES_RULES_TEMPLATE.md");
140
+ if (fs.existsSync(templatePath)) {
141
+ return fs.readFileSync(templatePath, "utf-8");
142
+ }
143
+ const agentsPath = path.join(projectRoot, "AGENTS.md");
144
+ if (fs.existsSync(agentsPath)) {
145
+ return fs.readFileSync(agentsPath, "utf-8");
146
+ }
147
+ return `## Operational Directives
148
+ - **Read Before Write**: Always inspect target files and symbol definitions before modifying code.
149
+ - **Verification**: Execute test and build verification suite and ensure 0 errors.
150
+ - **Anti-Patch Invariant**: Do NOT create out-of-band shell scripts (patch.sh, test-fix.sh) or disable assertions to force tests to pass.`;
133
151
  }
134
- const historyFile = path.join(historyDir, `${dateStr}-dispatch-${slug}.md`);
135
- fs.writeFileSync(historyFile, `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`, "utf-8");
136
152
 
137
- console.log(`šŸš€ Dispatching task to Google Jules: "${taskTitle}"...`);
138
- console.log(`šŸ“ Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
153
+ // 1. Resolve prompt content and construct directive envelope
154
+ let rawPrompt = "";
155
+ let fullPrompt = "";
156
+ let historyFile = "";
157
+ let tmpPayloadFile = "";
158
+ let tmpDir = "";
159
+
160
+ const apiKey = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
161
+ const repo = process.env.JULES_REPO || "";
162
+
163
+ if (isMainModule) {
164
+ const possiblePath = path.resolve(process.cwd(), taskPromptArg);
165
+ if (fs.existsSync(possiblePath) && fs.statSync(possiblePath).isFile()) {
166
+ assertPathWithinWorkspace(possiblePath);
167
+ rawPrompt = fs.readFileSync(possiblePath, "utf-8");
168
+ } else {
169
+ rawPrompt = taskPromptArg;
170
+ }
171
+ rawPrompt = redactSecrets(rawPrompt);
172
+
173
+ const dynamicRules = getDynamicGuardrails(rawPrompt);
174
+ const baseRules = getBaseRules(process.cwd());
175
+ const envelope = `## Envelope Directives
176
+ - **Zero Hallucination:** Inspect exact symbol definitions before editing.
177
+ - **Verification:** Execute project verification suite and ensure 0 errors.
178
+ - **Anti-Patch / Zero Out-of-band Scripts:** Do NOT create workaround runner scripts (e.g. patch.sh, test-fix.sh) or disable assertions to pass tests.`;
179
+
180
+ const { testCmd, buildCmd } = resolveProjectCommands(process.cwd());
181
+ const verifyDirective = testCmd || buildCmd
182
+ ? `\n\nVERIFICATION DIRECTIVE: Execute \`${[testCmd, buildCmd].filter(Boolean).join(" && ")}\` after patching.`
183
+ : "";
184
+
185
+ fullPrompt = redactSecrets(`MCP DIRECTIVE: ${rawPrompt.trim()}${verifyDirective}\n\n---\n\n${envelope}\n\n---\n\n${dynamicRules ? `${dynamicRules}\n\n---\n\n` : ""}${baseRules.trim()}`);
186
+
187
+ const dateStr = new Date().toISOString().split("T")[0];
188
+ const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
189
+
190
+ // Resolve history directory relative to main workspace root, avoiding temporary worktrees
191
+ const mainWorkspaceRoot = process.env.JULES_PROJECT_ROOT || process.env.INIT_CWD || process.cwd();
192
+ const historyDir = path.resolve(mainWorkspaceRoot, ".agent/history");
193
+ if (!fs.existsSync(historyDir)) {
194
+ fs.mkdirSync(historyDir, { recursive: true });
195
+ }
196
+ historyFile = path.join(historyDir, `${dateStr}-dispatch-${slug}.md`);
197
+ fs.writeFileSync(historyFile, `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`, "utf-8");
139
198
 
140
- // 6. Dispatch via REST API or Ephemeral Payload File (Avoid ARG_MAX shell limits)
141
- const apiKey = process.env.JULES_API_KEY;
142
- const repo = process.env.JULES_REPO;
199
+ console.log(`šŸš€ Dispatching task to Google Jules: "${taskTitle}"...`);
200
+ console.log(`šŸ“ Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
143
201
 
144
- const payloadHash = crypto.createHash("sha256").update(taskTitle + Date.now()).digest("hex").slice(0, 12);
145
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
146
- const tmpPayloadFile = path.join(tmpDir, `payload_${payloadHash}.json`);
147
- fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
202
+ const payloadHash = crypto.createHash("sha256").update(taskTitle + Date.now()).digest("hex").slice(0, 12);
203
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
204
+ tmpPayloadFile = path.join(tmpDir, `payload_${payloadHash}.json`);
205
+ fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
206
+ }
148
207
 
149
208
  function cleanupTmp() {
150
209
  try {
151
210
  if (fs.existsSync(tmpDir)) {
152
211
  fs.rmSync(tmpDir, { recursive: true, force: true });
153
212
  }
154
- } catch (_) {}
213
+ } catch (err) {
214
+ console.warn(`āš ļø Failed to remove temporary payload directory at ${tmpDir}:`, err.message);
215
+ }
155
216
  }
156
217
 
157
218
  async function executeDispatch() {
158
219
  try {
220
+ if (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1") {
221
+ console.log(`[DRY RUN] Dispatch payload prepared successfully for task: "${taskTitle}".`);
222
+ console.log(`[DRY RUN] Target Repository: ${repo || "(default)"}`);
223
+ console.log(`[DRY RUN] Prompt Length: ${fullPrompt.length} chars.`);
224
+ return;
225
+ }
226
+
159
227
  if (apiKey && repo) {
160
228
  console.log(`🌐 Using Jules REST API for repository ${repo}...`);
161
229
  const formattedSource = repo.startsWith("sources/") ? repo : (repo.includes("/") ? `sources/github/${repo}` : repo);
@@ -172,7 +240,8 @@ async function executeDispatch() {
172
240
 
173
241
  let res;
174
242
  try {
175
- res = await fetch("https://jules.googleapis.com/v1alpha/sessions", {
243
+ const apiUrl = process.env.JULES_API_URL || "https://jules.googleapis.com/v1alpha/sessions";
244
+ res = await fetch(apiUrl, {
176
245
  method: "POST",
177
246
  headers: {
178
247
  "X-Goog-Api-Key": apiKey,
@@ -202,7 +271,9 @@ async function executeDispatch() {
202
271
  try {
203
272
  const data = await res.json();
204
273
  sessionId = data.name || data.id || sessionId;
205
- } catch (_) {}
274
+ } catch (jsonErr) {
275
+ console.warn("āš ļø Failed to parse REST API JSON response:", jsonErr.message);
276
+ }
206
277
 
207
278
  console.log(`āœ… REST API Dispatch response: Session ${sessionId} created successfully.`);
208
279
  fs.appendFileSync(historyFile, `\n## Session\n- ID: \`${sessionId}\`\n`);
@@ -214,7 +285,6 @@ async function executeDispatch() {
214
285
  }
215
286
  }
216
287
 
217
-
218
288
  function dispatchViaCli(targetRepo, payloadFile, title) {
219
289
  try {
220
290
  let promptToSend = fullPrompt;
@@ -222,7 +292,9 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
222
292
  try {
223
293
  const payloadData = JSON.parse(fs.readFileSync(payloadFile, "utf-8"));
224
294
  if (payloadData.prompt) promptToSend = payloadData.prompt;
225
- } catch (_) {}
295
+ } catch (jsonErr) {
296
+ console.warn("āš ļø Failed to parse payload JSON file:", jsonErr.message);
297
+ }
226
298
  }
227
299
 
228
300
  const args = ["new"];
@@ -239,10 +311,9 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
239
311
  }
240
312
  }
241
313
 
242
-
243
- executeDispatch().catch((err) => {
244
- console.error("āŒ Fatal unhandled rejection in dispatch:", err);
245
- process.exit(1);
246
- });
247
-
248
-
314
+ if (isMainModule) {
315
+ executeDispatch().catch((err) => {
316
+ console.error("āŒ Fatal unhandled rejection in dispatch:", err);
317
+ process.exit(1);
318
+ });
319
+ }
@@ -1,9 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { execFileSync } from "node:child_process";
4
5
 
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const dispatchScript = path.resolve(__dirname, "jules-dispatch.mjs");
9
+
5
10
  const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
6
11
  const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
12
+ const queueLogFile = path.join(queueDir, "queue.jsonl");
7
13
 
8
14
  if (!fs.existsSync(queueDir)) {
9
15
  console.error(`āŒ Queue directory not found: ${queueDir}`);
@@ -14,11 +20,20 @@ if (!fs.existsSync(completedDir)) {
14
20
  fs.mkdirSync(completedDir, { recursive: true });
15
21
  }
16
22
 
23
+ function logQueueState(file, status, error = null) {
24
+ const entry = JSON.stringify({
25
+ timestamp: new Date().toISOString(),
26
+ file,
27
+ status,
28
+ error: error ? error.message || String(error) : null,
29
+ }) + "\n";
30
+ fs.appendFileSync(queueLogFile, entry, "utf-8");
31
+ }
32
+
17
33
  const files = fs.readdirSync(queueDir).filter(
18
34
  (f) => f.endsWith(".md") && f !== "README.md" && !f.startsWith("_") && !f.startsWith(".")
19
35
  );
20
36
 
21
-
22
37
  if (files.length === 0) {
23
38
  console.log("ā„¹ļø No tasks found in the queue.");
24
39
  process.exit(0);
@@ -33,17 +48,20 @@ files.forEach((file, index) => {
33
48
 
34
49
  console.log(`\n----------------------------------------`);
35
50
  console.log(`[${index + 1}/${files.length}] Dispatching queued task: ${title}`);
51
+ logQueueState(file, "RUNNING");
36
52
 
37
53
  try {
38
- execFileSync("node", ["scripts/jules-dispatch.mjs", title, filePath], {
54
+ execFileSync("node", [dispatchScript, title, filePath], {
39
55
  stdio: "inherit",
40
56
  });
41
57
 
42
58
  // Move to completed
43
59
  const destPath = path.join(completedDir, file);
44
60
  fs.renameSync(filePath, destPath);
61
+ logQueueState(file, "COMPLETED");
45
62
  console.log(`āœ… Moved ${file} to completed/`);
46
63
  } catch (error) {
64
+ logQueueState(file, "FAILED", error);
47
65
  console.error(`āš ļø Failed to dispatch task [${title}]:`, error.message);
48
66
  }
49
67
  });
@@ -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
+ });