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.
- package/.agent/history/2026-07-27-dispatch-test-swarm-task.md +9 -0
- package/.agent/jules-queue/README.md +8 -1
- package/.env.example +17 -0
- package/.github/workflows/jules-audit.yml +32 -0
- package/.github/workflows/jules-nightly.yml +21 -0
- package/.github/workflows/publish.yml +27 -0
- package/JULES_RULES_TEMPLATE.md +12 -8
- package/README.md +206 -78
- package/bin/init.js +93 -7
- package/package.json +3 -1
- package/scripts/command-resolver.mjs +88 -131
- package/scripts/jules-dispatch.mjs +165 -94
- package/scripts/jules-queue-runner.mjs +20 -2
- package/scripts/jules-self-audit.mjs +72 -5
- package/scripts/jules-swarm.mjs +85 -7
|
@@ -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
|
|
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
|
-
|
|
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
|
|
43
|
-
function
|
|
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
|
-
//
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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", [
|
|
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
|
-
|
|
221
|
+
if (!runVerification(resolvedCmds.testCmd)) {
|
|
222
|
+
testPassed = false;
|
|
223
|
+
console.error(`ā Test verification failed: ${resolvedCmds.testCmd}`);
|
|
224
|
+
}
|
|
177
225
|
}
|
|
178
|
-
|
|
226
|
+
|
|
227
|
+
if (testPassed && resolvedCmds.buildCmd) {
|
|
179
228
|
console.log(`ā¶ Running Build Verification: ${resolvedCmds.buildCmd}`);
|
|
180
|
-
|
|
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
|
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -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
|
|
29
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|