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.
- 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 +24 -0
- package/JULES_RULES_TEMPLATE.md +7 -3
- package/README.md +200 -73
- package/bin/init.js +93 -7
- package/package.json +3 -1
- package/scripts/command-resolver.mjs +88 -131
- package/scripts/jules-dispatch.mjs +162 -93
- package/scripts/jules-queue-runner.mjs +20 -2
- package/scripts/jules-self-audit.mjs +72 -5
- package/scripts/jules-swarm.mjs +85 -7
|
@@ -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
|
+
});
|