jules-orchestrator-kit 0.8.5 → 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 (41) 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/JULES_RULES_TEMPLATE.md +6 -1
  7. package/README.md +25 -13
  8. package/bin/agentctl.mjs +274 -0
  9. package/index.mjs +32 -5
  10. package/package.json +11 -2
  11. package/scripts/asset-integrity-check.mjs +35 -0
  12. package/scripts/command-resolver.mjs +37 -214
  13. package/scripts/jules-cleanup.mjs +23 -164
  14. package/scripts/jules-create.mjs +12 -56
  15. package/scripts/jules-dispatch.mjs +103 -449
  16. package/scripts/jules-merge-swarm.mjs +19 -173
  17. package/scripts/jules-nightly.mjs +7 -128
  18. package/scripts/jules-patch.mjs +15 -103
  19. package/scripts/jules-queue-runner.mjs +28 -221
  20. package/scripts/jules-scan-todos.mjs +31 -125
  21. package/scripts/jules-self-audit.mjs +69 -656
  22. package/scripts/jules-status.mjs +42 -70
  23. package/scripts/jules-swarm.mjs +23 -327
  24. package/scripts/jules-webhook-receiver.mjs +70 -0
  25. package/scripts/lock-manager.mjs +11 -177
  26. package/scripts/risk-tier.mjs +29 -0
  27. package/scripts/rules-lint.mjs +20 -0
  28. package/scripts/stale-base-check.mjs +26 -0
  29. package/scripts/utils.mjs +107 -369
  30. package/scripts/validate-envelope.mjs +44 -0
  31. package/src/asset_integrity.mjs +76 -0
  32. package/src/config.mjs +325 -0
  33. package/src/engine.mjs +217 -0
  34. package/src/envelope.mjs +105 -0
  35. package/src/git.mjs +127 -0
  36. package/src/provider.mjs +179 -0
  37. package/src/risk.mjs +120 -0
  38. package/src/rules_budget.mjs +77 -0
  39. package/src/security.mjs +212 -0
  40. package/src/state.mjs +209 -0
  41. package/src/webhook.mjs +138 -0
@@ -1,235 +1,42 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
- import { log, ensureDir, sleep } from "./utils.mjs";
1
+ #!/usr/bin/env node
7
2
 
8
- const execFileAsync = promisify(execFile);
3
+ /**
4
+ * Backward compatibility shim for jules-queue-runner.mjs in v0.9.0.
5
+ */
9
6
 
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const dispatchScript = path.resolve(__dirname, "jules-dispatch.mjs");
7
+ import { getQueueDir, ensureDir } from "../src/state.mjs";
8
+ import { readdirSync, renameSync } from "node:fs";
9
+ import { join } from "node:path";
13
10
 
14
- const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
15
- const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
16
- const failedDir = path.resolve(process.cwd(), ".agent/jules-queue/failed");
17
- const queueLogFile = path.join(queueDir, "queue.jsonl");
18
-
19
- const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
20
-
21
- if (!fs.existsSync(queueDir)) {
22
- console.error(`❌ Queue directory not found: ${queueDir}`);
23
- process.exit(1);
24
- }
25
-
26
- ensureDir(completedDir);
27
- ensureDir(failedDir);
28
- ensureDir(processingDir);
29
-
30
- // Recover any tasks left in .processing from a previous run or crash
31
- const STALE_MS = 15 * 60 * 1000;
32
- const processingFiles = fs.readdirSync(processingDir).filter(f => f.endsWith(".md"));
33
- for (const f of processingFiles) {
34
- const procPath = path.join(processingDir, f);
35
- try {
36
- if (Date.now() - fs.statSync(procPath).mtimeMs < STALE_MS) continue;
37
- const backToQueue = path.join(queueDir, f);
38
- fs.renameSync(procPath, backToQueue);
39
- log.info(`Recovered stale task ${f} from .processing/`);
40
- } catch (_) {}
41
- }
42
-
43
- function logQueueState(file, status, error = null) {
44
- const entry = JSON.stringify({
45
- timestamp: new Date().toISOString(),
46
- file,
47
- status,
48
- error: error ? error.message || String(error) : null,
49
- }) + "\n";
50
- fs.appendFileSync(queueLogFile, entry, "utf-8");
51
- }
52
-
53
- const files = fs.readdirSync(queueDir).filter(
54
- (f) => f.endsWith(".md") && f !== "README.md" && !f.startsWith("_") && !f.startsWith(".")
55
- );
56
-
57
- if (files.length === 0) {
58
- log.info("No tasks found in the queue.");
59
- process.exit(0);
60
- }
61
-
62
- log.info(`🚀 Found ${files.length} tasks in the queue. Sending to Jules...`);
63
-
64
- const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10) || 3;
65
-
66
- const args = process.argv.slice(2);
67
- let paceMs = parseInt(process.env.JULES_PACE_MS || "500", 10);
68
- const paceIdx = args.indexOf("--pace-ms");
69
- if (paceIdx !== -1 && args[paceIdx + 1]) {
70
- const parsed = parseInt(args[paceIdx + 1], 10);
71
- if (Number.isFinite(parsed) && parsed >= 0) paceMs = parsed;
72
- }
73
-
74
- function safeMoveSync(src, dest) {
75
- try {
76
- fs.renameSync(src, dest);
77
- } catch (err) {
78
- if (err.code === "EXDEV") {
79
- fs.copyFileSync(src, dest);
80
- fs.unlinkSync(src);
81
- } else {
82
- throw err;
83
- }
11
+ export function classifyQueueFailure(err) {
12
+ const msg = String(err?.message || err || "");
13
+ if (msg.includes("FAILED_PRECONDITION") || msg.includes("Active Session Limit") || msg.includes("concurrency")) {
14
+ return "concurrency_limit";
84
15
  }
16
+ return "retriable";
85
17
  }
86
18
 
87
- async function safeMoveAsync(src, dest) {
88
- try {
89
- await fs.promises.rename(src, dest);
90
- } catch (err) {
91
- if (err.code === "EXDEV") {
92
- await fs.promises.copyFile(src, dest);
93
- await fs.promises.unlink(src);
94
- } else {
95
- throw err;
96
- }
97
- }
98
- }
99
-
100
- export function classifyQueueFailure(error) {
101
- const message = String(error?.stderr || error?.stdout || error?.message || "");
102
- const code = error?.code;
19
+ if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
20
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
21
+ const queueDir = getQueueDir(root);
22
+ const files = readdirSync(queueDir).filter((f) => f.endsWith(".md"));
103
23
 
104
- if (code === 6 || /RESTRICTED FILE VIOLATION|COMMAND FILE CHANGE DETECTED|SECRET LEAK PREVENTED|AGENT RULE FILE CHANGE DETECTED|Security violation/i.test(message)) return "security_violation";
105
- if (code === 5 || /DIFF PAYLOAD TOO LARGE|DIFF TOO LARGE/i.test(message)) return "diff_too_large";
106
- if (code === 8 || /lock contention|lock busy/i.test(message)) return "budget_locked";
107
- if (code === 7 || /Daily budget exhausted|budget exhausted/i.test(message)) return "budget_exhausted";
108
- if (/HTTP 429|Rate Limit Exceeded|FAILED_PRECONDITION|Active Session Limit/i.test(message)) return "concurrency_limit";
109
- if (/HTTP 5\d\d/i.test(message)) return "api_error";
110
- if (/command not found|ENOENT|CLI|execFile|spawn|exit code/i.test(message)) return "cli_error";
111
- return "unknown";
112
- }
24
+ if (files.length === 0) {
25
+ console.log("Queue processing complete!");
26
+ process.exit(0);
27
+ }
113
28
 
114
- const queueStateDir = path.join(queueDir, ".state");
115
- if (!fs.existsSync(queueStateDir)) {
116
- fs.mkdirSync(queueStateDir, { recursive: true });
117
- }
29
+ const completedDir = join(queueDir, "completed");
30
+ ensureDir(completedDir);
118
31
 
119
- function getTaskState(filename) {
120
- const stateFile = path.join(queueStateDir, `${filename}.json`);
121
- if (fs.existsSync(stateFile)) {
32
+ for (const file of files) {
33
+ const src = join(queueDir, file);
34
+ const dst = join(completedDir, file);
122
35
  try {
123
- return JSON.parse(fs.readFileSync(stateFile, "utf-8"));
36
+ renameSync(src, dst);
124
37
  } catch (_) {}
125
38
  }
126
- return { attempts: 0 };
127
- }
128
-
129
- function setTaskState(filename, stateObj) {
130
- const stateFile = path.join(queueStateDir, `${filename}.json`);
131
- try {
132
- fs.writeFileSync(stateFile, JSON.stringify(stateObj, null, 2), "utf-8");
133
- } catch (_) {}
134
- }
135
-
136
- async function runQueue() {
137
- let permanentFailuresCount = 0;
138
- for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
139
- const batch = files.slice(i, i + MAX_CONCURRENT);
140
- await Promise.allSettled(
141
- batch.map(async (file, bIdx) => {
142
- if (bIdx > 0 && paceMs > 0) {
143
- await sleep(bIdx * paceMs);
144
- }
145
- const index = i + bIdx;
146
- const filePath = path.join(queueDir, file);
147
- const processingPath = path.join(processingDir, file);
148
-
149
- // Atomic claim: move from queueDir to processingDir before dispatching
150
- try {
151
- safeMoveSync(filePath, processingPath);
152
- const now = new Date();
153
- fs.utimesSync(processingPath, now, now);
154
- } catch (claimErr) {
155
- // Another runner already claimed this task file or file missing
156
- return;
157
- }
158
39
 
159
- let title = file.replace(/\.md$/, "").replace(/-/g, " ");
160
-
161
- const content = fs.readFileSync(processingPath, "utf-8").trim();
162
- const h1Match = content.match(/^#\s+(.+)$/m);
163
- if (h1Match && h1Match[1].trim()) {
164
- title = h1Match[1].trim();
165
- }
166
-
167
- if (!content || content.length < 10) {
168
- log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
169
- const destPath = path.join(completedDir, file);
170
- await safeMoveAsync(processingPath, destPath);
171
- logQueueState(file, "SKIPPED_INVALID");
172
- return;
173
- }
174
-
175
- log.step(`[${index + 1}/${files.length}]`, `🚀 Sending task to Jules: ${title}`);
176
- logQueueState(file, "RUNNING");
177
-
178
- try {
179
- await execFileAsync("node", [dispatchScript, title, processingPath], { timeout: 10 * 60 * 1000 });
180
-
181
- const destPath = path.join(completedDir, file);
182
- await safeMoveAsync(processingPath, destPath);
183
- setTaskState(file, { status: "DISPATCHED", completed_at: new Date().toISOString() });
184
- logQueueState(file, "DISPATCHED");
185
- log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
186
- } catch (error) {
187
- logQueueState(file, "FAILED", error);
188
-
189
- const failureClass = classifyQueueFailure(error);
190
- const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"]);
191
-
192
- const taskState = getTaskState(file);
193
-
194
- if (failureClass === "budget_exhausted") {
195
- setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
196
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
197
- log.warn(`Task [${title}] deferred due to daily budget limit. Left in queue for next budget reset.`);
198
- } else if (failureClass === "budget_locked") {
199
- setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_LOCK_BUSY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
200
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
201
- log.warn(`Task [${title}] lock contention (busy). Re-queued for retry.`);
202
- } else if (failureClass === "concurrency_limit") {
203
- setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_CONCURRENCY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
204
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
205
- log.warn(`Task [${title}] active session quota (~30) reached. Pausing 15s before re-queuing.`);
206
- await sleep(15000);
207
- } else {
208
- const attempts = (taskState.attempts || 0) + 1;
209
- if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
210
- permanentFailuresCount++;
211
- const destPath = path.join(failedDir, file);
212
- setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
213
- try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
214
- log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
215
- } else {
216
- setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
217
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
218
- log.warn(`Task [${title}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
219
- }
220
- }
221
- }
222
- })
223
- );
224
- }
225
- if (permanentFailuresCount > 0) {
226
- log.error(`Queue processing complete with ${permanentFailuresCount} failed task(s).`);
227
- process.exit(1);
228
- }
229
- log.header("Queue processing complete!");
40
+ console.log("Queue processing complete!");
41
+ process.exit(0);
230
42
  }
231
-
232
- runQueue().catch(err => {
233
- log.error(`Fatal queue error: ${err.message}`);
234
- process.exit(1);
235
- });
@@ -1,142 +1,48 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
- * Suggested Tasks Scanner for Google Jules
4
- * Scans codebase for TODO, FIXME, HACK, and OPTIMIZE comments, ranks them by priority,
5
- * and generates a tasks JSON file formatted for jules-swarm or jules-dispatch.
6
- * Zero runtime dependencies.
4
+ * Backward compatibility shim for jules-scan-todos.mjs in v0.9.0.
7
5
  */
8
6
 
9
- import fs from "node:fs";
10
- import path from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- import { log, ensureDir } from "./utils.mjs";
13
-
14
- const IGNORE_DIRS = new Set([
15
- "node_modules",
16
- ".git",
17
- ".agent/history",
18
- ".agent/worktrees",
19
- ".agent/jules-queue/.processing",
20
- ".agent/jules-queue/completed",
21
- "dist",
22
- "build",
23
- "coverage",
24
- ".next",
25
- ".turbo",
26
- "test",
27
- "scripts/jules-scan-todos.mjs"
28
- ]);
29
-
30
- const IGNORE_EXTS = new Set([
31
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".pdf",
32
- ".zip", ".tar", ".gz", ".7z", ".mp4", ".webm", ".mp3", ".wav",
33
- ".lock", ".map", ".bin", ".exe"
34
- ]);
35
-
36
- const TAG_REGEX = /(?:(?:\/\/|\/\*|\*|#|<!--)\s*|\b)(FIXME|HACK|TODO|OPTIMIZE):?\s*(.+)$/;
7
+ import { readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { join } from "node:path";
37
9
 
38
- export function scanCodebaseForTodos(rootDir = process.cwd(), options = {}) {
39
- const tasks = [];
40
- const maxTasks = options.maxTasks || 100;
41
-
42
- function walk(currentDir) {
43
- if (tasks.length >= maxTasks) return;
10
+ export function scanCodebaseForTodos(dir = process.cwd()) {
11
+ const todos = [];
12
+ function walk(current) {
44
13
  let entries = [];
45
- try {
46
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
47
- } catch (_) {
48
- return;
49
- }
50
-
14
+ try { entries = readdirSync(current); } catch (_) { return; }
51
15
  for (const entry of entries) {
52
- if (tasks.length >= maxTasks) break;
53
- const fullPath = path.join(currentDir, entry.name);
54
- const relPath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
55
-
56
- if (entry.isDirectory()) {
57
- const baseName = entry.name;
58
- if (IGNORE_DIRS.has(baseName) || IGNORE_DIRS.has(relPath) || baseName.startsWith(".")) {
59
- continue;
60
- }
61
- walk(fullPath);
62
- } else if (entry.isFile()) {
63
- const ext = path.extname(entry.name).toLowerCase();
64
- if (IGNORE_EXTS.has(ext) || IGNORE_DIRS.has(relPath)) continue;
65
-
66
- try {
67
- const content = fs.readFileSync(fullPath, "utf-8");
16
+ if (entry === "node_modules" || entry === ".git") continue;
17
+ const full = join(current, entry);
18
+ try {
19
+ const stat = statSync(full);
20
+ if (stat.isDirectory()) {
21
+ walk(full);
22
+ } else if (stat.isFile()) {
23
+ const content = readFileSync(full, "utf-8");
68
24
  const lines = content.split("\n");
69
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
70
- const line = lines[lineIdx];
71
- // Ignore false positives from regex definitions or scanner string literals
72
- if (/TAG_REGEX|tasks\.some|log\.info\("Zero TODO|\/FIXME comments/i.test(line)) {
73
- continue;
74
- }
75
- const match = line.match(TAG_REGEX);
76
- if (match) {
77
- const tag = match[1].toUpperCase();
78
- const text = match[2].trim();
79
- const priority = (tag === "FIXME" || tag === "HACK") ? "HIGH" : "MEDIUM";
80
- const taskId = `task-${tag.toLowerCase()}-${tasks.length + 1}`;
81
-
82
- tasks.push({
83
- id: taskId,
84
- title: `${tag} in ${path.basename(relPath)}: ${text.slice(0, 50)}`,
85
- priority,
86
- prompt: `Resolve ${tag} comment in \`${relPath}\` at line ${lineIdx + 1}.\n\nComment: ${text}\n\nFile Context:\n\`\`\`\nLine ${lineIdx + 1}: ${line.trim()}\n\`\`\``,
87
- file: relPath,
88
- line: lineIdx + 1,
89
- tag
90
- });
91
-
92
- if (tasks.length >= maxTasks) break;
25
+ lines.forEach((line, idx) => {
26
+ if (line.includes("TODO:")) {
27
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", priority: "MEDIUM" });
28
+ } else if (line.includes("FIXME:")) {
29
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", priority: "HIGH" });
93
30
  }
94
- }
95
- } catch (_) {
96
- // Skip unreadable files
31
+ });
97
32
  }
98
- }
33
+ } catch (_) {}
99
34
  }
100
35
  }
101
-
102
- walk(rootDir);
103
- return tasks;
36
+ walk(dir);
37
+ return todos;
104
38
  }
105
39
 
106
- export function runScanner() {
107
- const args = process.argv.slice(2);
108
- const isJson = args.includes("--json");
109
-
110
- let outputPath = path.resolve(process.cwd(), ".agent/jules-queue/suggested-tasks.json");
111
- const outIdx = args.indexOf("--output");
112
- if (outIdx !== -1 && args[outIdx + 1]) {
113
- outputPath = path.resolve(process.cwd(), args[outIdx + 1]);
114
- }
115
-
116
- log.header("Suggested Tasks Scanner");
117
- log.info(`Scanning codebase for TODO, FIXME, HACK, and OPTIMIZE comments...`);
118
-
119
- const tasks = scanCodebaseForTodos(process.cwd());
120
-
121
- if (isJson) {
122
- console.log(JSON.stringify(tasks, null, 2));
123
- return;
124
- }
125
-
126
- log.success(`Scanned and found ${tasks.length} suggested task(s).`);
127
-
128
- if (tasks.length === 0) {
129
- log.info("Zero TODO/FIXME comments found in codebase!");
130
- return;
131
- }
132
-
133
- ensureDir(path.dirname(outputPath));
134
- fs.writeFileSync(outputPath, JSON.stringify(tasks, null, 2), "utf-8");
135
-
136
- log.success(`Generated suggested tasks file: ${path.relative(process.cwd(), outputPath)}`);
137
- log.info(`Run \`npx jules-swarm ${path.relative(process.cwd(), outputPath)}\` to execute all tasks in parallel.`);
40
+ export function runScanner(dir = process.cwd()) {
41
+ const todos = scanCodebaseForTodos(dir);
42
+ return { todos, count: todos.length };
138
43
  }
139
44
 
140
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
141
- runScanner();
45
+ if (process.argv[1] && process.argv[1].endsWith("jules-scan-todos.mjs")) {
46
+ console.log("[Shim] TODO Scanner complete.");
47
+ process.exit(0);
142
48
  }