jules-orchestrator-kit 0.3.0 → 0.5.2
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/.env.example +49 -3
- package/.github/workflows/agent-scope-guard.yml +56 -0
- package/.github/workflows/jules-audit.yml +7 -1
- package/JULES_RULES_TEMPLATE.md +52 -0
- package/README.md +207 -152
- package/bin/init.js +65 -6
- package/index.mjs +11 -0
- package/package.json +12 -4
- package/scripts/jules-cleanup.mjs +200 -0
- package/scripts/jules-create.mjs +47 -0
- package/scripts/jules-dispatch.mjs +150 -107
- package/scripts/jules-nightly.mjs +37 -29
- package/scripts/jules-queue-runner.mjs +94 -40
- package/scripts/jules-scan-todos.mjs +142 -0
- package/scripts/jules-self-audit.mjs +172 -40
- package/scripts/jules-status.mjs +57 -0
- package/scripts/jules-swarm.mjs +68 -32
- package/scripts/lock-manager.mjs +147 -0
- package/scripts/utils.mjs +94 -0
|
@@ -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);
|
|
@@ -18,13 +22,8 @@ if (!fs.existsSync(queueDir)) {
|
|
|
18
22
|
process.exit(1);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!fs.existsSync(processingDir)) {
|
|
26
|
-
fs.mkdirSync(processingDir, { recursive: true });
|
|
27
|
-
}
|
|
25
|
+
ensureDir(completedDir);
|
|
26
|
+
ensureDir(processingDir);
|
|
28
27
|
|
|
29
28
|
function logQueueState(file, status, error = null) {
|
|
30
29
|
const entry = JSON.stringify({
|
|
@@ -41,45 +40,100 @@ const files = fs.readdirSync(queueDir).filter(
|
|
|
41
40
|
);
|
|
42
41
|
|
|
43
42
|
if (files.length === 0) {
|
|
44
|
-
|
|
43
|
+
log.info("No tasks found in the queue.");
|
|
45
44
|
process.exit(0);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
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;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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) {
|
|
55
60
|
try {
|
|
56
|
-
fs.renameSync(
|
|
57
|
-
} catch (
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
69
|
}
|
|
70
|
+
}
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
const title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
64
|
-
|
|
65
|
-
console.log(`\n----------------------------------------`);
|
|
66
|
-
console.log(`[${index + 1}/${files.length}] Dispatching queued task: ${title}`);
|
|
67
|
-
logQueueState(file, "RUNNING");
|
|
68
|
-
|
|
72
|
+
async function safeMoveAsync(src, dest) {
|
|
69
73
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(`✅ Moved ${file} to completed/`);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
logQueueState(file, "FAILED", error);
|
|
81
|
-
console.error(`⚠️ Failed to dispatch task [${title}]:`, error.message);
|
|
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
82
|
}
|
|
83
|
-
}
|
|
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
|
+
}
|
|
115
|
+
|
|
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
|
+
}
|
|
84
135
|
|
|
85
|
-
|
|
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
|
+
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import os from "node:os";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
|
|
8
|
+
import { log, logToHistory } from "./utils.mjs";
|
|
6
9
|
|
|
7
|
-
function
|
|
10
|
+
function runGitCommand(args, ignoreError = false) {
|
|
8
11
|
try {
|
|
9
|
-
return
|
|
12
|
+
return execFileSync("git", args, { encoding: "utf-8" }).trim();
|
|
10
13
|
} catch (error) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
process.exit(1);
|
|
14
|
+
const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
|
|
15
|
+
err.code = 1;
|
|
16
|
+
throw err;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -145,32 +147,48 @@ export function logAuditMetrics(metrics) {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export function runSelfAudit() {
|
|
148
|
-
|
|
150
|
+
log.header("Running Jules PR Self-Audit Gatekeeper...");
|
|
149
151
|
|
|
150
152
|
const targetBranch = process.env.BASE_BRANCH || "main";
|
|
151
153
|
const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
|
|
152
154
|
if (!SAFE_BRANCH.test(targetBranch)) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
const err = new Error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
|
|
156
|
+
err.code = 2;
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
log.info(`Target Branch: ${targetBranch}`);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
163
|
+
} catch {
|
|
164
|
+
const err = new Error("FATAL: git is not installed or not in PATH.");
|
|
165
|
+
err.code = 2;
|
|
166
|
+
throw err;
|
|
155
167
|
}
|
|
156
|
-
console.log(`🎯 Target Branch: ${targetBranch}`);
|
|
157
168
|
|
|
158
169
|
if (process.env.CI) {
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
log.info("☁️ CI environment detected. Fetching merge-base history...");
|
|
171
|
+
runGitCommand(["fetch", "origin", targetBranch, "--depth=100"], true);
|
|
172
|
+
const tmpRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
173
|
+
const tmpMerge = runGitCommand(["merge-base", "HEAD", tmpRef], true);
|
|
174
|
+
if (!tmpMerge) {
|
|
175
|
+
log.warn("Shallow fetch failed to find merge-base, executing full unshallow...");
|
|
176
|
+
runGitCommand(["fetch", "origin", targetBranch, "--unshallow"], true);
|
|
177
|
+
}
|
|
161
178
|
}
|
|
162
179
|
|
|
163
|
-
const mainRef =
|
|
164
|
-
const mergeBase =
|
|
180
|
+
const mainRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
181
|
+
const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
|
|
165
182
|
|
|
166
183
|
if (!mergeBase) {
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
const err = new Error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
|
|
185
|
+
err.code = 2;
|
|
186
|
+
throw err;
|
|
169
187
|
}
|
|
170
188
|
|
|
171
|
-
|
|
189
|
+
log.info(`Merge-Base Hash: ${mergeBase}`);
|
|
172
190
|
|
|
173
|
-
const rawDiffFiles =
|
|
191
|
+
const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
|
|
174
192
|
.split("\0")
|
|
175
193
|
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
176
194
|
.filter(Boolean);
|
|
@@ -178,12 +196,12 @@ export function runSelfAudit() {
|
|
|
178
196
|
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
|
179
197
|
const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
changedCodeFiles.forEach((file) =>
|
|
199
|
+
log.info(`Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
|
|
200
|
+
changedCodeFiles.forEach((file) => log.dim(` - ${file}`));
|
|
183
201
|
|
|
184
202
|
// Fail closed: Load security config exclusively from base branch (mainRef).
|
|
185
203
|
// Never fall back to working-tree config which an untrusted PR could craft.
|
|
186
|
-
const trustedConfig =
|
|
204
|
+
const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
|
|
187
205
|
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
188
206
|
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
189
207
|
|
|
@@ -195,16 +213,15 @@ export function runSelfAudit() {
|
|
|
195
213
|
});
|
|
196
214
|
|
|
197
215
|
if (violations.length > 0) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
process.exit(1);
|
|
216
|
+
const err = new Error("RESTRICTED FILE VIOLATION DETECTED!");
|
|
217
|
+
err.code = 3;
|
|
218
|
+
throw err;
|
|
202
219
|
}
|
|
203
|
-
|
|
220
|
+
log.success("Restricted File Boundary Check: PASSED");
|
|
204
221
|
|
|
205
|
-
|
|
222
|
+
log.info("Resolving Dynamic Verification Suite...");
|
|
206
223
|
const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
|
|
207
|
-
|
|
224
|
+
log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
|
|
208
225
|
|
|
209
226
|
const startTime = Date.now();
|
|
210
227
|
let testPassed = true;
|
|
@@ -221,24 +238,24 @@ export function runSelfAudit() {
|
|
|
221
238
|
};
|
|
222
239
|
|
|
223
240
|
if (resolvedCmds.testCmd) {
|
|
224
|
-
|
|
241
|
+
log.step("▶", `Running Test Verification: ${resolvedCmds.testCmd}`);
|
|
225
242
|
if (!runVerification(resolvedCmds.testCmd)) {
|
|
226
243
|
testPassed = false;
|
|
227
|
-
|
|
244
|
+
log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
if (testPassed && resolvedCmds.buildCmd) {
|
|
232
|
-
|
|
249
|
+
log.step("▶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
|
|
233
250
|
if (!runVerification(resolvedCmds.buildCmd)) {
|
|
234
251
|
testPassed = false;
|
|
235
|
-
|
|
252
|
+
log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
|
|
236
253
|
}
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
log.info("No build or test scripts found. Running git status check.");
|
|
258
|
+
execSync("git status", { stdio: "inherit" });
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
const durationMs = Date.now() - startTime;
|
|
@@ -251,17 +268,132 @@ export function runSelfAudit() {
|
|
|
251
268
|
});
|
|
252
269
|
|
|
253
270
|
if (!testPassed) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
log.error("OODA SELF-HEALING FEEDBACK LOG:");
|
|
272
|
+
log.dim(failureLog.slice(-1500));
|
|
273
|
+
|
|
274
|
+
if (process.env.CI && !process.env.ALLOW_AUTO_REPAIR) {
|
|
275
|
+
const err = new Error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
|
|
276
|
+
err.code = 4;
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let oodaRetries = 0;
|
|
281
|
+
try {
|
|
282
|
+
const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
|
|
283
|
+
const matches = history.match(/OODA Auto-Repair/gi);
|
|
284
|
+
if (matches) {
|
|
285
|
+
oodaRetries = matches.length;
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Ignorera git-fel här
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (oodaRetries < 3) {
|
|
292
|
+
log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
|
|
293
|
+
const prompt = `OODA Auto-Repair Attempt ${oodaRetries + 1}\n\nThe verification suite failed with the following errors after the previous patch:\n\n\`\`\`\n${failureLog.slice(-1500)}\n\`\`\`\n\nPlease fix the errors so the verification passes.`;
|
|
294
|
+
|
|
295
|
+
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
296
|
+
try {
|
|
297
|
+
const env = {
|
|
298
|
+
...process.env,
|
|
299
|
+
BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
|
|
300
|
+
};
|
|
301
|
+
execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
|
|
302
|
+
stdio: "inherit",
|
|
303
|
+
env
|
|
304
|
+
});
|
|
305
|
+
} catch (dispatchErr) {
|
|
306
|
+
log.error(`Failed to trigger OODA repair dispatch: ${dispatchErr.message}`);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
|
|
310
|
+
}
|
|
311
|
+
const err = new Error("OODA Auto-Repair failed or exhausted.");
|
|
312
|
+
err.code = 4;
|
|
313
|
+
throw err;
|
|
257
314
|
}
|
|
258
315
|
|
|
259
|
-
|
|
316
|
+
log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function runPreflightSandbox() {
|
|
320
|
+
log.header("Initializing Pre-Flight Sandbox (Sterile Simulation)...");
|
|
321
|
+
|
|
322
|
+
const runId = crypto.randomBytes(4).toString("hex");
|
|
323
|
+
const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
|
|
324
|
+
const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
|
|
328
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
329
|
+
|
|
330
|
+
execSync(`git archive HEAD -o "${tempArchive}"`);
|
|
331
|
+
execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
|
|
332
|
+
|
|
333
|
+
log.step("[2/5]", "Sterilizing environment variables...");
|
|
334
|
+
const sterileEnv = { ...process.env };
|
|
335
|
+
const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
|
|
336
|
+
for (const key of stripKeys) {
|
|
337
|
+
if (sterileEnv[key]) {
|
|
338
|
+
sterileEnv[key] = `mock-${key.toLowerCase()}`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
log.step("[3/5]", "Installing dependencies in sandbox (npm ci)...");
|
|
343
|
+
const packageJsonPath = path.join(sandboxDir, "package.json");
|
|
344
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
345
|
+
try {
|
|
346
|
+
execSync("npm ci --ignore-scripts", { cwd: sandboxDir, stdio: "ignore" });
|
|
347
|
+
} catch (e) {
|
|
348
|
+
log.warn("npm ci failed or wasn't needed. Proceeding...");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
log.step("[4/5]", "Resolving execution boundary in sandbox...");
|
|
353
|
+
const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
|
|
354
|
+
|
|
355
|
+
if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
|
|
356
|
+
log.step("[5/5]", `Running verification: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
|
|
357
|
+
execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
|
|
358
|
+
cwd: sandboxDir,
|
|
359
|
+
env: sterileEnv,
|
|
360
|
+
stdio: "inherit"
|
|
361
|
+
});
|
|
362
|
+
log.success("Pre-Flight Passed. Zero epistemic drift detected.");
|
|
363
|
+
} else {
|
|
364
|
+
log.info("No test/build commands found for verification.");
|
|
365
|
+
log.success("Pre-Flight Passed (No-Op).");
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
log.error("\nPRE-FLIGHT FAILED!");
|
|
369
|
+
log.error("The agent would likely crash (The 5-Minute Drop-off) in the Web UI due to missing dependencies.");
|
|
370
|
+
const wrappedErr = new Error(err.message);
|
|
371
|
+
wrappedErr.code = 1;
|
|
372
|
+
throw wrappedErr;
|
|
373
|
+
} finally {
|
|
374
|
+
try {
|
|
375
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
376
|
+
if (fs.existsSync(tempArchive)) {
|
|
377
|
+
fs.rmSync(tempArchive, { force: true });
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
// Ignorera fel vid cleanup
|
|
381
|
+
}
|
|
382
|
+
}
|
|
260
383
|
}
|
|
261
384
|
|
|
262
385
|
// Execute when invoked directly from CLI
|
|
263
386
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
264
|
-
|
|
387
|
+
try {
|
|
388
|
+
if (process.argv.includes("--preflight")) {
|
|
389
|
+
runPreflightSandbox();
|
|
390
|
+
} else {
|
|
391
|
+
runSelfAudit();
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err.message) log.error(err.message);
|
|
395
|
+
process.exit(err.code || 1);
|
|
396
|
+
}
|
|
265
397
|
}
|
|
266
398
|
|
|
267
399
|
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { log } from "./utils.mjs";
|
|
10
|
+
|
|
11
|
+
const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(queueLogPath)) {
|
|
14
|
+
log.info("Queue log is empty. No tasks have been dispatched yet.");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(queueLogPath, "utf-8");
|
|
20
|
+
const lines = content.split("\n").filter(Boolean);
|
|
21
|
+
|
|
22
|
+
if (lines.length === 0) {
|
|
23
|
+
log.info("Queue log is empty.");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// We only want the latest status for each task file
|
|
28
|
+
const statusMap = new Map();
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
try {
|
|
32
|
+
const entry = JSON.parse(line);
|
|
33
|
+
if (entry.file) {
|
|
34
|
+
statusMap.set(entry.file, {
|
|
35
|
+
Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
|
|
36
|
+
Status: entry.status,
|
|
37
|
+
Timestamp: new Date(entry.timestamp).toLocaleString(),
|
|
38
|
+
Error: entry.error || "-"
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Ignore malformed lines
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (statusMap.size === 0) {
|
|
47
|
+
log.info("No parseable task events found in queue.jsonl");
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log.header("Jules Queue Status");
|
|
52
|
+
console.table(Array.from(statusMap.values()));
|
|
53
|
+
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.error(`Failed to read queue log: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|