jules-orchestrator-kit 0.23.0 ā 0.24.0
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/bin/agentctl.mjs +3 -3
- package/bin/init.js +2 -3
- package/index.mjs +3 -1
- package/package.json +3 -4
- package/scripts/jules-queue-runner.mjs +3 -22
- package/scripts/utils.mjs +65 -0
- package/src/engine.mjs +93 -17
- package/src/envelope.mjs +3 -3
- package/src/flaky-ledger.mjs +0 -2
- package/src/git.mjs +1 -6
- package/src/merge-verify.mjs +7 -7
- package/src/state.mjs +4 -2
- package/src/telemetry.mjs +55 -19
- package/scripts/jules-cleanup.mjs +0 -49
- package/scripts/jules-swarm.mjs +0 -38
- package/scripts/lock-manager.mjs +0 -22
package/bin/agentctl.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const command = args[0];
|
|
|
14
14
|
|
|
15
15
|
function printHelp() {
|
|
16
16
|
console.log(`
|
|
17
|
-
š agentctl v0.
|
|
17
|
+
š agentctl v0.24.0 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
18
18
|
|
|
19
19
|
Usage: agentctl <command> [options]
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ async function main() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
47
|
-
console.log("agentctl v0.
|
|
47
|
+
console.log("agentctl v0.24.0");
|
|
48
48
|
process.exit(0);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -226,7 +226,7 @@ async function main() {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
case "doctor": {
|
|
229
|
-
console.log(`\nš agentctl System Diagnostics (v0.
|
|
229
|
+
console.log(`\nš agentctl System Diagnostics (v0.24.0)`);
|
|
230
230
|
console.log(`--------------------------------------------------`);
|
|
231
231
|
console.log(` Project Root : ${root}`);
|
|
232
232
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/bin/init.js
CHANGED
|
@@ -248,13 +248,12 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
248
248
|
let updated = false;
|
|
249
249
|
const julesScripts = {
|
|
250
250
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
251
|
-
"jules:queue": "node
|
|
251
|
+
"jules:queue": "node bin/agentctl.mjs queue",
|
|
252
252
|
"jules:create": "node scripts/jules-create.mjs",
|
|
253
253
|
"jules:status": "node scripts/jules-status.mjs",
|
|
254
254
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
255
|
-
"jules:cleanup": "node scripts/jules-cleanup.mjs",
|
|
256
255
|
"jules:scan": "node scripts/jules-scan-todos.mjs",
|
|
257
|
-
"jules:swarm": "node
|
|
256
|
+
"jules:swarm": "node bin/agentctl.mjs swarm",
|
|
258
257
|
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
259
258
|
};
|
|
260
259
|
|
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google Jules Orchestrator Kit - Node.js SDK (v0.
|
|
2
|
+
* Google Jules Orchestrator Kit - Node.js SDK (v0.24.0)
|
|
3
3
|
*
|
|
4
4
|
* Exposes core orchestrator functions for programmatically driving agent tasks,
|
|
5
5
|
* security auditing, repo gating, and state operations.
|
|
@@ -58,3 +58,5 @@ export { runSelfAudit, runPreflightSandbox } from "./scripts/jules-self-audit.mj
|
|
|
58
58
|
export { scanCodebaseForTodos, runScanner } from "./scripts/jules-scan-todos.mjs";
|
|
59
59
|
export { getDynamicGuardrails, dispatchTask } from "./scripts/jules-dispatch.mjs";
|
|
60
60
|
export { classifyQueueFailure } from "./scripts/jules-queue-runner.mjs";
|
|
61
|
+
export { extractPrUrls, auditSessions, buildSyncManifest, pushReservationManifest } from "./scripts/utils.mjs";
|
|
62
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Orchestration kit for running Google Jules autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,13 +42,12 @@
|
|
|
42
42
|
"init": "node bin/init.js",
|
|
43
43
|
"test": "node --test test/*.test.mjs",
|
|
44
44
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
45
|
-
"jules:queue": "node
|
|
45
|
+
"jules:queue": "node bin/agentctl.mjs queue",
|
|
46
46
|
"jules:create": "node scripts/jules-create.mjs",
|
|
47
47
|
"jules:status": "node scripts/jules-status.mjs",
|
|
48
48
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
49
|
-
"jules:cleanup": "node scripts/jules-cleanup.mjs",
|
|
50
49
|
"jules:scan": "node scripts/jules-scan-todos.mjs",
|
|
51
|
-
"jules:swarm": "node
|
|
50
|
+
"jules:swarm": "node bin/agentctl.mjs swarm",
|
|
52
51
|
"jules:merge-swarm": "node scripts/jules-merge-swarm.mjs",
|
|
53
52
|
"jules:patch": "node scripts/jules-patch.mjs",
|
|
54
53
|
"jules:nightly": "node scripts/jules-nightly.mjs",
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
* Backward compatibility shim for jules-queue-runner.mjs in v0.9.0.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { isTaskFile } from "../src/engine.mjs";
|
|
9
|
-
import { readdirSync, renameSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
7
|
+
import { run } from "../src/engine.mjs";
|
|
11
8
|
|
|
12
9
|
export function classifyQueueFailure(err) {
|
|
13
10
|
const msg = String(err?.message || err || "");
|
|
@@ -19,25 +16,9 @@ export function classifyQueueFailure(err) {
|
|
|
19
16
|
|
|
20
17
|
if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
|
|
21
18
|
const root = process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
22
|
-
const
|
|
23
|
-
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
24
|
-
|
|
25
|
-
if (files.length === 0) {
|
|
26
|
-
console.log("Queue processing complete!");
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const completedDir = join(queueDir, "completed");
|
|
31
|
-
ensureDir(completedDir);
|
|
32
|
-
|
|
33
|
-
for (const file of files) {
|
|
34
|
-
const src = join(queueDir, file);
|
|
35
|
-
const dst = join(completedDir, file);
|
|
36
|
-
try {
|
|
37
|
-
renameSync(src, dst);
|
|
38
|
-
} catch (_) {}
|
|
39
|
-
}
|
|
19
|
+
const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
40
20
|
|
|
21
|
+
await run({ root, dryRun: isDry });
|
|
41
22
|
console.log("Queue processing complete!");
|
|
42
23
|
process.exit(0);
|
|
43
24
|
}
|
package/scripts/utils.mjs
CHANGED
|
@@ -159,3 +159,68 @@ export function acquireBudgetLock(_root) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export function releaseBudgetLock(_root) {}
|
|
162
|
+
|
|
163
|
+
export function extractPrUrls(outputs = []) {
|
|
164
|
+
const prs = [];
|
|
165
|
+
const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
|
|
166
|
+
for (const item of outputs) {
|
|
167
|
+
const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
|
|
168
|
+
const matches = text.match(regex);
|
|
169
|
+
if (matches) {
|
|
170
|
+
for (const m of matches) {
|
|
171
|
+
if (!prs.includes(m)) prs.push(m);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return prs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function auditSessions(sessions = [], opts = {}) {
|
|
179
|
+
const staleHoursThreshold = opts.staleHoursThreshold || 24;
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const merged = [];
|
|
182
|
+
const active = [];
|
|
183
|
+
const stale = [];
|
|
184
|
+
|
|
185
|
+
for (const s of sessions) {
|
|
186
|
+
if (s.state === "MERGED") {
|
|
187
|
+
merged.push(s);
|
|
188
|
+
} else {
|
|
189
|
+
const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
|
|
190
|
+
const ageHours = (now - updatedMs) / (1000 * 60 * 60);
|
|
191
|
+
if (ageHours > staleHoursThreshold) {
|
|
192
|
+
stale.push(s);
|
|
193
|
+
} else {
|
|
194
|
+
active.push(s);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { merged, active, stale };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildSyncManifest(tasks = []) {
|
|
203
|
+
const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
totalTasks: tasks.length,
|
|
207
|
+
reservations,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
|
|
212
|
+
const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
213
|
+
const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
|
|
214
|
+
|
|
215
|
+
const agentDir = join(projectRoot, ".agent");
|
|
216
|
+
if (!existsSync(agentDir)) {
|
|
217
|
+
try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
|
|
218
|
+
}
|
|
219
|
+
const manifestPath = join(agentDir, "sync-manifest.json");
|
|
220
|
+
try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
|
|
221
|
+
|
|
222
|
+
if (isDry) return { status: "DRY_RUN", path: manifestPath };
|
|
223
|
+
if (isRemote) return { status: "PUSHED", path: manifestPath };
|
|
224
|
+
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
225
|
+
}
|
|
226
|
+
|
package/src/engine.mjs
CHANGED
|
@@ -354,6 +354,16 @@ export async function repair(failure, opts = {}) {
|
|
|
354
354
|
attempts.push({ n, session, ok: true });
|
|
355
355
|
appendTelemetry(root, "ooda_repair_attempt", { attempt: n, ok: true });
|
|
356
356
|
|
|
357
|
+
// Poll async provider for terminal session state before executing re-verification gates
|
|
358
|
+
if (provider && session) {
|
|
359
|
+
await pollSessionState(provider, session, {
|
|
360
|
+
root,
|
|
361
|
+
dryRun: opts.dryRun,
|
|
362
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
363
|
+
maxPollAttempts: opts.maxPollAttempts,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
357
367
|
// Re-verify after repair attempt
|
|
358
368
|
const gateRes = await gate({ root, config, fix: false, progressBus, progressToken });
|
|
359
369
|
if (gateRes.ok) {
|
|
@@ -379,6 +389,50 @@ export async function repair(failure, opts = {}) {
|
|
|
379
389
|
return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
|
|
380
390
|
}
|
|
381
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Polls an async provider for terminal session state (COMPLETED / FAILED) before re-verification.
|
|
394
|
+
*/
|
|
395
|
+
export async function pollSessionState(provider, session, opts = {}) {
|
|
396
|
+
if (!session || !session.id) return { status: "COMPLETED" };
|
|
397
|
+
const initialStatus = String(session.status || session.state || "").toUpperCase();
|
|
398
|
+
if (initialStatus === "COMPLETED" || initialStatus === "FAILED" || opts.dryRun || session.id === "dry-run-session-id") {
|
|
399
|
+
return { status: initialStatus || "COMPLETED" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const maxAttempts = opts.maxPollAttempts || 30;
|
|
403
|
+
const pollIntervalMs = opts.pollIntervalMs || 1000;
|
|
404
|
+
const timeoutMs = opts.pollTimeoutMs || 300000;
|
|
405
|
+
const startTime = Date.now();
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
408
|
+
if (Date.now() - startTime > timeoutMs) break;
|
|
409
|
+
|
|
410
|
+
let currentSession = null;
|
|
411
|
+
if (provider && typeof provider.getSession === "function") {
|
|
412
|
+
try {
|
|
413
|
+
currentSession = await provider.getSession(session.id, opts);
|
|
414
|
+
} catch (_) {}
|
|
415
|
+
} else if (typeof opts.pollFn === "function") {
|
|
416
|
+
try {
|
|
417
|
+
currentSession = await opts.pollFn(session.id);
|
|
418
|
+
} catch (_) {}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (currentSession) {
|
|
422
|
+
const status = String(currentSession.status || currentSession.state || "").toUpperCase();
|
|
423
|
+
if (status === "COMPLETED" || status === "FAILED") {
|
|
424
|
+
return { ...currentSession, status };
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { ...session, status: String(session.status || "COMPLETED").toUpperCase() };
|
|
434
|
+
}
|
|
435
|
+
|
|
382
436
|
function buildRepairPrompt(failure, attempt, _config) {
|
|
383
437
|
const cleanStderr = redactSecrets(failure.stderr || failure.stdout || "Unknown Error");
|
|
384
438
|
|
|
@@ -435,32 +489,54 @@ export async function dispatch(task, opts = {}) {
|
|
|
435
489
|
);
|
|
436
490
|
}
|
|
437
491
|
|
|
438
|
-
export async function run(opts = {}) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
492
|
+
export async function run(tasksOrOpts = {}, opts = {}) {
|
|
493
|
+
let tasks = null;
|
|
494
|
+
let options = opts;
|
|
495
|
+
if (Array.isArray(tasksOrOpts)) {
|
|
496
|
+
tasks = tasksOrOpts;
|
|
497
|
+
options = opts || {};
|
|
498
|
+
} else if (tasksOrOpts && typeof tasksOrOpts === "object") {
|
|
499
|
+
options = tasksOrOpts;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const root = options.root || process.cwd();
|
|
503
|
+
const config = options.config || loadConfig(root);
|
|
504
|
+
const concurrency = options.concurrency || config.limits.concurrency || 1;
|
|
505
|
+
const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
|
|
442
506
|
const queueDir = getQueueDir(root);
|
|
443
507
|
const completedDir = join(queueDir, "completed");
|
|
444
508
|
ensureDir(completedDir);
|
|
445
509
|
|
|
446
|
-
|
|
510
|
+
let filesToProcess = [];
|
|
511
|
+
if (Array.isArray(tasks)) {
|
|
512
|
+
filesToProcess = tasks.map((t) => (typeof t === "string" ? t : t.id || t.file || t.title));
|
|
513
|
+
} else {
|
|
514
|
+
filesToProcess = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
515
|
+
}
|
|
516
|
+
|
|
447
517
|
const results = [];
|
|
448
518
|
|
|
449
|
-
for (let i = 0; i <
|
|
450
|
-
const batch =
|
|
519
|
+
for (let i = 0; i < filesToProcess.length; i += concurrency) {
|
|
520
|
+
const batch = filesToProcess.slice(i, i + concurrency);
|
|
451
521
|
await Promise.all(
|
|
452
|
-
batch.map(async (
|
|
453
|
-
const
|
|
522
|
+
batch.map(async (fileOrItem) => {
|
|
523
|
+
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem.id || fileOrItem.file;
|
|
524
|
+
const srcPath = join(queueDir, fileName);
|
|
454
525
|
try {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
526
|
+
let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
|
|
527
|
+
if (!prompt && existsSync(srcPath)) {
|
|
528
|
+
prompt = readFileSync(srcPath, "utf-8");
|
|
529
|
+
}
|
|
530
|
+
const task = { title: fileName, prompt };
|
|
531
|
+
const session = await dispatch(task, { root, config, dryRun: isDry });
|
|
532
|
+
const dstPath = join(completedDir, fileName);
|
|
533
|
+
if (existsSync(srcPath)) {
|
|
534
|
+
renameSync(srcPath, dstPath);
|
|
535
|
+
}
|
|
536
|
+
appendLedger({ event: "task_completed", file: fileName, session }, root);
|
|
537
|
+
results.push({ file: fileName, ok: true, session });
|
|
462
538
|
} catch (err) {
|
|
463
|
-
results.push({ file, ok: false, error: err.message });
|
|
539
|
+
results.push({ file: fileName, ok: false, error: err.message });
|
|
464
540
|
}
|
|
465
541
|
})
|
|
466
542
|
);
|
package/src/envelope.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { git, resolveBase } from "./git.mjs";
|
|
1
|
+
import { git, runCmd, resolveBase } from "./git.mjs";
|
|
2
2
|
import { checkScope } from "./security.mjs";
|
|
3
3
|
import { normalizeScope } from "./config.mjs";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -70,8 +70,8 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
70
70
|
const absPath = join(root, relPath);
|
|
71
71
|
let existsInGit = false;
|
|
72
72
|
if (resolvedBase) {
|
|
73
|
-
const catRes =
|
|
74
|
-
existsInGit =
|
|
73
|
+
const catRes = runCmd(["git", "cat-file", "-e", `${resolvedBase}:${relPath}`], { cwd: root, ignoreError: true });
|
|
74
|
+
existsInGit = catRes.status === 0;
|
|
75
75
|
}
|
|
76
76
|
const existsOnDisk = existsSync(absPath);
|
|
77
77
|
if (!existsInGit && !existsOnDisk) {
|
package/src/flaky-ledger.mjs
CHANGED
package/src/git.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizePath } from "./config.mjs";
|
|
3
3
|
import { journalIntent, journalDone } from "./journal.mjs";
|
|
4
4
|
|
|
5
5
|
export const NET_GUARD_PRELOAD_URL = new URL("./preload-net-guard.mjs", import.meta.url).href;
|
|
@@ -13,11 +13,6 @@ export class GateError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function normalizePath(p) {
|
|
17
|
-
if (!p || typeof p !== "string") return "";
|
|
18
|
-
return p.split(sep).join("/").replace(/\\/g, "/");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
22
17
|
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
23
18
|
|
package/src/merge-verify.mjs
CHANGED
|
@@ -31,14 +31,14 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
|
|
|
31
31
|
tool = "python3 -m py_compile";
|
|
32
32
|
} else if (ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts") {
|
|
33
33
|
const hasTsConfig = existsSync(join(root, "tsconfig.json"));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const localTsc = join(root, "node_modules", ".bin", "tsc");
|
|
35
|
+
const hasLocalTsc = existsSync(localTsc);
|
|
36
|
+
if (hasTsConfig && hasLocalTsc) {
|
|
37
|
+
cmd = localTsc;
|
|
38
|
+
args = ["--noEmit", tempFilePath];
|
|
37
39
|
tool = "tsc --noEmit";
|
|
38
40
|
} else {
|
|
39
|
-
|
|
40
|
-
args = ["--check", tempFilePath];
|
|
41
|
-
tool = "node --check";
|
|
41
|
+
return { ok: true, tool: "ts-skipped-no-tsconfig" };
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
44
44
|
// JS, MJS, CJS or default
|
|
@@ -47,7 +47,7 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
|
|
|
47
47
|
tool = "node --check";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
50
|
+
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 120000 });
|
|
51
51
|
const ok = res.status === 0;
|
|
52
52
|
|
|
53
53
|
return {
|
package/src/state.mjs
CHANGED
|
@@ -349,8 +349,9 @@ export function isPidAlive(pid, expectedStartTime = null) {
|
|
|
349
349
|
return true;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
|
|
353
|
-
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
352
|
+
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot(), opts = {}) {
|
|
353
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : (rootOrOpts?.root || resolveRoot());
|
|
354
|
+
const branch = (typeof rootOrOpts === "object" && rootOrOpts?.branch) || opts?.branch || process.env.JULES_BRANCH || process.env.BRANCH_NAME || "";
|
|
354
355
|
const lockDir = getLockDir(root);
|
|
355
356
|
const lockFile = join(lockDir, `${taskId}.json`);
|
|
356
357
|
|
|
@@ -375,6 +376,7 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
|
|
|
375
376
|
const payload = {
|
|
376
377
|
agent: agentName,
|
|
377
378
|
taskId,
|
|
379
|
+
branch,
|
|
378
380
|
files,
|
|
379
381
|
pid: process.pid,
|
|
380
382
|
processStartTime: startTime,
|
package/src/telemetry.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openSync,
|
|
3
3
|
writeSync,
|
|
4
|
+
readSync,
|
|
4
5
|
closeSync,
|
|
5
6
|
readFileSync,
|
|
6
7
|
existsSync,
|
|
@@ -16,6 +17,37 @@ import { safeAtomicWrite } from "./security.mjs";
|
|
|
16
17
|
export const MAX_TELEMETRY_SEGMENT_BYTES = 8 * 1024 * 1024; // 8 MB log segment ceiling
|
|
17
18
|
export const TELEMETRY_GENESIS_HASH = "0".repeat(64);
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Reads up to 64 KB from EOF of target log segment to extract true tail hash.
|
|
22
|
+
*/
|
|
23
|
+
function readTailHash(filePath, bufferSize = 64 * 1024) {
|
|
24
|
+
if (!existsSync(filePath)) return null;
|
|
25
|
+
try {
|
|
26
|
+
const stat = statSync(filePath);
|
|
27
|
+
if (stat.size === 0) return null;
|
|
28
|
+
const readSize = Math.min(stat.size, bufferSize);
|
|
29
|
+
const position = stat.size - readSize;
|
|
30
|
+
const fd = openSync(filePath, "r");
|
|
31
|
+
const buf = Buffer.alloc(readSize);
|
|
32
|
+
try {
|
|
33
|
+
readSync(fd, buf, 0, readSize, position);
|
|
34
|
+
} finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
const content = buf.toString("utf-8");
|
|
38
|
+
const lines = content.split("\n").filter(Boolean);
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(lines[i]);
|
|
42
|
+
if (obj && typeof obj.hash === "string") {
|
|
43
|
+
return obj.hash;
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
/**
|
|
20
52
|
* Returns telemetry file paths for a given date string sorted by segment index.
|
|
21
53
|
*/
|
|
@@ -50,18 +82,10 @@ function coldScanTelemetry(stateDir, dateStr) {
|
|
|
50
82
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
51
83
|
const seg = segments[i];
|
|
52
84
|
if (existsSync(seg.path)) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const entry = JSON.parse(lines[j]);
|
|
59
|
-
if (entry && typeof entry.hash === "string") {
|
|
60
|
-
return { hash: entry.hash, segment: seg.index };
|
|
61
|
-
}
|
|
62
|
-
} catch (_) {}
|
|
63
|
-
}
|
|
64
|
-
} catch (_) {}
|
|
85
|
+
const tailHash = readTailHash(seg.path);
|
|
86
|
+
if (tailHash) {
|
|
87
|
+
return { hash: tailHash, segment: seg.index };
|
|
88
|
+
}
|
|
65
89
|
}
|
|
66
90
|
}
|
|
67
91
|
|
|
@@ -70,7 +94,7 @@ function coldScanTelemetry(stateDir, dateStr) {
|
|
|
70
94
|
|
|
71
95
|
/**
|
|
72
96
|
* Appends a structured telemetry event to .agent/state/telemetry-<date>.jsonl with SHA-256 hash chaining.
|
|
73
|
-
* Uses .head atomic cache for O(1) constant-time appends.
|
|
97
|
+
* Uses .head atomic cache with true tail reconciliation for O(1) constant-time appends.
|
|
74
98
|
*
|
|
75
99
|
* @param {string} [rootOrOpts]
|
|
76
100
|
* @param {string} kind
|
|
@@ -102,18 +126,30 @@ export function appendTelemetry(rootOrOpts = resolveRoot(), kind = "event", fiel
|
|
|
102
126
|
} catch (_) {}
|
|
103
127
|
}
|
|
104
128
|
|
|
105
|
-
if (!headValid) {
|
|
106
|
-
const recovered = coldScanTelemetry(stateDir, dateStr);
|
|
107
|
-
prevHash = recovered.hash;
|
|
108
|
-
activeSegmentIndex = recovered.segment;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
129
|
let activeFileName =
|
|
112
130
|
activeSegmentIndex === 0
|
|
113
131
|
? `telemetry-${dateStr}.jsonl`
|
|
114
132
|
: `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
|
|
115
133
|
let activeFilePath = join(stateDir, activeFileName);
|
|
116
134
|
|
|
135
|
+
// Reconcile .head hash against true log tail (reading last 64 KB from EOF)
|
|
136
|
+
const tailHash = readTailHash(activeFilePath);
|
|
137
|
+
if (tailHash) {
|
|
138
|
+
if (tailHash !== prevHash) {
|
|
139
|
+
prevHash = tailHash;
|
|
140
|
+
}
|
|
141
|
+
headValid = true;
|
|
142
|
+
} else if (!headValid) {
|
|
143
|
+
const recovered = coldScanTelemetry(stateDir, dateStr);
|
|
144
|
+
prevHash = recovered.hash;
|
|
145
|
+
activeSegmentIndex = recovered.segment;
|
|
146
|
+
activeFileName =
|
|
147
|
+
activeSegmentIndex === 0
|
|
148
|
+
? `telemetry-${dateStr}.jsonl`
|
|
149
|
+
: `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
|
|
150
|
+
activeFilePath = join(stateDir, activeFileName);
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
if (existsSync(activeFilePath)) {
|
|
118
154
|
try {
|
|
119
155
|
const stat = statSync(activeFilePath);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Backward compatibility shim for jules-cleanup.mjs in v0.9.0.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function extractPrUrls(outputs = []) {
|
|
8
|
-
const prs = [];
|
|
9
|
-
const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
|
|
10
|
-
for (const item of outputs) {
|
|
11
|
-
const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
|
|
12
|
-
const matches = text.match(regex);
|
|
13
|
-
if (matches) {
|
|
14
|
-
for (const m of matches) {
|
|
15
|
-
if (!prs.includes(m)) prs.push(m);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return prs;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function auditSessions(sessions = [], opts = {}) {
|
|
23
|
-
const staleHoursThreshold = opts.staleHoursThreshold || 24;
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
const merged = [];
|
|
26
|
-
const active = [];
|
|
27
|
-
const stale = [];
|
|
28
|
-
|
|
29
|
-
for (const s of sessions) {
|
|
30
|
-
if (s.state === "MERGED") {
|
|
31
|
-
merged.push(s);
|
|
32
|
-
} else {
|
|
33
|
-
const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
|
|
34
|
-
const ageHours = (now - updatedMs) / (1000 * 60 * 60);
|
|
35
|
-
if (ageHours > staleHoursThreshold) {
|
|
36
|
-
stale.push(s);
|
|
37
|
-
} else {
|
|
38
|
-
active.push(s);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { merged, active, stale };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (process.argv[1] && process.argv[1].endsWith("jules-cleanup.mjs")) {
|
|
47
|
-
console.log("[Shim] Cleanup audit completed.");
|
|
48
|
-
process.exit(0);
|
|
49
|
-
}
|
package/scripts/jules-swarm.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Backward compatibility shim for jules-swarm.mjs in v0.9.0.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
|
|
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
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
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";
|
|
22
|
-
|
|
23
|
-
const agentDir = join(projectRoot, ".agent");
|
|
24
|
-
if (!existsSync(agentDir)) {
|
|
25
|
-
try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
|
|
26
|
-
}
|
|
27
|
-
const manifestPath = join(agentDir, "sync-manifest.json");
|
|
28
|
-
try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
|
|
29
|
-
|
|
30
|
-
if (isDry) return { status: "DRY_RUN", path: manifestPath };
|
|
31
|
-
if (isRemote) return { status: "PUSHED", path: manifestPath };
|
|
32
|
-
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
33
|
-
}
|
|
34
|
-
|
|
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);
|
|
38
|
-
}
|
package/scripts/lock-manager.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backward compatibility shim for lock-manager.mjs in v0.9.0.
|
|
3
|
-
* Delegates to src/state.mjs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { acquireLock, releaseLock, lockStatus } from "../src/state.mjs";
|
|
7
|
-
|
|
8
|
-
export function acquire(agent, taskId, filePaths = [], opts = {}) {
|
|
9
|
-
return acquireLock(agent, taskId, filePaths, opts);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function release(agent, taskId, opts = {}) {
|
|
13
|
-
return releaseLock(agent, taskId, opts);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function status(opts = {}) {
|
|
17
|
-
return lockStatus(opts);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function cleanup() {
|
|
21
|
-
return { removed: 0 };
|
|
22
|
-
}
|