jules-orchestrator-kit 0.8.1 → 0.9.3

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 (50) 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/.agent/rules/dynamic-guardrails.json +0 -4
  7. package/.github/workflows/jules-audit.yml +16 -5
  8. package/JULES_RULES_TEMPLATE.md +7 -2
  9. package/README.md +113 -21
  10. package/bin/agentctl.mjs +274 -0
  11. package/bin/init.js +1 -1
  12. package/index.mjs +34 -6
  13. package/package.json +22 -6
  14. package/scripts/asset-integrity-check.mjs +35 -0
  15. package/scripts/command-resolver.mjs +37 -214
  16. package/scripts/jules-cleanup.mjs +23 -174
  17. package/scripts/jules-create.mjs +12 -51
  18. package/scripts/jules-dispatch.mjs +103 -349
  19. package/scripts/jules-merge-swarm.mjs +30 -0
  20. package/scripts/jules-nightly.mjs +7 -124
  21. package/scripts/jules-patch.mjs +23 -0
  22. package/scripts/jules-queue-runner.mjs +28 -203
  23. package/scripts/jules-scan-todos.mjs +31 -125
  24. package/scripts/jules-self-audit.mjs +76 -578
  25. package/scripts/jules-status.mjs +42 -65
  26. package/scripts/jules-swarm.mjs +27 -232
  27. package/scripts/jules-webhook-receiver.mjs +70 -0
  28. package/scripts/lock-manager.mjs +11 -170
  29. package/scripts/risk-tier.mjs +29 -0
  30. package/scripts/rules-lint.mjs +20 -0
  31. package/scripts/stale-base-check.mjs +26 -0
  32. package/scripts/utils.mjs +118 -252
  33. package/scripts/validate-envelope.mjs +44 -0
  34. package/src/asset_integrity.mjs +76 -0
  35. package/src/config.mjs +325 -0
  36. package/src/engine.mjs +217 -0
  37. package/src/envelope.mjs +105 -0
  38. package/src/git.mjs +127 -0
  39. package/src/provider.mjs +179 -0
  40. package/src/risk.mjs +120 -0
  41. package/src/rules_budget.mjs +77 -0
  42. package/src/security.mjs +212 -0
  43. package/src/state.mjs +209 -0
  44. package/src/webhook.mjs +138 -0
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
  46. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
  47. package/.github/social-preview.png +0 -0
  48. package/.github/workflows/agent-scope-guard.yml +0 -57
  49. package/.github/workflows/jules-nightly.yml +0 -21
  50. package/.github/workflows/publish.yml +0 -24
@@ -0,0 +1,212 @@
1
+ import { normalizePath } from "./config.mjs";
2
+
3
+ export const HIGH_CONFIDENCE_PATTERNS = [
4
+ /\bghp_[A-Za-z0-9_]{36,255}\b/g,
5
+ /\bgho_[A-Za-z0-9_]{36,255}\b/g,
6
+ /\bghu_[A-Za-z0-9_]{36,255}\b/g,
7
+ /\bghs_[A-Za-z0-9_]{36,255}\b/g,
8
+ /\bghr_[A-Za-z0-9_]{36,255}\b/g,
9
+
10
+ /\bAKIA[0-9A-Z]{16}\b/g,
11
+ /\bASIA[0-9A-Z]{16}\b/g,
12
+ /\baws_secret_access_key\s*=\s*['"]?[A-Za-z0-9\/+=]{40}['"]?/g,
13
+
14
+ /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----[\s\S]*?-----END\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----/g,
15
+ /PuTTY-User-Key-File-[0-9]:[^\n]+/g,
16
+
17
+ /\bsk_live_[0-9a-zA-Z]{24,99}\b/g,
18
+ /\brk_live_[0-9a-zA-Z]{24,99}\b/g,
19
+ /\bnpm_[0-9a-zA-Z]{36}\b/g,
20
+ /\bglpat-[0-9a-zA-Z_-]{20,}\b/g,
21
+ /\bGOCSPX-[0-9a-zA-Z_-]{28}\b/g,
22
+
23
+ /\bAIzaSy[A-Za-z0-9_-]{33}\b/g,
24
+
25
+ /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_]+\/B[A-Za-z0-9_]+\/[A-Za-z0-9_]+/g,
26
+
27
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
28
+ ];
29
+
30
+ export const LOW_CONFIDENCE_PATTERNS = [
31
+ /Bearer\s+[A-Za-z0-9._~+/-]{10,}/gi,
32
+ /Authorization:\s*Bearer\s+[A-Za-z0-9._~+/-]+=*/gi,
33
+ /\bsk_test_[0-9a-zA-Z]{24,99}\b/g,
34
+ /(?:api[_-]?key|secret|password|passwd|token|auth[_-]?token)\s*[:=]\s*['"]([^'"]{8,128})['"]/gi,
35
+ ];
36
+
37
+ export function shannonEntropy(str) {
38
+ if (!str || typeof str !== "string") return 0;
39
+ const len = str.length;
40
+ const frequencies = {};
41
+ for (let i = 0; i < len; i++) {
42
+ const char = str[i];
43
+ frequencies[char] = (frequencies[char] || 0) + 1;
44
+ }
45
+ let entropy = 0;
46
+ for (const char in frequencies) {
47
+ const p = frequencies[char] / len;
48
+ entropy -= p * Math.log2(p);
49
+ }
50
+ return entropy;
51
+ }
52
+
53
+ export function hasHighConfidenceSecret(text) {
54
+ if (!text) return false;
55
+ return HIGH_CONFIDENCE_PATTERNS.some((pat) => {
56
+ pat.lastIndex = 0;
57
+ const res = pat.test(text);
58
+ pat.lastIndex = 0;
59
+ return res;
60
+ });
61
+ }
62
+
63
+ export function hasLowConfidenceSecret(text) {
64
+ if (!text) return false;
65
+ return LOW_CONFIDENCE_PATTERNS.some((pat) => {
66
+ pat.lastIndex = 0;
67
+ const res = pat.test(text);
68
+ pat.lastIndex = 0;
69
+ return res;
70
+ });
71
+ }
72
+
73
+ export function redactSecrets(text) {
74
+ if (!text) return "";
75
+ let sanitized = text;
76
+
77
+ for (const [envKey, envVal] of Object.entries(process.env)) {
78
+ if (
79
+ envVal &&
80
+ (envVal.length >= 20 || shannonEntropy(envVal) > 3.6) &&
81
+ /KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH|PASSPHRASE|URL|URI|DSN|CONNECTION|ACCOUNT/i.test(envKey)
82
+ ) {
83
+ if (sanitized.includes(envVal)) {
84
+ sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
85
+ }
86
+ }
87
+ }
88
+
89
+ const allPatterns = [...HIGH_CONFIDENCE_PATTERNS, ...LOW_CONFIDENCE_PATTERNS];
90
+ for (const pat of allPatterns) {
91
+ pat.lastIndex = 0;
92
+ sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
93
+ }
94
+
95
+ return sanitized;
96
+ }
97
+
98
+ export function anonymizePii(text) {
99
+ if (!text) return "";
100
+ let sanitized = text;
101
+
102
+ sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]");
103
+ sanitized = sanitized.replace(/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g, (ip) => {
104
+ if (ip === "127.0.0.1" || ip === "0.0.0.0") return ip;
105
+ return "[REDACTED_IP]";
106
+ });
107
+ sanitized = sanitized.replace(/(?:(?:\+\d{1,3}[\s-]?)|\b)\(?\d{2,4}\)?(?:[\s-]?\d{2,4}){2,4}\b/g, (phone) => {
108
+ const digitsOnly = phone.replace(/\D/g, "");
109
+ if (digitsOnly.length >= 7 && digitsOnly.length <= 15) {
110
+ return "[REDACTED_PHONE]";
111
+ }
112
+ return phone;
113
+ });
114
+
115
+ return sanitized;
116
+ }
117
+
118
+ export function matchesGlob(filePath, globPattern) {
119
+ if (!filePath || !globPattern) return false;
120
+ const file = normalizePath(filePath);
121
+ const pattern = normalizePath(globPattern);
122
+
123
+ if (file === pattern) return true;
124
+
125
+ const parts = pattern.split("/");
126
+ const regexParts = parts.map((part) => {
127
+ if (part === "**") return "___GLOBSTAR___";
128
+ if (part === "*") return "[^/]+";
129
+ const escaped = part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
130
+ return escaped.replace(/\\\*/g, "[^/]*").replace(/\\\?/g, ".");
131
+ });
132
+
133
+ let regexStr = regexParts.join("/");
134
+ regexStr = regexStr
135
+ .replace(/^___GLOBSTAR___\//g, "(?:.*/|^)")
136
+ .replace(/\/___GLOBSTAR___$/g, "(?:/.*|$)")
137
+ .replace(/\/___GLOBSTAR___\//g, "(?:/|/.+/|/)")
138
+ .replace(/___GLOBSTAR___/g, ".*");
139
+
140
+ try {
141
+ return new RegExp(`^${regexStr}$`).test(file);
142
+ } catch (_) {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ export function isForbiddenPath(filePath, config = {}) {
148
+ const normFile = normalizePath(filePath);
149
+ const forbidden = config.scope?.deny || config.forbidden_paths || [];
150
+ return forbidden.some((pattern) => matchesGlob(normFile, pattern));
151
+ }
152
+
153
+ export function checkScope(files = [], scope = {}, opts = {}) {
154
+ const violations = [];
155
+ const deny = scope.deny || [];
156
+ const allow = scope.allow || [];
157
+ const protect = scope.protect || [];
158
+
159
+ for (const rawFile of files) {
160
+ const file = normalizePath(rawFile);
161
+
162
+ const matchedDeny = deny.find((pat) => matchesGlob(file, pat));
163
+ if (matchedDeny) {
164
+ violations.push({ file, reason: `Forbidden path restriction matched pattern "${matchedDeny}"`, rule: "deny", pattern: matchedDeny });
165
+ continue;
166
+ }
167
+
168
+ if (allow.length > 0) {
169
+ const isExplicitlyAllowed = allow.some((pat) => matchesGlob(file, pat));
170
+ if (!isExplicitlyAllowed) {
171
+ violations.push({ file, reason: "Path not included in allowed paths list", rule: "allow" });
172
+ continue;
173
+ }
174
+ }
175
+
176
+ if (!opts.allowProtected) {
177
+ const matchedProtect = protect.find((pat) => matchesGlob(file, pat));
178
+ if (matchedProtect) {
179
+ violations.push({ file, reason: `Protected file modification restriction matched pattern "${matchedProtect}"`, rule: "protect", pattern: matchedProtect });
180
+ }
181
+ }
182
+ }
183
+
184
+ return {
185
+ ok: violations.length === 0,
186
+ violations,
187
+ };
188
+ }
189
+
190
+ export function scanDiff(diffTextStr = "") {
191
+ if (!diffTextStr) return { ok: true, findings: [] };
192
+ const addedLines = diffTextStr
193
+ .split("\n")
194
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
195
+ .map((line) => line.slice(1))
196
+ .join("\n");
197
+
198
+ const hasHigh = hasHighConfidenceSecret(addedLines);
199
+ const hasLow = !hasHigh && hasLowConfidenceSecret(addedLines);
200
+ const findings = [];
201
+
202
+ if (hasHigh) {
203
+ findings.push({ severity: "CRITICAL", type: "HIGH_CONFIDENCE_SECRET", description: "High-confidence secret pattern detected in added diff lines" });
204
+ } else if (hasLow) {
205
+ findings.push({ severity: "HIGH", type: "LOW_CONFIDENCE_SECRET", description: "Low-confidence secret or authorization token detected in added diff lines" });
206
+ }
207
+
208
+ return {
209
+ ok: !hasHigh && !hasLow,
210
+ findings,
211
+ };
212
+ }
package/src/state.mjs ADDED
@@ -0,0 +1,209 @@
1
+ import {
2
+ readFileSync,
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ openSync,
8
+ writeSync,
9
+ closeSync,
10
+ unlinkSync,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { resolveRoot } from "./config.mjs";
14
+
15
+ export function ensureDir(dirPath) {
16
+ if (!dirPath || typeof dirPath !== "string" || dirPath.includes("\0")) {
17
+ throw new Error("Invalid directory path provided");
18
+ }
19
+ if (!existsSync(dirPath)) {
20
+ mkdirSync(dirPath, { recursive: true });
21
+ }
22
+ return dirPath;
23
+ }
24
+
25
+ export function getQueueDir(rootOrOpts = resolveRoot()) {
26
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
27
+ const dir = join(root, ".agent/jules-queue");
28
+ ensureDir(dir);
29
+ return dir;
30
+ }
31
+
32
+ export function getStateDir(rootOrOpts = resolveRoot()) {
33
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
34
+ const dir = join(root, ".agent/state");
35
+ ensureDir(dir);
36
+ return dir;
37
+ }
38
+
39
+ export function getDailyLedgerPath(rootOrOpts = resolveRoot()) {
40
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
41
+ const dateStr = new Date().toISOString().split("T")[0];
42
+ return join(getStateDir(root), `ledger-${dateStr}.jsonl`);
43
+ }
44
+
45
+ /**
46
+ * Appends a structured audit event to the daily session ledger.
47
+ */
48
+ export function appendLedger(entry, rootOrOpts = resolveRoot()) {
49
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
50
+ const filePath = getDailyLedgerPath(root);
51
+ const payload = {
52
+ timestamp: new Date().toISOString(),
53
+ ...entry,
54
+ };
55
+ appendFileSync(filePath, JSON.stringify(payload) + "\n", "utf-8");
56
+ return payload;
57
+ }
58
+
59
+ export function readLedger(filePath) {
60
+ if (!filePath || !existsSync(filePath)) return [];
61
+ try {
62
+ const raw = readFileSync(filePath, "utf-8");
63
+ return raw
64
+ .split("\n")
65
+ .filter(Boolean)
66
+ .map((line) => {
67
+ const obj = JSON.parse(line);
68
+ return Object.freeze(obj);
69
+ });
70
+ } catch (_) {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Enforces a daily token budget.
77
+ */
78
+ export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
79
+ let root = typeof arg1 === "string" ? arg1 : resolveRoot();
80
+ let limit = typeof arg1 === "number" ? arg1 : typeof arg2 === "number" ? arg2 : 300;
81
+
82
+ const filePath = getDailyLedgerPath(root);
83
+ if (!existsSync(filePath)) {
84
+ return { ok: true, used: 0, budget: limit, remaining: limit };
85
+ }
86
+ try {
87
+ const content = readFileSync(filePath, "utf-8");
88
+ const lines = content.split("\n").filter(Boolean);
89
+ const count = lines.length;
90
+ return {
91
+ ok: count < limit,
92
+ used: count,
93
+ budget: limit,
94
+ remaining: Math.max(0, limit - count),
95
+ };
96
+ } catch (_) {
97
+ return { ok: true, used: 0, budget: limit, remaining: limit };
98
+ }
99
+ }
100
+
101
+ export async function withBudget(fn, root = resolveRoot(), limit = 300) {
102
+ const budget = checkDailyBudget(root, limit);
103
+ if (!budget.ok) {
104
+ throw new Error(`Daily budget exhausted (${budget.used}/${budget.budget} tasks executed)`);
105
+ }
106
+ return fn();
107
+ }
108
+
109
+ /**
110
+ * Atomic Advisory Lock File Manager with PID Vitality Check & Stale Lock Reaper (CWE-367 Fix).
111
+ */
112
+ export function getLockDir(rootOrOpts = resolveRoot()) {
113
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
114
+ const dir = join(getStateDir(root), "locks");
115
+ ensureDir(dir);
116
+ return dir;
117
+ }
118
+
119
+ export function isPidAlive(pid) {
120
+ if (!pid || typeof pid !== "number") return false;
121
+ try {
122
+ process.kill(pid, 0);
123
+ return true;
124
+ } catch (_) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
130
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
131
+ const lockDir = getLockDir(root);
132
+ const lockFile = join(lockDir, `${taskId}.json`);
133
+
134
+ // Stale Lock Reaper Check
135
+ if (existsSync(lockFile)) {
136
+ try {
137
+ const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
138
+ const isAlive = isPidAlive(existing.pid);
139
+ const isExpired = existing.acquiredAt && Date.now() - new Date(existing.acquiredAt).getTime() > 7200000;
140
+
141
+ if (!isAlive || isExpired) {
142
+ // Dead or expired holder; safely reap lock
143
+ try { unlinkSync(lockFile); } catch (_) {}
144
+ } else {
145
+ return { ok: false, holder: existing.agent, taskId, pid: existing.pid };
146
+ }
147
+ } catch (_) {
148
+ try { unlinkSync(lockFile); } catch (_) {}
149
+ }
150
+ }
151
+
152
+ const payload = {
153
+ agent: agentName,
154
+ taskId,
155
+ files,
156
+ pid: process.pid,
157
+ acquiredAt: new Date().toISOString(),
158
+ };
159
+
160
+ // Atomic file creation via 'wx' flag (Fixes TOCTOU Race Condition)
161
+ try {
162
+ const fd = openSync(lockFile, "wx");
163
+ writeSync(fd, JSON.stringify(payload, null, 2), "utf-8");
164
+ closeSync(fd);
165
+ return { ok: true, lockFile };
166
+ } catch (err) {
167
+ if (err.code === "EEXIST") {
168
+ try {
169
+ const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
170
+ return { ok: false, holder: existing.agent, taskId };
171
+ } catch (_) {
172
+ return { ok: false, holder: "unknown", taskId };
173
+ }
174
+ }
175
+ throw err;
176
+ }
177
+ }
178
+
179
+ export function releaseLock(taskId, rootOrOpts = resolveRoot()) {
180
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
181
+ const lockDir = getLockDir(root);
182
+ const lockFile = join(lockDir, `${taskId}.json`);
183
+
184
+ if (existsSync(lockFile)) {
185
+ try {
186
+ unlinkSync(lockFile); // CRITICAL FIX: Fixed ESM unlinkSync (removed legacy require("node:fs"))
187
+ return true;
188
+ } catch (_) {}
189
+ }
190
+ return false;
191
+ }
192
+
193
+ export function lockStatus(rootOrOpts = resolveRoot()) {
194
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
195
+ const lockDir = getLockDir(root);
196
+ const locks = [];
197
+ try {
198
+ const files = readdirSync(lockDir);
199
+ for (const file of files) {
200
+ if (file.endsWith(".json")) {
201
+ try {
202
+ const content = readFileSync(join(lockDir, file), "utf-8");
203
+ locks.push(Object.freeze(JSON.parse(content)));
204
+ } catch (_) {}
205
+ }
206
+ }
207
+ } catch (_) {}
208
+ return locks;
209
+ }
@@ -0,0 +1,138 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+
4
+ /**
5
+ * Verify GitHub webhook HMAC SHA-256 signature against request payload.
6
+ * @param {Buffer|string} payload - Raw request body
7
+ * @param {string} signatureHeader - Value of X-Hub-Signature-256 header
8
+ * @param {string} secret - Configured webhook secret
9
+ * @returns {boolean} True if signature matches
10
+ */
11
+ export function verifySignature(payload, signatureHeader, secret) {
12
+ if (!secret) return true; // If no secret is configured, skip verification (development mode)
13
+ if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;
14
+
15
+ const hmac = createHmac("sha256", secret);
16
+ const bodyBuf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf-8");
17
+ const digest = `sha256=${hmac.update(bodyBuf).digest("hex")}`;
18
+
19
+ const sigBuf = Buffer.from(signatureHeader, "utf-8");
20
+ const digestBuf = Buffer.from(digest, "utf-8");
21
+
22
+ if (sigBuf.length !== digestBuf.length) return false;
23
+ return timingSafeEqual(sigBuf, digestBuf);
24
+ }
25
+
26
+ /**
27
+ * Parse incoming webhook body buffer into a JS object.
28
+ * @param {Buffer} bodyBuf
29
+ * @param {string} contentType
30
+ * @returns {object} Parsed JSON payload
31
+ */
32
+ export function parseWebhookPayload(bodyBuf, contentType = "application/json") {
33
+ const str = bodyBuf.toString("utf-8");
34
+ if (contentType.includes("application/x-www-form-urlencoded")) {
35
+ const params = new URLSearchParams(str);
36
+ const payloadStr = params.get("payload");
37
+ return payloadStr ? JSON.parse(payloadStr) : {};
38
+ }
39
+ return str ? JSON.parse(str) : {};
40
+ }
41
+
42
+ /**
43
+ * Route and handle recognized GitHub event types.
44
+ * @param {string} eventType - Value of X-GitHub-Event header
45
+ * @param {object} payload - Parsed payload body
46
+ * @param {object} [handlers] - Custom action callbacks
47
+ * @returns {object} Response summary { handled: boolean, action: string, details: object }
48
+ */
49
+ export function routeWebhookEvent(eventType, payload, handlers = {}) {
50
+ if (eventType === "ping") {
51
+ return { handled: true, action: "ping", details: { zen: payload.zen, hookId: payload.hook_id } };
52
+ }
53
+
54
+ if (eventType === "pull_request") {
55
+ const action = payload.action;
56
+ const prNumber = payload.number || payload.pull_request?.number;
57
+ const repo = payload.repository?.full_name;
58
+ const branch = payload.pull_request?.head?.ref;
59
+ const merged = payload.pull_request?.merged || false;
60
+
61
+ const details = { action, prNumber, repo, branch, merged };
62
+ if (typeof handlers.onPullRequest === "function") {
63
+ handlers.onPullRequest(details, payload);
64
+ }
65
+ return { handled: true, action: `pull_request:${action}`, details };
66
+ }
67
+
68
+ if (eventType === "workflow_run") {
69
+ const action = payload.action;
70
+ const name = payload.workflow?.name || payload.workflow_run?.name;
71
+ const conclusion = payload.workflow_run?.conclusion;
72
+ const repo = payload.repository?.full_name;
73
+
74
+ const details = { action, name, conclusion, repo };
75
+ if (typeof handlers.onWorkflowRun === "function") {
76
+ handlers.onWorkflowRun(details, payload);
77
+ }
78
+ return { handled: true, action: `workflow_run:${action}`, details };
79
+ }
80
+
81
+ return { handled: false, action: eventType, details: {} };
82
+ }
83
+
84
+ /**
85
+ * Create zero-dependency HTTP Webhook Server.
86
+ * @param {object} config
87
+ * @param {number} [config.port=8787]
88
+ * @param {string} [config.secret]
89
+ * @param {object} [config.handlers]
90
+ * @param {function} [config.log]
91
+ * @returns {import("node:http").Server}
92
+ */
93
+ export function createWebhookServer({ _port = 8787, secret = process.env.JULES_WEBHOOK_SECRET, handlers = {}, log = console.log } = {}) {
94
+ const server = createServer((req, res) => {
95
+ if (req.method === "GET" && req.url === "/health") {
96
+ res.writeHead(200, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
98
+ return;
99
+ }
100
+
101
+ if (req.method !== "POST") {
102
+ res.writeHead(405, { "Content-Type": "application/json" });
103
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
104
+ return;
105
+ }
106
+
107
+ const chunks = [];
108
+ req.on("data", (chunk) => chunks.push(chunk));
109
+ req.on("end", () => {
110
+ const bodyBuf = Buffer.concat(chunks);
111
+ const sigHeader = req.headers["x-hub-signature-256"];
112
+ const eventType = req.headers["x-github-event"];
113
+ const contentType = req.headers["content-type"] || "application/json";
114
+
115
+ if (secret && !verifySignature(bodyBuf, sigHeader, secret)) {
116
+ log(`[Webhook] Invalid signature received from ${req.socket.remoteAddress}`);
117
+ res.writeHead(401, { "Content-Type": "application/json" });
118
+ res.end(JSON.stringify({ error: "Invalid signature" }));
119
+ return;
120
+ }
121
+
122
+ try {
123
+ const payload = parseWebhookPayload(bodyBuf, contentType);
124
+ const result = routeWebhookEvent(eventType, payload, handlers);
125
+
126
+ log(`[Webhook] Event: ${eventType} (${result.action}) handled=${result.handled}`);
127
+ res.writeHead(200, { "Content-Type": "application/json" });
128
+ res.end(JSON.stringify({ received: true, result }));
129
+ } catch (err) {
130
+ log(`[Webhook] Error parsing payload: ${err.message}`);
131
+ res.writeHead(400, { "Content-Type": "application/json" });
132
+ res.end(JSON.stringify({ error: "Malformed payload" }));
133
+ }
134
+ });
135
+ });
136
+
137
+ return server;
138
+ }
@@ -1,63 +0,0 @@
1
- name: Bug Report
2
- description: Create a report to help us improve Jules Orchestrator Kit
3
- title: "[BUG] "
4
- labels: ["bug"]
5
- assignees: []
6
- body:
7
- - type: markdown
8
- attributes:
9
- value: |
10
- Thanks for taking the time to fill out this bug report!
11
- - type: input
12
- id: version
13
- attributes:
14
- label: "Node.js Version"
15
- description: "What version of Node.js are you using? (e.g. v20.11.0)"
16
- placeholder: "v20.x"
17
- validations:
18
- required: true
19
- - type: dropdown
20
- id: package_manager
21
- attributes:
22
- label: "Package Manager"
23
- description: "Which package manager are you using?"
24
- options:
25
- - npm
26
- - pnpm
27
- - yarn
28
- - bun
29
- validations:
30
- required: true
31
- - type: dropdown
32
- id: execution_mode
33
- attributes:
34
- label: "Execution Mode"
35
- description: "How are you running Jules?"
36
- options:
37
- - API Mode (JULES_API_KEY set)
38
- - CLI Mode (Local jules binary)
39
- validations:
40
- required: true
41
- - type: textarea
42
- id: description
43
- attributes:
44
- label: "Bug Description"
45
- description: "A clear and concise description of what the bug is."
46
- placeholder: "When I run npm run jules:queue..."
47
- validations:
48
- required: true
49
- - type: textarea
50
- id: reproduction
51
- attributes:
52
- label: "Steps to Reproduce"
53
- description: "Steps to reproduce the behavior."
54
- placeholder: "1. Go to...\n2. Click on...\n3. See error"
55
- validations:
56
- required: true
57
- - type: textarea
58
- id: expected
59
- attributes:
60
- label: "Expected Behavior"
61
- description: "A clear and concise description of what you expected to happen."
62
- validations:
63
- required: true
@@ -1,34 +0,0 @@
1
- name: Feature Request
2
- description: Suggest an idea for Jules Orchestrator Kit
3
- title: "[FEATURE] "
4
- labels: ["enhancement"]
5
- assignees: []
6
- body:
7
- - type: markdown
8
- attributes:
9
- value: |
10
- Thanks for suggesting a new feature!
11
- - type: textarea
12
- id: description
13
- attributes:
14
- label: "Is your feature request related to a problem? Please describe."
15
- description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
16
- placeholder: "..."
17
- validations:
18
- required: true
19
- - type: textarea
20
- id: solution
21
- attributes:
22
- label: "Describe the solution you'd like"
23
- description: "A clear and concise description of what you want to happen."
24
- placeholder: "..."
25
- validations:
26
- required: true
27
- - type: textarea
28
- id: alternatives
29
- attributes:
30
- label: "Describe alternatives you've considered"
31
- description: "A clear and concise description of any alternative solutions or features you've considered."
32
- placeholder: "..."
33
- validations:
34
- required: false
Binary file