jules-orchestrator-kit 0.8.5 → 0.9.4

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.
Files changed (43) hide show
  1. package/.agent/jules-queue/README.md +4 -4
  2. package/.agent/prompts/Bolt.md +18 -0
  3. package/.agent/prompts/Overseer.md +18 -0
  4. package/.agent/prompts/Sentinel.md +18 -0
  5. package/.agent/prompts/Task_Template.md +26 -0
  6. package/JULES_RULES_TEMPLATE.md +6 -1
  7. package/README.md +54 -31
  8. package/bin/agentctl.mjs +281 -0
  9. package/bin/mcp-server.mjs +5 -0
  10. package/index.mjs +32 -5
  11. package/package.json +13 -2
  12. package/scripts/asset-integrity-check.mjs +35 -0
  13. package/scripts/command-resolver.mjs +37 -214
  14. package/scripts/jules-cleanup.mjs +23 -164
  15. package/scripts/jules-create.mjs +12 -56
  16. package/scripts/jules-dispatch.mjs +103 -449
  17. package/scripts/jules-merge-swarm.mjs +19 -173
  18. package/scripts/jules-nightly.mjs +7 -128
  19. package/scripts/jules-patch.mjs +15 -103
  20. package/scripts/jules-queue-runner.mjs +28 -221
  21. package/scripts/jules-scan-todos.mjs +31 -125
  22. package/scripts/jules-self-audit.mjs +69 -656
  23. package/scripts/jules-status.mjs +42 -70
  24. package/scripts/jules-swarm.mjs +23 -327
  25. package/scripts/jules-webhook-receiver.mjs +70 -0
  26. package/scripts/lock-manager.mjs +11 -177
  27. package/scripts/risk-tier.mjs +29 -0
  28. package/scripts/rules-lint.mjs +20 -0
  29. package/scripts/stale-base-check.mjs +26 -0
  30. package/scripts/utils.mjs +107 -369
  31. package/scripts/validate-envelope.mjs +44 -0
  32. package/src/asset_integrity.mjs +76 -0
  33. package/src/config.mjs +325 -0
  34. package/src/engine.mjs +217 -0
  35. package/src/envelope.mjs +105 -0
  36. package/src/git.mjs +127 -0
  37. package/src/mcp.mjs +211 -0
  38. package/src/provider.mjs +179 -0
  39. package/src/risk.mjs +120 -0
  40. package/src/rules_budget.mjs +77 -0
  41. package/src/security.mjs +212 -0
  42. package/src/state.mjs +217 -0
  43. package/src/webhook.mjs +138 -0
@@ -1,79 +1,51 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { log, checkDailyBudget } from "./utils.mjs";
1
+ #!/usr/bin/env node
5
2
 
6
- const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
3
+ /**
4
+ * Backward compatibility shim for jules-status.mjs in v0.9.0.
5
+ */
7
6
 
8
- if (isMainModule) {
9
- const isJson = process.argv.includes("--json");
10
- const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
7
+ import { checkDailyBudget } from "../src/state.mjs";
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { join } from "node:path";
11
10
 
12
- const budgetInfo = checkDailyBudget();
13
-
14
- if (!fs.existsSync(queueLogPath)) {
15
- if (isJson) {
16
- console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
17
- } else {
18
- log.info("Queue log is empty. No tasks have been dispatched yet.");
11
+ export function categorizeTaskStatus(statusStr = "") {
12
+ const s = String(statusStr).toUpperCase();
13
+ if (s === "AWAITING_PLAN_APPROVAL" || s === "AWAITING_USER_FEEDBACK") {
14
+ return "action_required";
15
+ }
16
+ if (s === "IN_PROGRESS" || s === "DISPATCHED") {
17
+ return "in_progress";
18
+ }
19
+ if (s === "COMPLETED" || s === "FAILED") {
20
+ return "completed";
19
21
  }
20
- process.exit(0);
22
+ return "unknown";
21
23
  }
22
24
 
23
- try {
24
- const content = fs.readFileSync(queueLogPath, "utf-8");
25
- const lines = content.split("\n").filter(Boolean);
26
-
27
- if (lines.length === 0) {
28
- if (isJson) {
29
- console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
30
- } else {
31
- log.info("Queue log is empty.");
32
- }
33
- process.exit(0);
34
- }
35
-
36
- // We only want the latest status for each task file
37
- const statusMap = new Map();
38
-
39
- for (const line of lines) {
40
- try {
41
- const entry = JSON.parse(line);
42
- if (entry.file) {
43
- statusMap.set(entry.file, {
44
- file: entry.file,
45
- Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
46
- Status: entry.status,
47
- Timestamp: entry.timestamp,
48
- Error: entry.error || null
49
- });
50
- }
51
- } catch (e) {
52
- // Ignore malformed lines
53
- }
54
- }
25
+ const isJson = process.argv.includes("--json");
26
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
27
+ const budget = checkDailyBudget(root);
55
28
 
56
- const tasksList = Array.from(statusMap.values());
57
-
58
- if (isJson) {
59
- console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
60
- process.exit(0);
61
- }
62
-
63
- if (tasksList.length === 0) {
64
- log.info("No parseable task events found in queue.jsonl");
65
- process.exit(0);
66
- }
67
-
68
- log.header(`JULES QUEUE STATUS (Budget: ${budgetInfo.used}/${budgetInfo.budget} sessions used)`);
69
- console.table(tasksList.map(t => ({ Task: t.Task, Status: t.Status, Timestamp: new Date(t.Timestamp).toLocaleString(), Error: t.Error || "-" })));
29
+ let queueItems = [];
30
+ const queueFile = join(root, ".agent/jules-queue/queue.jsonl");
31
+ if (existsSync(queueFile)) {
32
+ try {
33
+ const raw = readFileSync(queueFile, "utf-8");
34
+ queueItems = raw
35
+ .split("\n")
36
+ .filter(Boolean)
37
+ .map((line) => {
38
+ const parsed = JSON.parse(line);
39
+ return {
40
+ ...parsed,
41
+ Status: parsed.Status || parsed.status || "PENDING",
42
+ };
43
+ });
44
+ } catch (_) {}
45
+ }
70
46
 
71
- } catch (err) {
72
- if (isJson) {
73
- console.log(JSON.stringify({ error: err.message }, null, 2));
74
- } else {
75
- log.error(`Failed to read queue log: ${err.message}`);
76
- }
77
- process.exit(1);
78
- }
47
+ if (isJson) {
48
+ console.log(JSON.stringify({ queue: queueItems, sessions: [], budget }));
49
+ } else {
50
+ console.log(`[Shim] Status: Daily Budget ${budget.used}/${budget.budget} used.`);
79
51
  }
@@ -1,342 +1,38 @@
1
- import { execFile, execFileSync } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { log, sleep } from "./utils.mjs";
1
+ #!/usr/bin/env node
7
2
 
8
- const execFileAsync = promisify(execFile);
3
+ /**
4
+ * Backward compatibility shim for jules-swarm.mjs in v0.9.0.
5
+ */
9
6
 
10
- const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
11
-
12
- let tasks = [];
13
- let MAX_CONCURRENT = 3;
14
- let STAGGER_MS = 1500;
15
- let USE_WORKTREES = false;
16
-
17
- const activeChildProcesses = new Set();
18
-
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
- }
45
-
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
- }
51
-
52
- const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
53
- MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
54
-
55
- const parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
56
- STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
57
-
58
- USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
59
-
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
- }
63
-
64
- const activeWorktrees = new Set();
65
-
66
- function cleanupAllWorktreesSync() {
67
- for (const child of Array.from(activeChildProcesses)) {
68
- try {
69
- child.kill("SIGTERM");
70
- } catch (_) {}
71
- }
72
- activeChildProcesses.clear();
73
-
74
- for (const item of Array.from(activeWorktrees)) {
75
- if (item.wtDir && fs.existsSync(item.wtDir)) {
76
- try {
77
- fs.chmodSync(item.wtDir, 0o755);
78
- } catch (_) {}
79
- }
80
- try {
81
- execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
82
- } catch (_) {}
83
- if (item.branchName) {
84
- try {
85
- execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
86
- } catch (_) {}
87
- }
88
- }
89
- activeWorktrees.clear();
90
- }
91
-
92
- function reapOrphanedWorktrees() {
93
- const worktreesBase = path.resolve(process.cwd(), ".agent/worktrees");
94
- if (!fs.existsSync(worktreesBase)) return;
95
- try {
96
- const entries = fs.readdirSync(worktreesBase);
97
- for (const entry of entries) {
98
- const wtPath = path.join(worktreesBase, entry);
99
- try {
100
- execFileSync("git", ["worktree", "remove", "--force", wtPath], { stdio: "ignore" });
101
- } catch (_) {
102
- try {
103
- fs.rmSync(wtPath, { recursive: true, force: true });
104
- } catch (_) {}
105
- }
106
- try {
107
- execFileSync("git", ["branch", "-D", `jules/${entry}`], { stdio: "ignore" });
108
- } catch (_) {}
109
- }
110
- } catch (_) {}
111
- try {
112
- execFileSync("git", ["worktree", "prune"], { stdio: "ignore" });
113
- } catch (_) {}
114
- }
115
-
116
-
117
-
118
- async function createWorktree(taskId) {
119
- const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
120
- const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
121
- const branchName = `jules/${slug}`;
122
- let item = null;
123
- await fs.promises.mkdir(path.dirname(wtDir), { recursive: true });
124
-
125
- const maxRetries = 3;
126
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
127
- try {
128
- await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
129
- item = { wtDir, branchName };
130
- break;
131
- } catch (err) {
132
- if (attempt < maxRetries) {
133
- const jitter = Math.floor(Math.random() * 200) + 100 * Math.pow(2, attempt);
134
- await sleep(jitter);
135
- continue;
136
- }
137
- try {
138
- await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
139
- item = { wtDir, branchName: null };
140
- } catch (fallbackErr) {
141
- log.error(`Failed to create worktree for ${taskId}: ${fallbackErr.message}`);
142
- return null;
143
- }
144
- }
145
- }
146
- if (item) activeWorktrees.add(item);
147
- return item;
148
- }
149
-
150
- async function removeWorktree(wtDir, branchName) {
151
- if (!wtDir) return;
152
- for (const item of Array.from(activeWorktrees)) {
153
- if (item.wtDir === wtDir) {
154
- activeWorktrees.delete(item);
155
- }
156
- }
157
- try {
158
- await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
159
- } catch (err) {
160
- log.error(`Failed to remove worktree at ${wtDir}: ${err.message}`);
161
- }
162
- if (branchName) {
163
- try {
164
- await execFileAsync("git", ["branch", "-D", branchName]);
165
- } catch (err) {
166
- log.error(`Failed to delete swarm branch ${branchName}: ${err.message}`);
167
- }
168
- }
169
- }
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
- }));
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
178
9
 
10
+ export function buildSyncManifest(tasks = []) {
11
+ const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
179
12
  return {
180
13
  version: 1,
181
- generatedAt: new Date().toISOString(),
182
- totalTasks: tasksList.length,
183
- reservations
14
+ totalTasks: tasks.length,
15
+ reservations,
184
16
  };
185
17
  }
186
18
 
187
19
  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";
20
+ const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
21
+ const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
197
22
 
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 };
23
+ const agentDir = join(projectRoot, ".agent");
24
+ if (!existsSync(agentDir)) {
25
+ try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
231
26
  }
232
- }
233
-
234
- async function runSwarm() {
235
- const manifest = buildSyncManifest(tasks);
236
- await pushReservationManifest(manifest);
27
+ const manifestPath = join(agentDir, "sync-manifest.json");
28
+ try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
237
29
 
238
- if (USE_WORKTREES) {
239
- reapOrphanedWorktrees();
240
- }
241
- const results = [];
242
- let hasFailure = false;
243
-
244
- for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
245
- const batch = tasks.slice(i, i + MAX_CONCURRENT);
246
- const batchResults = await Promise.allSettled(
247
- batch.map(async (task, bIdx) => {
248
- const taskNum = i + bIdx + 1;
249
- const taskId = task.id || `task-${taskNum}`;
250
-
251
- if (bIdx > 0) {
252
- await sleep(STAGGER_MS * bIdx);
253
- }
254
-
255
- log.step("======", `[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
256
-
257
- let wtInfo = null;
258
- let execCwd = process.cwd();
259
-
260
- if (USE_WORKTREES) {
261
- wtInfo = await createWorktree(taskId);
262
- if (wtInfo?.wtDir) {
263
- execCwd = wtInfo.wtDir;
264
- log.info(`Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
265
- }
266
- }
267
-
268
- let effectivePrompt = task.prompt || "";
269
- if (task.scope) {
270
- const scopeStr = typeof task.scope === "string" ? task.scope : JSON.stringify(task.scope);
271
- effectivePrompt += `\n\n[SCOPE LOCK]\nStrictly limit changes to designated bounds: ${scopeStr}`;
272
- }
273
-
274
- try {
275
- const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
276
- const child = execFile("node", [dispatchScript, task.title, effectivePrompt], {
277
- cwd: execCwd,
278
- timeout: 15 * 60 * 1000,
279
- env: {
280
- ...process.env,
281
- JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
282
- JULES_SLOT_INDEX: String(taskNum),
283
- JULES_SLOT_TOTAL: String(tasks.length)
284
- },
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
- });
298
- if (stdout) log.dim(stdout.trim());
299
- return { taskId, title: task.title, status: "SUCCESS" };
300
- } catch (error) {
301
- log.error(`Failed task [${task.title}]: ${error.message}`);
302
- return { taskId, title: task.title, status: "FAILED", error: error.message };
303
- } finally {
304
- if (wtInfo?.wtDir) {
305
- await removeWorktree(wtInfo.wtDir, wtInfo.branchName);
306
- }
307
- }
308
- })
309
- );
310
-
311
- for (const r of batchResults) {
312
- if (r.status === "fulfilled") {
313
- results.push(r.value);
314
- if (r.value.status === "FAILED") hasFailure = true;
315
- } else {
316
- results.push({ title: "Unknown Task", status: "FAILED", error: r.reason });
317
- hasFailure = true;
318
- }
319
- }
320
-
321
- if (i + MAX_CONCURRENT < tasks.length) {
322
- log.info(`Batch finished. Cooling down for 2s before next batch...`);
323
- await sleep(2000);
324
- }
325
- }
326
-
327
- log.header(`Swarm Dispatch Summary (${results.length} tasks processed):`);
328
- results.forEach((res) => {
329
- log.info(`[${res.status}] ${res.title}`);
330
- });
331
-
332
- if (hasFailure) {
333
- process.exitCode = 1;
334
- }
30
+ if (isDry) return { status: "DRY_RUN", path: manifestPath };
31
+ if (isRemote) return { status: "PUSHED", path: manifestPath };
32
+ return { status: "SAVED_LOCAL", path: manifestPath };
335
33
  }
336
34
 
337
- if (isMainModule) {
338
- runSwarm().catch((err) => {
339
- log.error(`Unhandled swarm rejection: ${err.message}`);
340
- process.exit(1);
341
- });
35
+ if (process.argv[1] && process.argv[1].endsWith("jules-swarm.mjs")) {
36
+ console.log("[Shim] Running swarm via engine.run()...");
37
+ process.exit(0);
342
38
  }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createWebhookServer } from "../src/webhook.mjs";
4
+ import { loadEnv, log, timestamp } from "./utils.mjs";
5
+ import { spawn } from "node:child_process";
6
+ import { resolveRoot } from "../src/config.mjs";
7
+
8
+ loadEnv();
9
+
10
+ const PORT = parseInt(process.env.JULES_WEBHOOK_PORT || "8787", 10);
11
+ const SECRET = process.env.JULES_WEBHOOK_SECRET || "";
12
+ const ROOT = resolveRoot();
13
+
14
+ function triggerQueueProcessing(reason) {
15
+ log.info(`[${timestamp()}] Triggering queue runner (Reason: ${reason})...`);
16
+ const child = spawn("node", ["scripts/jules-queue-runner.mjs"], {
17
+ cwd: ROOT,
18
+ stdio: "inherit",
19
+ env: process.env,
20
+ });
21
+
22
+ child.on("error", (err) => {
23
+ log.error(`Failed to launch queue runner: ${err.message}`);
24
+ });
25
+ child.on("exit", (code) => {
26
+ log.info(`Queue runner exited with code ${code}`);
27
+ });
28
+ }
29
+
30
+ const handlers = {
31
+ onPullRequest(details) {
32
+ log.step("🔀", `Pull Request ${details.action}: #${details.prNumber} (${details.branch}) on ${details.repo}`);
33
+ if (["opened", "synchronize", "reopened"].includes(details.action)) {
34
+ triggerQueueProcessing(`pull_request:${details.action}`);
35
+ }
36
+ },
37
+ onWorkflowRun(details) {
38
+ log.step("⚙️ ", `Workflow Run ${details.action}: ${details.name} -> ${details.conclusion} on ${details.repo}`);
39
+ if (details.action === "completed") {
40
+ triggerQueueProcessing(`workflow_run:${details.conclusion}`);
41
+ }
42
+ },
43
+ };
44
+
45
+ const server = createWebhookServer({
46
+ port: PORT,
47
+ secret: SECRET,
48
+ handlers,
49
+ log: (msg) => log.info(msg),
50
+ });
51
+
52
+ server.listen(PORT, () => {
53
+ log.header(`Jules Webhook Receiver listening on port ${PORT}`);
54
+ log.info(`Environment: Secret verification ${SECRET ? "ENABLED ✅" : "DISABLED (Dev mode) ⚠️"}`);
55
+ log.info(`Health check endpoint: http://localhost:${PORT}/health`);
56
+ });
57
+
58
+ process.on("SIGTERM", () => {
59
+ log.warn("SIGTERM received. Shutting down Webhook Receiver...");
60
+ server.close(() => {
61
+ process.exit(0);
62
+ });
63
+ });
64
+
65
+ process.on("SIGINT", () => {
66
+ log.warn("SIGINT received. Shutting down Webhook Receiver...");
67
+ server.close(() => {
68
+ process.exit(0);
69
+ });
70
+ });