jules-orchestrator-kit 0.2.9 ā 0.5.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/.agent/rules/jules-protocol.md +33 -0
- package/.github/workflows/agent-scope-guard.yml +56 -0
- package/.github/workflows/publish.yml +0 -3
- package/JULES_RULES_TEMPLATE.md +52 -0
- package/README.md +208 -165
- package/bin/init.js +65 -6
- package/index.mjs +11 -0
- package/package.json +15 -5
- package/scripts/jules-cleanup.mjs +200 -0
- package/scripts/jules-create.mjs +47 -0
- package/scripts/jules-dispatch.mjs +142 -98
- package/scripts/jules-nightly.mjs +42 -28
- package/scripts/jules-queue-runner.mjs +100 -30
- package/scripts/jules-scan-todos.mjs +142 -0
- package/scripts/jules-self-audit.mjs +197 -44
- package/scripts/jules-status.mjs +57 -0
- package/scripts/jules-swarm.mjs +106 -31
- package/scripts/lock-manager.mjs +147 -0
- package/scripts/utils.mjs +94 -0
- package/.agent/history/2026-07-27-dispatch-test-swarm-task.md +0 -9
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { execFileSync, execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { log, logToHistory } from "./utils.mjs";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
10
14
|
|
|
11
15
|
const isDryRun = process.argv.includes("--dry-run");
|
|
12
16
|
|
|
@@ -50,67 +54,77 @@ const UNIVERSAL_NIGHTLY_TASKS = [
|
|
|
50
54
|
];
|
|
51
55
|
|
|
52
56
|
function logNightlyHistory(taskResults, dryRun = false) {
|
|
53
|
-
const dateStr = new Date().toISOString().split("T")[0];
|
|
54
|
-
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
55
|
-
if (!fs.existsSync(historyDir)) {
|
|
56
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
const historyFile = path.join(historyDir, `${dateStr}-nightly-audit.md`);
|
|
59
|
-
|
|
60
57
|
const modeLabel = dryRun ? "[DRY RUN]" : "[DISPATCHED]";
|
|
58
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
61
59
|
let content = `---\ntype: nightly_jules_audit\ntimestamp: "${new Date().toISOString()}"\nstatus: "${modeLabel}"\n---\n# Nightly Jules Maintenance Suite Audit - ${dateStr}\n\nSummary of automated audit dispatches:\n\n`;
|
|
62
60
|
|
|
63
61
|
for (const res of taskResults) {
|
|
64
62
|
content += `- **${res.title}** (\`${res.id}\`): ${res.status}\n`;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
const historyFile = logToHistory(`nightly-audit.md`, content, "nightly");
|
|
66
|
+
log.success(`Logged nightly audit summary to: ${path.relative(process.cwd(), historyFile)}`);
|
|
67
|
+
|
|
68
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
69
|
+
try {
|
|
70
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n${content}\n`, "utf-8");
|
|
71
|
+
log.success("Appended nightly audit summary to GitHub Step Summary.");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log.warn(`Failed to write to GITHUB_STEP_SUMMARY: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
function dispatchTask(task, dryRun = false) {
|
|
78
|
+
async function dispatchTask(task, dryRun = false) {
|
|
72
79
|
const title = task.title;
|
|
73
80
|
const fullPrompt = task.prompt;
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
log.step("š", `[${task.id}] Preparing task: '${title}'...`);
|
|
76
83
|
|
|
77
84
|
if (dryRun) {
|
|
78
|
-
|
|
85
|
+
log.dim(` [DRY RUN] Would dispatch task '${title}' to target repository`);
|
|
79
86
|
return { id: task.id, title, status: "Dry Run OK" };
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
83
90
|
if (fs.existsSync(dispatchScript)) {
|
|
84
91
|
try {
|
|
85
|
-
|
|
92
|
+
await execFileAsync("node", [dispatchScript, title, fullPrompt]);
|
|
86
93
|
return { id: task.id, title, status: "Dispatched successfully" };
|
|
87
94
|
} catch (error) {
|
|
88
|
-
|
|
95
|
+
log.error(`Failed to dispatch via jules-dispatch.mjs: ${error.message}`);
|
|
89
96
|
return { id: task.id, title, status: `Failed: ${error.message}` };
|
|
90
97
|
}
|
|
91
98
|
} else {
|
|
92
|
-
|
|
99
|
+
log.error(`Dispatch script not found at ${dispatchScript}`);
|
|
93
100
|
return { id: task.id, title, status: "Failed (Script missing)" };
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
function main() {
|
|
98
|
-
|
|
99
|
-
console.log("š Nightly Jules Maintenance & Audit Suite");
|
|
100
|
-
console.log("==================================================");
|
|
104
|
+
async function main() {
|
|
105
|
+
log.header("Nightly Jules Maintenance & Audit Suite");
|
|
101
106
|
|
|
102
107
|
if (isDryRun) {
|
|
103
|
-
|
|
108
|
+
log.info("Running in DRY RUN mode. No tasks will be dispatched.");
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
const taskPromises = UNIVERSAL_NIGHTLY_TASKS.map((task) => dispatchTask(task, isDryRun));
|
|
112
|
+
const settled = await Promise.allSettled(taskPromises);
|
|
113
|
+
const results = settled.map((s, idx) =>
|
|
114
|
+
s.status === "fulfilled" ? s.value : { id: UNIVERSAL_NIGHTLY_TASKS[idx].id, title: UNIVERSAL_NIGHTLY_TASKS[idx].title, status: `Failed: ${s.reason}` }
|
|
115
|
+
);
|
|
111
116
|
|
|
112
117
|
logNightlyHistory(results, isDryRun);
|
|
113
|
-
|
|
118
|
+
const hasFailures = results.some((r) => r.status && (r.status.includes("Failed") || r.status.includes("error")));
|
|
119
|
+
if (hasFailures) {
|
|
120
|
+
log.error("Nightly Jules audit suite completed with task errors.");
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
} else {
|
|
123
|
+
log.success("Nightly Jules audit suite execution completed cleanly.");
|
|
124
|
+
}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
main()
|
|
127
|
+
main().catch((err) => {
|
|
128
|
+
log.error(`Unhandled error in nightly suite: ${err.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { log, ensureDir, sleep } from "./utils.mjs";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
5
9
|
|
|
6
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
11
|
const __dirname = path.dirname(__filename);
|
|
@@ -11,14 +15,15 @@ const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
|
11
15
|
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
12
16
|
const queueLogFile = path.join(queueDir, "queue.jsonl");
|
|
13
17
|
|
|
18
|
+
const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
|
|
19
|
+
|
|
14
20
|
if (!fs.existsSync(queueDir)) {
|
|
15
21
|
console.error(`ā Queue directory not found: ${queueDir}`);
|
|
16
22
|
process.exit(1);
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
25
|
+
ensureDir(completedDir);
|
|
26
|
+
ensureDir(processingDir);
|
|
22
27
|
|
|
23
28
|
function logQueueState(file, status, error = null) {
|
|
24
29
|
const entry = JSON.stringify({
|
|
@@ -35,35 +40,100 @@ const files = fs.readdirSync(queueDir).filter(
|
|
|
35
40
|
);
|
|
36
41
|
|
|
37
42
|
if (files.length === 0) {
|
|
38
|
-
|
|
43
|
+
log.info("No tasks found in the queue.");
|
|
39
44
|
process.exit(0);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
log.info(`š Found ${files.length} tasks in the queue. Sending to Jules...`);
|
|
48
|
+
|
|
49
|
+
const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10) || 3;
|
|
50
|
+
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
let paceMs = parseInt(process.env.JULES_PACE_MS || "500", 10);
|
|
53
|
+
const paceIdx = args.indexOf("--pace-ms");
|
|
54
|
+
if (paceIdx !== -1 && args[paceIdx + 1]) {
|
|
55
|
+
const parsed = parseInt(args[paceIdx + 1], 10);
|
|
56
|
+
if (Number.isFinite(parsed) && parsed >= 0) paceMs = parsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeMoveSync(src, dest) {
|
|
53
60
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.log(`ā
Moved ${file} to completed/`);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
logQueueState(file, "FAILED", error);
|
|
65
|
-
console.error(`ā ļø Failed to dispatch task [${title}]:`, error.message);
|
|
61
|
+
fs.renameSync(src, dest);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code === "EXDEV") {
|
|
64
|
+
fs.copyFileSync(src, dest);
|
|
65
|
+
fs.unlinkSync(src);
|
|
66
|
+
} else {
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
|
-
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function safeMoveAsync(src, dest) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.promises.rename(src, dest);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.code === "EXDEV") {
|
|
77
|
+
await fs.promises.copyFile(src, dest);
|
|
78
|
+
await fs.promises.unlink(src);
|
|
79
|
+
} else {
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runQueue() {
|
|
86
|
+
for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
|
|
87
|
+
const batch = files.slice(i, i + MAX_CONCURRENT);
|
|
88
|
+
await Promise.allSettled(
|
|
89
|
+
batch.map(async (file, bIdx) => {
|
|
90
|
+
if (bIdx > 0 && paceMs > 0) {
|
|
91
|
+
await sleep(bIdx * paceMs);
|
|
92
|
+
}
|
|
93
|
+
const index = i + bIdx;
|
|
94
|
+
const filePath = path.join(queueDir, file);
|
|
95
|
+
const processingPath = path.join(processingDir, file);
|
|
96
|
+
|
|
97
|
+
// Atomic claim: move from queueDir to processingDir before dispatching
|
|
98
|
+
try {
|
|
99
|
+
safeMoveSync(filePath, processingPath);
|
|
100
|
+
} catch (claimErr) {
|
|
101
|
+
// Another runner already claimed this task file or file missing
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
106
|
+
|
|
107
|
+
const content = fs.readFileSync(processingPath, "utf-8").trim();
|
|
108
|
+
if (!content || content.length < 10) {
|
|
109
|
+
log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
|
|
110
|
+
const destPath = path.join(completedDir, file);
|
|
111
|
+
await safeMoveAsync(processingPath, destPath);
|
|
112
|
+
logQueueState(file, "SKIPPED_INVALID");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
68
115
|
|
|
69
|
-
|
|
116
|
+
log.step(`[${index + 1}/${files.length}]`, `š Sending task to Jules: ${title}`);
|
|
117
|
+
logQueueState(file, "RUNNING");
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await execFileAsync("node", [dispatchScript, title, processingPath]);
|
|
121
|
+
|
|
122
|
+
const destPath = path.join(completedDir, file);
|
|
123
|
+
await safeMoveAsync(processingPath, destPath);
|
|
124
|
+
logQueueState(file, "DISPATCHED");
|
|
125
|
+
log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logQueueState(file, "FAILED", error);
|
|
128
|
+
log.error(`Failed to dispatch task [${title}]: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
log.header("Queue processing complete!");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
runQueue().catch(err => {
|
|
137
|
+
log.error(`Fatal queue error: ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
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 external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { log, ensureDir } from "./utils.mjs";
|
|
12
|
+
|
|
13
|
+
const IGNORE_DIRS = new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
".agent/history",
|
|
17
|
+
".agent/worktrees",
|
|
18
|
+
".agent/jules-queue/.processing",
|
|
19
|
+
".agent/jules-queue/completed",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
"coverage",
|
|
23
|
+
".next",
|
|
24
|
+
".turbo",
|
|
25
|
+
"tmp_repos",
|
|
26
|
+
"tmp_repos2",
|
|
27
|
+
"tmp_repos3"
|
|
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*(.+)$/;
|
|
37
|
+
|
|
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;
|
|
44
|
+
let entries = [];
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
47
|
+
} catch (_) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
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)) continue;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
68
|
+
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, test assertions, or scanner string literals
|
|
72
|
+
if (/TAG_REGEX|assert\.|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;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (_) {
|
|
96
|
+
// Skip unreadable files
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
walk(rootDir);
|
|
103
|
+
return tasks;
|
|
104
|
+
}
|
|
105
|
+
|
|
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.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.url.slice(7))) {
|
|
141
|
+
runScanner();
|
|
142
|
+
}
|