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.
- package/.agent/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/.agent/rules/dynamic-guardrails.json +0 -4
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +7 -2
- package/README.md +113 -21
- package/bin/agentctl.mjs +274 -0
- package/bin/init.js +1 -1
- package/index.mjs +34 -6
- package/package.json +22 -6
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -174
- package/scripts/jules-create.mjs +12 -51
- package/scripts/jules-dispatch.mjs +103 -349
- package/scripts/jules-merge-swarm.mjs +30 -0
- package/scripts/jules-nightly.mjs +7 -124
- package/scripts/jules-patch.mjs +23 -0
- package/scripts/jules-queue-runner.mjs +28 -203
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +76 -578
- package/scripts/jules-status.mjs +42 -65
- package/scripts/jules-swarm.mjs +27 -232
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -170
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +118 -252
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +209 -0
- package/src/webhook.mjs +138 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
package/scripts/jules-status.mjs
CHANGED
|
@@ -1,74 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { log, checkDailyBudget } from "./utils.mjs";
|
|
1
|
+
#!/usr/bin/env node
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const budgetInfo = checkDailyBudget();
|
|
9
|
-
|
|
10
|
-
if (!fs.existsSync(queueLogPath)) {
|
|
11
|
-
if (isJson) {
|
|
12
|
-
console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
|
|
13
|
-
} else {
|
|
14
|
-
log.info("Queue log is empty. No tasks have been dispatched yet.");
|
|
15
|
-
}
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const content = fs.readFileSync(queueLogPath, "utf-8");
|
|
21
|
-
const lines = content.split("\n").filter(Boolean);
|
|
22
|
-
|
|
23
|
-
if (lines.length === 0) {
|
|
24
|
-
if (isJson) {
|
|
25
|
-
console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
|
|
26
|
-
} else {
|
|
27
|
-
log.info("Queue log is empty.");
|
|
28
|
-
}
|
|
29
|
-
process.exit(0);
|
|
30
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Backward compatibility shim for jules-status.mjs in v0.9.0.
|
|
5
|
+
*/
|
|
31
6
|
|
|
32
|
-
|
|
33
|
-
|
|
7
|
+
import { checkDailyBudget } from "../src/state.mjs";
|
|
8
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
34
10
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
statusMap.set(entry.file, {
|
|
40
|
-
file: entry.file,
|
|
41
|
-
Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
|
|
42
|
-
Status: entry.status,
|
|
43
|
-
Timestamp: entry.timestamp,
|
|
44
|
-
Error: entry.error || null
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// Ignore malformed lines
|
|
49
|
-
}
|
|
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";
|
|
50
15
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (isJson) {
|
|
55
|
-
console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
|
|
56
|
-
process.exit(0);
|
|
16
|
+
if (s === "IN_PROGRESS" || s === "DISPATCHED") {
|
|
17
|
+
return "in_progress";
|
|
57
18
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
log.info("No parseable task events found in queue.jsonl");
|
|
61
|
-
process.exit(0);
|
|
19
|
+
if (s === "COMPLETED" || s === "FAILED") {
|
|
20
|
+
return "completed";
|
|
62
21
|
}
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
63
24
|
|
|
64
|
-
|
|
65
|
-
|
|
25
|
+
const isJson = process.argv.includes("--json");
|
|
26
|
+
const root = process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
27
|
+
const budget = checkDailyBudget(root);
|
|
28
|
+
|
|
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
|
+
}
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
log.error(`Failed to read queue log: ${err.message}`);
|
|
72
|
-
}
|
|
73
|
-
process.exit(1);
|
|
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.`);
|
|
74
51
|
}
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -1,243 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { log, sleep } from "./utils.mjs";
|
|
1
|
+
#!/usr/bin/env node
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Backward compatibility shim for jules-swarm.mjs in v0.9.0.
|
|
5
|
+
*/
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export function buildSyncManifest(tasks = []) {
|
|
11
|
+
const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
totalTasks: tasks.length,
|
|
15
|
+
reservations,
|
|
16
|
+
};
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
|
|
24
|
-
if (!Array.isArray(tasks)) {
|
|
25
|
-
log.error("tasks.json must contain a JSON array of task objects.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
|
|
30
|
-
const MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
|
|
31
|
-
|
|
32
|
-
const parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
|
|
33
|
-
const STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
|
|
34
|
-
|
|
35
|
-
const USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
|
|
36
|
-
|
|
37
|
-
// Ensure root project path is preserved for history logs even when running in worktrees
|
|
38
|
-
process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
39
|
-
|
|
40
|
-
log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
|
|
41
|
-
|
|
42
|
-
const activeWorktrees = new Set();
|
|
19
|
+
export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
|
|
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";
|
|
43
22
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
fs.chmodSync(item.wtDir, 0o755);
|
|
49
|
-
} catch (_) {}
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
|
|
53
|
-
} catch (_) {}
|
|
54
|
-
if (item.branchName) {
|
|
55
|
-
try {
|
|
56
|
-
execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
|
|
57
|
-
} catch (_) {}
|
|
58
|
-
}
|
|
23
|
+
const agentDir = join(projectRoot, ".agent");
|
|
24
|
+
if (!existsSync(agentDir)) {
|
|
25
|
+
try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
|
|
59
26
|
}
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function reapOrphanedWorktrees() {
|
|
64
|
-
const worktreesBase = path.resolve(process.cwd(), ".agent/worktrees");
|
|
65
|
-
if (!fs.existsSync(worktreesBase)) return;
|
|
66
|
-
try {
|
|
67
|
-
const entries = fs.readdirSync(worktreesBase);
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const wtPath = path.join(worktreesBase, entry);
|
|
70
|
-
try {
|
|
71
|
-
execFileSync("git", ["worktree", "remove", "--force", wtPath], { stdio: "ignore" });
|
|
72
|
-
} catch (_) {
|
|
73
|
-
try {
|
|
74
|
-
fs.rmSync(wtPath, { recursive: true, force: true });
|
|
75
|
-
} catch (_) {}
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
execFileSync("git", ["branch", "-D", `jules/${entry}`], { stdio: "ignore" });
|
|
79
|
-
} catch (_) {}
|
|
80
|
-
}
|
|
81
|
-
} catch (_) {}
|
|
82
|
-
try {
|
|
83
|
-
execFileSync("git", ["worktree", "prune"], { stdio: "ignore" });
|
|
84
|
-
} catch (_) {}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
process.on("SIGINT", () => {
|
|
88
|
-
log.dim("SIGINT received. Cleaning up active Git worktrees...");
|
|
89
|
-
cleanupAllWorktreesSync();
|
|
90
|
-
process.exit(130);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
process.on("SIGTERM", () => {
|
|
94
|
-
log.dim("SIGTERM received. Cleaning up active Git worktrees...");
|
|
95
|
-
cleanupAllWorktreesSync();
|
|
96
|
-
process.exit(143);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
async function createWorktree(taskId) {
|
|
100
|
-
const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
101
|
-
const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
|
|
102
|
-
const branchName = `jules/${slug}`;
|
|
103
|
-
let item = null;
|
|
104
|
-
await fs.promises.mkdir(path.dirname(wtDir), { recursive: true });
|
|
105
|
-
|
|
106
|
-
const maxRetries = 3;
|
|
107
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
108
|
-
try {
|
|
109
|
-
await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
|
|
110
|
-
item = { wtDir, branchName };
|
|
111
|
-
break;
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (attempt < maxRetries) {
|
|
114
|
-
const jitter = Math.floor(Math.random() * 200) + 100 * Math.pow(2, attempt);
|
|
115
|
-
await sleep(jitter);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
|
|
120
|
-
item = { wtDir, branchName: null };
|
|
121
|
-
} catch (fallbackErr) {
|
|
122
|
-
log.error(`Failed to create worktree for ${taskId}: ${fallbackErr.message}`);
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (item) activeWorktrees.add(item);
|
|
128
|
-
return item;
|
|
129
|
-
}
|
|
27
|
+
const manifestPath = join(agentDir, "sync-manifest.json");
|
|
28
|
+
try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
|
|
130
29
|
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
if (item.wtDir === wtDir) {
|
|
135
|
-
activeWorktrees.delete(item);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
log.error(`Failed to remove worktree at ${wtDir}: ${err.message}`);
|
|
142
|
-
}
|
|
143
|
-
if (branchName) {
|
|
144
|
-
try {
|
|
145
|
-
await execFileAsync("git", ["branch", "-D", branchName]);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
log.error(`Failed to delete swarm branch ${branchName}: ${err.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
30
|
+
if (isDry) return { status: "DRY_RUN", path: manifestPath };
|
|
31
|
+
if (isRemote) return { status: "PUSHED", path: manifestPath };
|
|
32
|
+
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
150
33
|
}
|
|
151
34
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
const results = [];
|
|
157
|
-
let hasFailure = false;
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
|
|
160
|
-
const batch = tasks.slice(i, i + MAX_CONCURRENT);
|
|
161
|
-
const batchResults = await Promise.allSettled(
|
|
162
|
-
batch.map(async (task, bIdx) => {
|
|
163
|
-
const taskNum = i + bIdx + 1;
|
|
164
|
-
const taskId = task.id || `task-${taskNum}`;
|
|
165
|
-
|
|
166
|
-
if (bIdx > 0) {
|
|
167
|
-
await sleep(STAGGER_MS * bIdx);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
log.step("======", `[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
|
|
171
|
-
|
|
172
|
-
let wtInfo = null;
|
|
173
|
-
let execCwd = process.cwd();
|
|
174
|
-
|
|
175
|
-
if (USE_WORKTREES) {
|
|
176
|
-
wtInfo = await createWorktree(taskId);
|
|
177
|
-
if (wtInfo?.wtDir) {
|
|
178
|
-
execCwd = wtInfo.wtDir;
|
|
179
|
-
log.info(`Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let effectivePrompt = task.prompt || "";
|
|
184
|
-
if (task.scope) {
|
|
185
|
-
const scopeStr = typeof task.scope === "string" ? task.scope : JSON.stringify(task.scope);
|
|
186
|
-
effectivePrompt += `\n\n[SCOPE LOCK]\nStrictly limit changes to designated bounds: ${scopeStr}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
191
|
-
const { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
|
|
192
|
-
cwd: execCwd,
|
|
193
|
-
timeout: 15 * 60 * 1000,
|
|
194
|
-
env: {
|
|
195
|
-
...process.env,
|
|
196
|
-
JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
|
|
197
|
-
JULES_SLOT_INDEX: String(taskNum),
|
|
198
|
-
JULES_SLOT_TOTAL: String(tasks.length)
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
if (stdout) log.dim(stdout.trim());
|
|
202
|
-
return { taskId, title: task.title, status: "SUCCESS" };
|
|
203
|
-
} catch (error) {
|
|
204
|
-
log.error(`Failed task [${task.title}]: ${error.message}`);
|
|
205
|
-
return { taskId, title: task.title, status: "FAILED", error: error.message };
|
|
206
|
-
} finally {
|
|
207
|
-
if (wtInfo?.wtDir) {
|
|
208
|
-
await removeWorktree(wtInfo.wtDir, wtInfo.branchName);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
})
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
for (const r of batchResults) {
|
|
215
|
-
if (r.status === "fulfilled") {
|
|
216
|
-
results.push(r.value);
|
|
217
|
-
if (r.value.status === "FAILED") hasFailure = true;
|
|
218
|
-
} else {
|
|
219
|
-
results.push({ title: "Unknown Task", status: "FAILED", error: r.reason });
|
|
220
|
-
hasFailure = true;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (i + MAX_CONCURRENT < tasks.length) {
|
|
225
|
-
log.info(`Batch finished. Cooling down for 2s before next batch...`);
|
|
226
|
-
await sleep(2000);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
log.header(`Swarm Dispatch Summary (${results.length} tasks processed):`);
|
|
231
|
-
results.forEach((res) => {
|
|
232
|
-
log.info(`[${res.status}] ${res.title}`);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
if (hasFailure) {
|
|
236
|
-
process.exitCode = 1;
|
|
237
|
-
}
|
|
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);
|
|
238
38
|
}
|
|
239
|
-
|
|
240
|
-
runSwarm().catch((err) => {
|
|
241
|
-
log.error(`Unhandled swarm rejection: ${err.message}`);
|
|
242
|
-
process.exit(1);
|
|
243
|
-
});
|
|
@@ -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
|
+
});
|
package/scripts/lock-manager.mjs
CHANGED
|
@@ -1,181 +1,22 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* lock-manager.mjs
|
|
5
|
-
*
|
|
6
|
-
* Manages multi-agent file locks to prevent concurrent modification collisions.
|
|
7
|
-
* Uses .agent/sync-manifest.json as the lock registry.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node scripts/lock-manager.mjs acquire <agent_name> <task_id> <file_path1> <file_path2> [--unattended]
|
|
11
|
-
* node scripts/lock-manager.mjs release <task_id>
|
|
2
|
+
* Backward compatibility shim for lock-manager.mjs in v0.9.0.
|
|
3
|
+
* Delegates to src/state.mjs.
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
|
-
import
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
|
|
17
|
-
const MANIFEST_PATH = path.join(process.cwd(), '.agent', 'sync-manifest.json');
|
|
18
|
-
const TTL_MS = 20 * 60 * 1000; // 20 minutes
|
|
19
|
-
|
|
20
|
-
async function readManifest() {
|
|
21
|
-
try {
|
|
22
|
-
const data = await fs.readFile(MANIFEST_PATH, 'utf-8');
|
|
23
|
-
return JSON.parse(data);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
if (err.code === 'ENOENT') {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
throw err;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function writeManifest(manifest) {
|
|
33
|
-
await fs.mkdir(path.dirname(MANIFEST_PATH), { recursive: true });
|
|
34
|
-
const tmp = MANIFEST_PATH + `.tmp-${process.pid}`;
|
|
35
|
-
await fs.writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
36
|
-
await fs.rename(tmp, MANIFEST_PATH);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function withManifestLock(fn) {
|
|
40
|
-
const lockPath = MANIFEST_PATH + ".lock";
|
|
41
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
42
|
-
for (let i = 0; i < 50; i++) {
|
|
43
|
-
try {
|
|
44
|
-
const stats = await fs.stat(lockPath);
|
|
45
|
-
if (Date.now() - stats.mtimeMs > 5 * 60 * 1000) {
|
|
46
|
-
await fs.rm(lockPath, { force: true });
|
|
47
|
-
}
|
|
48
|
-
} catch (_) {}
|
|
49
|
-
|
|
50
|
-
let handle;
|
|
51
|
-
try {
|
|
52
|
-
handle = await fs.open(lockPath, "wx");
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (err.code !== "EEXIST") throw err;
|
|
55
|
-
await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
try { return await fn(); }
|
|
59
|
-
finally {
|
|
60
|
-
try { await handle.close(); } catch (_) {}
|
|
61
|
-
await fs.rm(lockPath, { force: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw new Error("Could not acquire manifest lock");
|
|
65
|
-
}
|
|
6
|
+
import { acquireLock, releaseLock, lockStatus } from "../src/state.mjs";
|
|
66
7
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
let changed = false;
|
|
70
|
-
for (const [file, lock] of Object.entries(manifest)) {
|
|
71
|
-
if (lock.expiresAt < now) {
|
|
72
|
-
delete manifest[file];
|
|
73
|
-
changed = true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return changed;
|
|
8
|
+
export function acquire(agent, taskId, filePaths = [], opts = {}) {
|
|
9
|
+
return acquireLock(agent, taskId, filePaths, opts);
|
|
77
10
|
}
|
|
78
11
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const manifest = await readManifest();
|
|
82
|
-
cleanExpiredLocks(manifest);
|
|
83
|
-
|
|
84
|
-
const now = Date.now();
|
|
85
|
-
const expiresAt = now + TTL_MS;
|
|
86
|
-
|
|
87
|
-
const conflicts = [];
|
|
88
|
-
for (const file of files) {
|
|
89
|
-
const normalizedFile = path.normalize(file);
|
|
90
|
-
if (manifest[normalizedFile] && manifest[normalizedFile].taskId !== taskId) {
|
|
91
|
-
conflicts.push({
|
|
92
|
-
file: normalizedFile,
|
|
93
|
-
lockedBy: manifest[normalizedFile].agentName,
|
|
94
|
-
task: manifest[normalizedFile].taskId
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (conflicts.length > 0) {
|
|
100
|
-
let msg = `Cannot acquire locks for task ${taskId}. Conflicts detected:\n`;
|
|
101
|
-
for (const conflict of conflicts) {
|
|
102
|
-
msg += ` - ${conflict.file} is locked by ${conflict.lockedBy} (Task: ${conflict.task})\n`;
|
|
103
|
-
}
|
|
104
|
-
throw new Error(msg.trim());
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const file of files) {
|
|
108
|
-
const normalizedFile = path.normalize(file);
|
|
109
|
-
manifest[normalizedFile] = {
|
|
110
|
-
agentName,
|
|
111
|
-
taskId,
|
|
112
|
-
acquiredAt: now,
|
|
113
|
-
expiresAt
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await writeManifest(manifest);
|
|
118
|
-
console.log(`SUCCESS: Acquired locks for task ${taskId} on ${files.length} files.`);
|
|
119
|
-
});
|
|
12
|
+
export function release(agent, taskId, opts = {}) {
|
|
13
|
+
return releaseLock(agent, taskId, opts);
|
|
120
14
|
}
|
|
121
15
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const manifest = await readManifest();
|
|
125
|
-
let releasedCount = 0;
|
|
126
|
-
|
|
127
|
-
for (const [file, lock] of Object.entries(manifest)) {
|
|
128
|
-
if (lock.taskId === taskId) {
|
|
129
|
-
delete manifest[file];
|
|
130
|
-
releasedCount++;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (releasedCount > 0) {
|
|
135
|
-
await writeManifest(manifest);
|
|
136
|
-
console.log(`SUCCESS: Released ${releasedCount} locks for task ${taskId}.`);
|
|
137
|
-
} else {
|
|
138
|
-
console.log(`INFO: No locks found for task ${taskId}.`);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
16
|
+
export function status(opts = {}) {
|
|
17
|
+
return lockStatus(opts);
|
|
141
18
|
}
|
|
142
19
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const command = args[0];
|
|
146
|
-
|
|
147
|
-
if (command === 'acquire') {
|
|
148
|
-
const agentName = args[1];
|
|
149
|
-
const taskId = args[2];
|
|
150
|
-
const unattendedIndex = args.indexOf('--unattended');
|
|
151
|
-
|
|
152
|
-
let files = [];
|
|
153
|
-
if (unattendedIndex !== -1) {
|
|
154
|
-
files = args.slice(3, unattendedIndex).concat(args.slice(unattendedIndex + 1));
|
|
155
|
-
} else {
|
|
156
|
-
files = args.slice(3);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!agentName || !taskId || files.length === 0) {
|
|
160
|
-
console.error('Usage: acquire <agent_name> <task_id> <file_paths...>');
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
await acquireLocks(agentName, taskId, files);
|
|
165
|
-
} else if (command === 'release') {
|
|
166
|
-
const taskId = args[1];
|
|
167
|
-
if (!taskId) {
|
|
168
|
-
console.error('Usage: release <task_id>');
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
await releaseLocks(taskId);
|
|
172
|
-
} else {
|
|
173
|
-
console.error('Unknown command. Use "acquire" or "release".');
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
20
|
+
export function cleanup() {
|
|
21
|
+
return { removed: 0 };
|
|
176
22
|
}
|
|
177
|
-
|
|
178
|
-
main().catch(err => {
|
|
179
|
-
console.error('Unhandled error:', err);
|
|
180
|
-
process.exit(1);
|
|
181
|
-
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { changedFiles } from "../src/git.mjs";
|
|
4
|
+
import { classifyRiskTier } from "../src/risk.mjs";
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const baseBranch = process.env.BASE_BRANCH || "main";
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const files = changedFiles(root, baseBranch);
|
|
11
|
+
const result = classifyRiskTier(files);
|
|
12
|
+
|
|
13
|
+
console.log(`[risk-tier] Classified PR Risk Tier: ${result.tier}`);
|
|
14
|
+
console.log(`[risk-tier] Reason: ${result.reason}`);
|
|
15
|
+
console.log(`[risk-tier] Auto-Merge Allowed: ${result.isAutoMergeAllowed}`);
|
|
16
|
+
console.log(`[risk-tier] Requires Human Review: ${result.requiresHumanReview}`);
|
|
17
|
+
|
|
18
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
19
|
+
const { appendFileSync } = await import("node:fs");
|
|
20
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `risk_tier=${result.tier}\n`);
|
|
21
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `auto_merge=${result.isAutoMergeAllowed}\n`);
|
|
22
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `requires_human=${result.requiresHumanReview}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.exit(0);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`❌ Risk tier classification failed: ${err.message}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|