jules-orchestrator-kit 0.21.0 ā 0.22.1
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/.github/workflows/jules-audit.yml +1 -1
- package/README.md +2 -2
- package/bin/agentctl.mjs +3 -3
- package/index.mjs +7 -0
- package/package.json +2 -2
- package/scripts/release.mjs +14 -4
- package/src/state.mjs +101 -33
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
[](https://github.com/FullThrottle83/jules-orchestrator-kit/actions/workflows/jules-audit.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/jules-orchestrator-kit)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
|
-
[](https://nodejs.org)
|
|
9
9
|
|
|
10
10
|
> **High-Volume Autonomous Orchestration Engine for Google Jules**
|
|
11
|
-
> Built specifically to execute 300+ daily agent sessions and parallel swarms safely. Zero external runtime dependencies. Built strictly on native Node.js
|
|
11
|
+
> Built specifically to execute 300+ daily agent sessions and parallel swarms safely. Zero external runtime dependencies. Built strictly on native Node.js 20+ ESM.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const command = args[0];
|
|
|
13
13
|
|
|
14
14
|
function printHelp() {
|
|
15
15
|
console.log(`
|
|
16
|
-
š agentctl v0.
|
|
16
|
+
š agentctl v0.22.1 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
17
17
|
|
|
18
18
|
Usage: agentctl <command> [options]
|
|
19
19
|
|
|
@@ -43,7 +43,7 @@ async function main() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
46
|
-
console.log("agentctl v0.
|
|
46
|
+
console.log("agentctl v0.22.1");
|
|
47
47
|
process.exit(0);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -223,7 +223,7 @@ async function main() {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
case "doctor": {
|
|
226
|
-
console.log(`\nš agentctl System Diagnostics (v0.
|
|
226
|
+
console.log(`\nš agentctl System Diagnostics (v0.22.1)`);
|
|
227
227
|
console.log(`--------------------------------------------------`);
|
|
228
228
|
console.log(` Project Root : ${root}`);
|
|
229
229
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/index.mjs
CHANGED
|
@@ -21,12 +21,19 @@ export {
|
|
|
21
21
|
readLedger,
|
|
22
22
|
verifyLedgerIntegrity,
|
|
23
23
|
reserveBudget,
|
|
24
|
+
reserveBudgetAtomic,
|
|
24
25
|
commitBudgetReservation,
|
|
25
26
|
withBudget,
|
|
26
27
|
checkDailyBudget,
|
|
27
28
|
acquireLock,
|
|
28
29
|
releaseLock,
|
|
29
30
|
lockStatus,
|
|
31
|
+
getLockDir,
|
|
32
|
+
withVfsMutex,
|
|
33
|
+
MutexTimeoutError,
|
|
34
|
+
BudgetError,
|
|
35
|
+
isPidAlive,
|
|
36
|
+
getProcessStartTime,
|
|
30
37
|
} from "./src/state.mjs";
|
|
31
38
|
export { gate, dispatch, repair, run, fingerprintFailureState } from "./src/engine.mjs";
|
|
32
39
|
export { validateEnvelope } from "./src/envelope.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Orchestration kit for running Google Jules autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"lint": "eslint ."
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
|
-
"node": ">=
|
|
65
|
+
"node": ">=20.0.0"
|
|
66
66
|
},
|
|
67
67
|
"keywords": [
|
|
68
68
|
"jules",
|
package/scripts/release.mjs
CHANGED
|
@@ -55,12 +55,22 @@ if (!notes) {
|
|
|
55
55
|
console.log(" ā
Extracted release notes from CHANGELOG.md.\n");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// 3. Create Git Tag if not exists
|
|
58
|
+
// 3. Create Git Tag if not exists (or update if version mismatch)
|
|
59
59
|
console.log(`3. Checking Git tag ${tagName}...`);
|
|
60
60
|
try {
|
|
61
|
-
const tags = execSync("git tag -l", { cwd: root, encoding: "utf-8" });
|
|
62
|
-
if (tags.
|
|
63
|
-
|
|
61
|
+
const tags = execSync("git tag -l", { cwd: root, encoding: "utf-8" }).split("\n").map((t) => t.trim());
|
|
62
|
+
if (tags.includes(tagName)) {
|
|
63
|
+
let taggedVersion = "";
|
|
64
|
+
try {
|
|
65
|
+
const rawPkg = execSync(`git show ${tagName}:package.json`, { cwd: root, encoding: "utf-8" });
|
|
66
|
+
taggedVersion = JSON.parse(rawPkg).version;
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
if (taggedVersion && taggedVersion !== version) {
|
|
69
|
+
console.log(` ā ļø Tag ${tagName} points to package.json version ${taggedVersion}. Updating tag to HEAD (${version})...`);
|
|
70
|
+
execSync(`git tag -f -a ${tagName} -m "${pkg.name} ${tagName}"`, { cwd: root, stdio: "inherit" });
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` ā¹ļø Git tag ${tagName} already exists.`);
|
|
73
|
+
}
|
|
64
74
|
} else {
|
|
65
75
|
execSync(`git tag -a ${tagName} -m "${pkg.name} ${tagName}"`, { cwd: root, stdio: "inherit" });
|
|
66
76
|
console.log(` ā
Created Git tag ${tagName}.`);
|
package/src/state.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
unlinkSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
14
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
15
15
|
import { hostname } from "node:os";
|
|
16
16
|
import { resolveRoot } from "./config.mjs";
|
|
17
17
|
|
|
@@ -34,7 +34,7 @@ export function getQueueDir(rootOrOpts = resolveRoot()) {
|
|
|
34
34
|
|
|
35
35
|
export function getStateDir(rootOrOpts = resolveRoot()) {
|
|
36
36
|
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
37
|
-
const dir = join(root, ".agent/state");
|
|
37
|
+
const dir = root.endsWith(".agent/state") ? root : join(root, ".agent/state");
|
|
38
38
|
ensureDir(dir);
|
|
39
39
|
return dir;
|
|
40
40
|
}
|
|
@@ -45,11 +45,19 @@ export function getDailyLedgerPath(rootOrOpts = resolveRoot()) {
|
|
|
45
45
|
return join(getStateDir(root), `ledger-${dateStr}.jsonl`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export class MutexTimeoutError extends Error {
|
|
49
|
+
constructor(message = "Failed to acquire VFS mutex lock within timeout") {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "MutexTimeoutError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
/**
|
|
49
56
|
* Executes a function inside a kernel-level VFS directory mutex for strict linearizability.
|
|
57
|
+
* Fail-closed: Throws MutexTimeoutError if lock acquisition times out.
|
|
50
58
|
*/
|
|
51
59
|
export function withVfsMutex(mutexDir, fn, opts = {}) {
|
|
52
|
-
const maxRetries = opts.maxRetries ||
|
|
60
|
+
const maxRetries = opts.maxRetries || 200;
|
|
53
61
|
const retryDelayMs = opts.retryDelayMs || 10;
|
|
54
62
|
|
|
55
63
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -71,7 +79,7 @@ export function withVfsMutex(mutexDir, fn, opts = {}) {
|
|
|
71
79
|
throw err;
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
throw new MutexTimeoutError(`Failed to acquire VFS mutex lock at ${mutexDir} after ${maxRetries} retries`);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -80,7 +88,7 @@ export function withVfsMutex(mutexDir, fn, opts = {}) {
|
|
|
80
88
|
export function appendLedger(entry, rootOrOpts = resolveRoot()) {
|
|
81
89
|
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
82
90
|
const filePath = getDailyLedgerPath(root);
|
|
83
|
-
const mutexDir = join(getStateDir(root), ".
|
|
91
|
+
const mutexDir = join(getStateDir(root), ".budget.mutex");
|
|
84
92
|
|
|
85
93
|
return withVfsMutex(mutexDir, () => {
|
|
86
94
|
let prevHash = "0".repeat(64);
|
|
@@ -200,16 +208,64 @@ export class BudgetError extends Error {
|
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
|
|
203
|
-
export function
|
|
204
|
-
const root = typeof
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
export function reserveBudgetAtomic(stateDirOrRoot = resolveRoot(), limit = 300, opts = {}) {
|
|
212
|
+
const root = typeof stateDirOrRoot === "string" ? stateDirOrRoot : resolveRoot();
|
|
213
|
+
const stateDir = getStateDir(root);
|
|
214
|
+
const mutexDir = join(stateDir, ".budget.mutex");
|
|
215
|
+
|
|
216
|
+
return withVfsMutex(mutexDir, () => {
|
|
217
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
218
|
+
const filePath = join(stateDir, `ledger-${dateStr}.jsonl`);
|
|
219
|
+
|
|
220
|
+
let count = 0;
|
|
221
|
+
let prevHash = "0".repeat(64);
|
|
222
|
+
|
|
223
|
+
if (existsSync(filePath)) {
|
|
224
|
+
try {
|
|
225
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
226
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
227
|
+
count = lines.length;
|
|
228
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
229
|
+
try {
|
|
230
|
+
const lastObj = JSON.parse(lines[i]);
|
|
231
|
+
if (lastObj.hash) {
|
|
232
|
+
prevHash = lastObj.hash;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
}
|
|
237
|
+
} catch (_) {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (count >= limit) {
|
|
241
|
+
throw new BudgetError(`Daily budget exhausted (${count}/${limit} tasks executed)`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const timestamp = new Date().toISOString();
|
|
245
|
+
const reservationId = `res-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
246
|
+
const rawPayload = { timestamp, event: "budget_reserved", reservationId, budget: limit, prevHash };
|
|
247
|
+
const hash = createHash("sha256").update(JSON.stringify(rawPayload)).digest("hex");
|
|
248
|
+
const payload = { ...rawPayload, hash };
|
|
249
|
+
|
|
250
|
+
const fd = openSync(filePath, "a");
|
|
251
|
+
try {
|
|
252
|
+
writeSync(fd, JSON.stringify(payload) + "\n", "utf-8");
|
|
253
|
+
fsyncSync(fd);
|
|
254
|
+
} finally {
|
|
255
|
+
closeSync(fd);
|
|
256
|
+
}
|
|
209
257
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
reservationId,
|
|
261
|
+
remaining: Math.max(0, limit - (count + 1)),
|
|
262
|
+
used: count + 1,
|
|
263
|
+
};
|
|
264
|
+
}, opts);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function reserveBudget(rootOrOpts = resolveRoot(), limit = 300) {
|
|
268
|
+
return reserveBudgetAtomic(rootOrOpts, limit);
|
|
213
269
|
}
|
|
214
270
|
|
|
215
271
|
export function commitBudgetReservation(rootOrOpts = resolveRoot(), reservationId = "") {
|
|
@@ -236,8 +292,25 @@ export function getLockDir(rootOrOpts = resolveRoot()) {
|
|
|
236
292
|
return dir;
|
|
237
293
|
}
|
|
238
294
|
|
|
295
|
+
export function getProcessStartTime(pid) {
|
|
296
|
+
if (!pid || typeof pid !== "number") return null;
|
|
297
|
+
if (process.platform === "linux") {
|
|
298
|
+
try {
|
|
299
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
300
|
+
const rpar = stat.lastIndexOf(")");
|
|
301
|
+
if (rpar !== -1) {
|
|
302
|
+
const rest = stat.slice(rpar + 1).trim().split(/\s+/);
|
|
303
|
+
return rest[19] || null;
|
|
304
|
+
}
|
|
305
|
+
const parts = stat.trim().split(/\s+/);
|
|
306
|
+
return parts[21] || null;
|
|
307
|
+
} catch (_) {}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
239
312
|
/**
|
|
240
|
-
* Checks if a PID is alive with
|
|
313
|
+
* Checks if a PID is alive with PID-recycling start time validation on Linux.
|
|
241
314
|
*/
|
|
242
315
|
export function isPidAlive(pid, expectedStartTime = null) {
|
|
243
316
|
if (!pid || typeof pid !== "number") return false;
|
|
@@ -247,30 +320,20 @@ export function isPidAlive(pid, expectedStartTime = null) {
|
|
|
247
320
|
return false;
|
|
248
321
|
}
|
|
249
322
|
|
|
250
|
-
if (expectedStartTime && process.platform === "linux") {
|
|
323
|
+
if (expectedStartTime !== null && expectedStartTime !== undefined && process.platform === "linux") {
|
|
251
324
|
try {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const startTime = parts[21];
|
|
255
|
-
if (startTime && String(startTime) !== String(expectedStartTime)) {
|
|
325
|
+
const actualStartTime = getProcessStartTime(pid);
|
|
326
|
+
if (!actualStartTime || String(actualStartTime) !== String(expectedStartTime)) {
|
|
256
327
|
return false;
|
|
257
328
|
}
|
|
258
|
-
} catch (_) {
|
|
329
|
+
} catch (_) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
259
332
|
}
|
|
260
333
|
|
|
261
334
|
return true;
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
function getProcessStartTime(pid) {
|
|
265
|
-
if (process.platform === "linux") {
|
|
266
|
-
try {
|
|
267
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
268
|
-
return stat.split(" ")[21] || null;
|
|
269
|
-
} catch (_) {}
|
|
270
|
-
}
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
337
|
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
|
|
275
338
|
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
276
339
|
const lockDir = getLockDir(root);
|
|
@@ -279,7 +342,8 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
|
|
|
279
342
|
if (existsSync(lockFile)) {
|
|
280
343
|
try {
|
|
281
344
|
const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
|
|
282
|
-
const
|
|
345
|
+
const recordedStartTime = existing.processStartTime ?? existing.starttime ?? null;
|
|
346
|
+
const isAlive = isPidAlive(existing.pid, recordedStartTime);
|
|
283
347
|
const isExpired = existing.acquiredAt && Date.now() - new Date(existing.acquiredAt).getTime() > 7200000;
|
|
284
348
|
|
|
285
349
|
if (!isAlive || isExpired) {
|
|
@@ -292,12 +356,15 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
|
|
|
292
356
|
}
|
|
293
357
|
}
|
|
294
358
|
|
|
359
|
+
const startTime = getProcessStartTime(process.pid);
|
|
295
360
|
const payload = {
|
|
296
361
|
agent: agentName,
|
|
297
362
|
taskId,
|
|
298
363
|
files,
|
|
299
364
|
pid: process.pid,
|
|
300
|
-
processStartTime:
|
|
365
|
+
processStartTime: startTime,
|
|
366
|
+
starttime: startTime,
|
|
367
|
+
nonce: randomUUID(),
|
|
301
368
|
hostname: hostname(),
|
|
302
369
|
acquiredAt: new Date().toISOString(),
|
|
303
370
|
};
|
|
@@ -352,3 +419,4 @@ export function lockStatus(rootOrOpts = resolveRoot()) {
|
|
|
352
419
|
} catch (_) {}
|
|
353
420
|
return locks;
|
|
354
421
|
}
|
|
422
|
+
|