jules-orchestrator-kit 0.7.0 → 0.8.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/.env.example +3 -0
- package/README.md +2 -1
- package/bin/init.js +3 -0
- package/package.json +1 -1
- package/scripts/command-resolver.mjs +25 -7
- package/scripts/jules-queue-runner.mjs +16 -10
- package/scripts/jules-self-audit.mjs +22 -18
- package/scripts/jules-status.mjs +32 -15
- package/scripts/jules-swarm.mjs +2 -2
- package/scripts/utils.mjs +37 -17
package/.env.example
CHANGED
|
@@ -27,6 +27,9 @@ JULES_REPO=owner/repo
|
|
|
27
27
|
# Allow modifications to command-defining files (package.json, Cargo.toml, etc.) in PRs (Default: false)
|
|
28
28
|
# JULES_ALLOW_COMMAND_FILE_CHANGES=false
|
|
29
29
|
|
|
30
|
+
# Allow modifications to agent rule files (AGENTS.md, JULES_RULES_TEMPLATE.md, .agent/rules/**) in PRs (Default: false)
|
|
31
|
+
# JULES_ALLOW_AGENT_RULE_CHANGES=false
|
|
32
|
+
|
|
30
33
|
# --- Execution Scope & Git ---
|
|
31
34
|
# Base Branch for PR Audits & Merge-Base Calculations (Default: main)
|
|
32
35
|
BASE_BRANCH=main
|
package/README.md
CHANGED
|
@@ -167,7 +167,8 @@ All commands are registered in `package.json` and can be run via `npm run <comma
|
|
|
167
167
|
| `JULES_REPOLESS` | Set to `true` or `1` to run in repoless/serverless mode |
|
|
168
168
|
| `JULES_DRY_RUN` | Set to `true` or `1` to simulate dispatching without making API calls |
|
|
169
169
|
| `JULES_DAILY_BUDGET` | Daily max session budget for autonomous dispatches (Default: `300`) |
|
|
170
|
-
| `JULES_ALLOW_COMMAND_FILE_CHANGES` | Set to `true` to allow PR changes to command
|
|
170
|
+
| `JULES_ALLOW_COMMAND_FILE_CHANGES` | Set to `true` to allow PR changes to command/config files like `package.json`, `tsconfig.json`, `vite.config.ts` (Default: `false`) |
|
|
171
|
+
| `JULES_ALLOW_AGENT_RULE_CHANGES` | Set to `true` to allow PR changes to agent rule files like `AGENTS.md`, `JULES_RULES_TEMPLATE.md` (Default: `false`) |
|
|
171
172
|
| `BASE_BRANCH` | Base branch for PR Audits & Merge-Base calculations (Default: `main`) |
|
|
172
173
|
| `GITHUB_HEAD_REF` | PR Head Branch (Used dynamically by CI during OODA repair) |
|
|
173
174
|
| `JULES_PROJECT_ROOT` | Root directory of the project (Auto-assigned during swarm executions) |
|
package/bin/init.js
CHANGED
|
@@ -265,6 +265,9 @@ const requiredGitignoreEntries = [
|
|
|
265
265
|
".env",
|
|
266
266
|
".agent/history/",
|
|
267
267
|
".agent/state/",
|
|
268
|
+
".agent/jules-queue/.state/",
|
|
269
|
+
".agent/jules-queue/failed/",
|
|
270
|
+
".agent/jules-queue/.processing/",
|
|
268
271
|
".agent/jules-queue/*.md",
|
|
269
272
|
"!.agent/jules-queue/README.md"
|
|
270
273
|
];
|
package/package.json
CHANGED
|
@@ -44,6 +44,23 @@ export function resolveProjectCommands(projectRoot = process.cwd()) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function detectPackageManager(root, pkg = {}) {
|
|
48
|
+
if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
49
|
+
if (fs.existsSync(path.join(root, "yarn.lock"))) return "yarn";
|
|
50
|
+
if (fs.existsSync(path.join(root, "bun.lockb"))) return "bun";
|
|
51
|
+
if (fs.existsSync(path.join(root, "package-lock.json"))) return "npm";
|
|
52
|
+
|
|
53
|
+
const pm = pkg.packageManager;
|
|
54
|
+
if (typeof pm === "string") {
|
|
55
|
+
if (pm.startsWith("pnpm")) return "pnpm";
|
|
56
|
+
if (pm.startsWith("yarn")) return "yarn";
|
|
57
|
+
if (pm.startsWith("bun")) return "bun";
|
|
58
|
+
if (pm.startsWith("npm")) return "npm";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return "npm";
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
// 2. Dynamic Detector Strategy List
|
|
48
65
|
const detectors = [
|
|
49
66
|
{
|
|
@@ -59,20 +76,21 @@ export function resolveProjectCommands(projectRoot = process.cwd()) {
|
|
|
59
76
|
resolve: (root) => {
|
|
60
77
|
try {
|
|
61
78
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
|
|
79
|
+
const pm = detectPackageManager(root, pkg);
|
|
62
80
|
const scripts = pkg.scripts || {};
|
|
63
|
-
let verifyScript = scripts["check:all"] ?
|
|
81
|
+
let verifyScript = scripts["check:all"] ? `${pm} run check:all` : "";
|
|
64
82
|
if (!verifyScript) {
|
|
65
83
|
const parts = [];
|
|
66
|
-
if (scripts.lint) parts.push(
|
|
67
|
-
else if (scripts.check) parts.push(
|
|
68
|
-
if (scripts.test) parts.push(
|
|
84
|
+
if (scripts.lint) parts.push(`${pm} run lint`);
|
|
85
|
+
else if (scripts.check) parts.push(`${pm} run check`);
|
|
86
|
+
if (scripts.test) parts.push(`${pm} test`);
|
|
69
87
|
verifyScript = parts.join(" && ");
|
|
70
88
|
}
|
|
71
|
-
const buildScript = scripts.build ?
|
|
89
|
+
const buildScript = scripts.build ? `${pm} run build` : "";
|
|
72
90
|
return {
|
|
73
|
-
testCmd: verifyScript ||
|
|
91
|
+
testCmd: verifyScript || `${pm} test`,
|
|
74
92
|
buildCmd: buildScript,
|
|
75
|
-
source:
|
|
93
|
+
source: `package.json (${pm})`,
|
|
76
94
|
};
|
|
77
95
|
} catch (err) {
|
|
78
96
|
console.warn("⚠️ Failed to parse package.json:", err.message);
|
|
@@ -183,20 +183,26 @@ async function runQueue() {
|
|
|
183
183
|
logQueueState(file, "FAILED", error);
|
|
184
184
|
|
|
185
185
|
const failureClass = classifyQueueFailure(error);
|
|
186
|
-
const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"
|
|
186
|
+
const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"]);
|
|
187
187
|
|
|
188
188
|
const taskState = getTaskState(file);
|
|
189
|
-
const attempts = (taskState.attempts || 0) + 1;
|
|
190
189
|
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
194
|
-
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
195
|
-
log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
|
|
196
|
-
} else {
|
|
197
|
-
setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
190
|
+
if (failureClass === "budget_exhausted") {
|
|
191
|
+
setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
198
192
|
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
199
|
-
log.warn(`Task [${title}]
|
|
193
|
+
log.warn(`Task [${title}] deferred due to daily budget limit. Left in queue for next budget reset.`);
|
|
194
|
+
} else {
|
|
195
|
+
const attempts = (taskState.attempts || 0) + 1;
|
|
196
|
+
if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
|
|
197
|
+
const destPath = path.join(failedDir, file);
|
|
198
|
+
setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
199
|
+
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
200
|
+
log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
|
|
201
|
+
} else {
|
|
202
|
+
setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
203
|
+
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
204
|
+
log.warn(`Task [${title}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
|
|
205
|
+
}
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
})
|
|
@@ -208,6 +208,22 @@ export function parseAndCleanStderr(stderrStr) {
|
|
|
208
208
|
return lines.slice(-100).join("\n");
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
export function getOodaStateFile(mergeBase = "main") {
|
|
212
|
+
const oodaDir = path.resolve(process.cwd(), ".agent/state/ooda");
|
|
213
|
+
if (!fs.existsSync(oodaDir)) fs.mkdirSync(oodaDir, { recursive: true });
|
|
214
|
+
|
|
215
|
+
const keyInput = [
|
|
216
|
+
process.env.GITHUB_REPOSITORY || "",
|
|
217
|
+
process.env.GITHUB_REF || "",
|
|
218
|
+
process.env.GITHUB_HEAD_REF || "",
|
|
219
|
+
mergeBase,
|
|
220
|
+
process.cwd()
|
|
221
|
+
].join("\u0000");
|
|
222
|
+
|
|
223
|
+
const key = crypto.createHash("sha256").update(keyInput).digest("hex").slice(0, 16);
|
|
224
|
+
return path.join(oodaDir, `${key}.json`);
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
export function logAuditMetrics(metrics) {
|
|
212
228
|
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
213
229
|
if (!fs.existsSync(historyDir)) {
|
|
@@ -275,7 +291,9 @@ export function runSelfAudit() {
|
|
|
275
291
|
|
|
276
292
|
if (commandFileChanges.length > 0 && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "true" && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "1") {
|
|
277
293
|
const err = new Error(
|
|
278
|
-
"COMMAND FILE CHANGE DETECTED: " + commandFileChanges.join(", ") + ".
|
|
294
|
+
"COMMAND / BUILD CONFIG FILE CHANGE DETECTED: [" + commandFileChanges.join(", ") + "]. " +
|
|
295
|
+
"Refusing to execute verification scripts from untrusted branch. " +
|
|
296
|
+
"To override for legitimate dependency updates, set JULES_ALLOW_COMMAND_FILE_CHANGES=true."
|
|
279
297
|
);
|
|
280
298
|
err.code = 3;
|
|
281
299
|
throw err;
|
|
@@ -287,7 +305,9 @@ export function runSelfAudit() {
|
|
|
287
305
|
|
|
288
306
|
if (agentRuleChanges.length > 0 && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "true" && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "1") {
|
|
289
307
|
const err = new Error(
|
|
290
|
-
"AGENT RULE FILE CHANGE DETECTED: " + agentRuleChanges.join(", ") + ".
|
|
308
|
+
"AGENT RULE FILE CHANGE DETECTED: [" + agentRuleChanges.join(", ") + "]. " +
|
|
309
|
+
"Refusing to load agent rules from untrusted branch. " +
|
|
310
|
+
"To override for legitimate rule updates, set JULES_ALLOW_AGENT_RULE_CHANGES=true."
|
|
291
311
|
);
|
|
292
312
|
err.code = 3;
|
|
293
313
|
throw err;
|
|
@@ -442,22 +462,6 @@ export function runSelfAudit() {
|
|
|
442
462
|
throw err;
|
|
443
463
|
}
|
|
444
464
|
|
|
445
|
-
function getOodaStateFile(mergeBase = "main") {
|
|
446
|
-
const oodaDir = path.resolve(process.cwd(), ".agent/state/ooda");
|
|
447
|
-
if (!fs.existsSync(oodaDir)) fs.mkdirSync(oodaDir, { recursive: true });
|
|
448
|
-
|
|
449
|
-
const keyInput = [
|
|
450
|
-
process.env.GITHUB_REPOSITORY || "",
|
|
451
|
-
process.env.GITHUB_REF || "",
|
|
452
|
-
process.env.GITHUB_HEAD_REF || "",
|
|
453
|
-
mergeBase,
|
|
454
|
-
process.cwd()
|
|
455
|
-
].join("\u0000");
|
|
456
|
-
|
|
457
|
-
const key = crypto.createHash("sha256").update(keyInput).digest("hex").slice(0, 16);
|
|
458
|
-
return path.join(oodaDir, `${key}.json`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
465
|
let oodaRetries = 0;
|
|
462
466
|
const oodaStateFile = getOodaStateFile(mergeBase);
|
|
463
467
|
try {
|
package/scripts/jules-status.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Jules Task Status Utility
|
|
4
|
-
* Parses the queue.jsonl file and renders a clean console.table
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import fs from "node:fs";
|
|
8
2
|
import path from "node:path";
|
|
9
|
-
import { log } from "./utils.mjs";
|
|
3
|
+
import { log, checkDailyBudget } from "./utils.mjs";
|
|
10
4
|
|
|
5
|
+
const isJson = process.argv.includes("--json");
|
|
11
6
|
const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
|
|
12
7
|
|
|
8
|
+
const budgetInfo = checkDailyBudget();
|
|
9
|
+
|
|
13
10
|
if (!fs.existsSync(queueLogPath)) {
|
|
14
|
-
|
|
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
|
+
}
|
|
15
16
|
process.exit(0);
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -20,7 +21,11 @@ try {
|
|
|
20
21
|
const lines = content.split("\n").filter(Boolean);
|
|
21
22
|
|
|
22
23
|
if (lines.length === 0) {
|
|
23
|
-
|
|
24
|
+
if (isJson) {
|
|
25
|
+
console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
log.info("Queue log is empty.");
|
|
28
|
+
}
|
|
24
29
|
process.exit(0);
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -32,10 +37,11 @@ try {
|
|
|
32
37
|
const entry = JSON.parse(line);
|
|
33
38
|
if (entry.file) {
|
|
34
39
|
statusMap.set(entry.file, {
|
|
40
|
+
file: entry.file,
|
|
35
41
|
Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
|
|
36
42
|
Status: entry.status,
|
|
37
|
-
Timestamp:
|
|
38
|
-
Error: entry.error ||
|
|
43
|
+
Timestamp: entry.timestamp,
|
|
44
|
+
Error: entry.error || null
|
|
39
45
|
});
|
|
40
46
|
}
|
|
41
47
|
} catch (e) {
|
|
@@ -43,15 +49,26 @@ try {
|
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
const tasksList = Array.from(statusMap.values());
|
|
53
|
+
|
|
54
|
+
if (isJson) {
|
|
55
|
+
console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tasksList.length === 0) {
|
|
47
60
|
log.info("No parseable task events found in queue.jsonl");
|
|
48
61
|
process.exit(0);
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
log.header(
|
|
52
|
-
console.table(
|
|
64
|
+
log.header(`JULES QUEUE STATUS (Budget: ${budgetInfo.used}/${budgetInfo.budget} sessions used)`);
|
|
65
|
+
console.table(tasksList.map(t => ({ Task: t.Task, Status: t.Status, Timestamp: new Date(t.Timestamp).toLocaleString(), Error: t.Error || "-" })));
|
|
53
66
|
|
|
54
67
|
} catch (err) {
|
|
55
|
-
|
|
68
|
+
if (isJson) {
|
|
69
|
+
console.log(JSON.stringify({ error: err.message }, null, 2));
|
|
70
|
+
} else {
|
|
71
|
+
log.error(`Failed to read queue log: ${err.message}`);
|
|
72
|
+
}
|
|
56
73
|
process.exit(1);
|
|
57
74
|
}
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -194,8 +194,8 @@ async function runSwarm() {
|
|
|
194
194
|
env: {
|
|
195
195
|
...process.env,
|
|
196
196
|
JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
|
|
197
|
-
JULES_SLOT_INDEX: String(
|
|
198
|
-
JULES_SLOT_TOTAL: String(
|
|
197
|
+
JULES_SLOT_INDEX: String(taskNum),
|
|
198
|
+
JULES_SLOT_TOTAL: String(tasks.length)
|
|
199
199
|
},
|
|
200
200
|
});
|
|
201
201
|
if (stdout) log.dim(stdout.trim());
|
package/scripts/utils.mjs
CHANGED
|
@@ -165,10 +165,34 @@ export function redactSecrets(text) {
|
|
|
165
165
|
return sanitized;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
export function
|
|
169
|
-
|
|
168
|
+
export function pruneOldLedgers(stateDir, retentionDays = 30) {
|
|
169
|
+
try {
|
|
170
|
+
if (!fs.existsSync(stateDir)) return;
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const maxAgeMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
173
|
+
const files = fs.readdirSync(stateDir);
|
|
174
|
+
files.forEach((file) => {
|
|
175
|
+
if (file.endsWith(".jsonl")) {
|
|
176
|
+
const filePath = path.join(stateDir, file);
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
179
|
+
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getDailyLedgerPath(ledgerName = "sessions") {
|
|
187
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
188
|
+
const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state", ledgerName);
|
|
170
189
|
ensureDir(stateDir);
|
|
171
|
-
|
|
190
|
+
pruneOldLedgers(stateDir, 30);
|
|
191
|
+
return path.join(stateDir, `${dateStr}.jsonl`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function appendLedger(ledgerName, payload) {
|
|
195
|
+
const ledgerPath = getDailyLedgerPath(ledgerName);
|
|
172
196
|
const entry = JSON.stringify({
|
|
173
197
|
timestamp: new Date().toISOString(),
|
|
174
198
|
...payload
|
|
@@ -178,34 +202,30 @@ export function appendLedger(ledgerName, payload) {
|
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
export function checkDailyBudget(maxSessions = 300) {
|
|
181
|
-
const
|
|
182
|
-
const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state");
|
|
183
|
-
const sessionsLedger = path.join(stateDir, "sessions.jsonl");
|
|
205
|
+
const ledgerPath = getDailyLedgerPath("sessions");
|
|
184
206
|
|
|
185
|
-
if (!fs.existsSync(
|
|
207
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
186
208
|
return { ok: true, used: 0, budget: maxSessions };
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
const content = fs.readFileSync(
|
|
211
|
+
const content = fs.readFileSync(ledgerPath, "utf-8");
|
|
190
212
|
const lines = content.split("\n").filter(Boolean);
|
|
191
213
|
|
|
192
214
|
let usedToday = 0;
|
|
193
215
|
for (const line of lines) {
|
|
194
216
|
try {
|
|
195
217
|
const entry = JSON.parse(line);
|
|
196
|
-
if (
|
|
197
|
-
entry.timestamp &&
|
|
198
|
-
entry.timestamp.startsWith(dateStr) &&
|
|
199
|
-
(entry.event === "budget_reserved" || !entry.event)
|
|
200
|
-
) {
|
|
218
|
+
if (entry.event === "budget_reserved" || !entry.event) {
|
|
201
219
|
usedToday++;
|
|
202
220
|
}
|
|
203
|
-
} catch (
|
|
204
|
-
// Ignore
|
|
205
|
-
}
|
|
221
|
+
} catch (_) {}
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
return {
|
|
224
|
+
return {
|
|
225
|
+
ok: usedToday < maxSessions,
|
|
226
|
+
used: usedToday,
|
|
227
|
+
budget: maxSessions
|
|
228
|
+
};
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
function sleepSync(ms) {
|