jules-orchestrator-kit 0.3.0 → 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/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 +11 -4
- package/scripts/jules-cleanup.mjs +200 -0
- package/scripts/jules-create.mjs +47 -0
- package/scripts/jules-dispatch.mjs +134 -104
- 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 +186 -37
- 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
|
@@ -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,16 +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
14
|
if (ignoreError) return "";
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
log.error(`Git command failed: git ${args.join(" ")}`);
|
|
16
|
+
log.error(error.message);
|
|
14
17
|
process.exit(1);
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -145,32 +148,45 @@ export function logAuditMetrics(metrics) {
|
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
export function runSelfAudit() {
|
|
148
|
-
|
|
151
|
+
log.header("Running Jules PR Self-Audit Gatekeeper...");
|
|
149
152
|
|
|
150
153
|
const targetBranch = process.env.BASE_BRANCH || "main";
|
|
151
154
|
const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
|
|
152
155
|
if (!SAFE_BRANCH.test(targetBranch)) {
|
|
153
|
-
|
|
154
|
-
process.exit(
|
|
156
|
+
log.error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
log.info(`Target Branch: ${targetBranch}`);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
163
|
+
} catch {
|
|
164
|
+
log.error("FATAL: git is not installed or not in PATH.");
|
|
165
|
+
process.exit(2);
|
|
155
166
|
}
|
|
156
|
-
console.log(`🎯 Target Branch: ${targetBranch}`);
|
|
157
167
|
|
|
158
168
|
if (process.env.CI) {
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
log.info("☁️ CI environment detected. Fetching merge-base history...");
|
|
170
|
+
runGitCommand(["fetch", "origin", targetBranch, "--depth=100"], true);
|
|
171
|
+
const tmpRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
172
|
+
const tmpMerge = runGitCommand(["merge-base", "HEAD", tmpRef], true);
|
|
173
|
+
if (!tmpMerge) {
|
|
174
|
+
log.warn("Shallow fetch failed to find merge-base, executing full unshallow...");
|
|
175
|
+
runGitCommand(["fetch", "origin", targetBranch, "--unshallow"], true);
|
|
176
|
+
}
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
const mainRef =
|
|
164
|
-
const mergeBase =
|
|
179
|
+
const mainRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
180
|
+
const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
|
|
165
181
|
|
|
166
182
|
if (!mergeBase) {
|
|
167
|
-
|
|
168
|
-
process.exit(
|
|
183
|
+
log.error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
|
|
184
|
+
process.exit(2);
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
|
|
187
|
+
log.info(`Merge-Base Hash: ${mergeBase}`);
|
|
172
188
|
|
|
173
|
-
const rawDiffFiles =
|
|
189
|
+
const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
|
|
174
190
|
.split("\0")
|
|
175
191
|
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
176
192
|
.filter(Boolean);
|
|
@@ -178,12 +194,12 @@ export function runSelfAudit() {
|
|
|
178
194
|
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
|
179
195
|
const changedCodeFiles = rawDiffFiles.filter((f) => !isBloatFile(f));
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
changedCodeFiles.forEach((file) =>
|
|
197
|
+
log.info(`Modified Code Files (${changedCodeFiles.length} of ${rawDiffFiles.length} total changes):`);
|
|
198
|
+
changedCodeFiles.forEach((file) => log.dim(` - ${file}`));
|
|
183
199
|
|
|
184
200
|
// Fail closed: Load security config exclusively from base branch (mainRef).
|
|
185
201
|
// Never fall back to working-tree config which an untrusted PR could craft.
|
|
186
|
-
const trustedConfig =
|
|
202
|
+
const trustedConfig = runGitCommand(["show", `${mainRef}:.agent/jules.yml`], true);
|
|
187
203
|
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
188
204
|
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
189
205
|
|
|
@@ -195,16 +211,16 @@ export function runSelfAudit() {
|
|
|
195
211
|
});
|
|
196
212
|
|
|
197
213
|
if (violations.length > 0) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
violations.forEach((v) =>
|
|
201
|
-
process.exit(
|
|
214
|
+
log.error("RESTRICTED FILE VIOLATION DETECTED!");
|
|
215
|
+
log.error("Jules PR attempted to modify forbidden system files:");
|
|
216
|
+
violations.forEach((v) => log.error(` - ${v}`));
|
|
217
|
+
process.exit(3);
|
|
202
218
|
}
|
|
203
|
-
|
|
219
|
+
log.success("Restricted File Boundary Check: PASSED");
|
|
204
220
|
|
|
205
|
-
|
|
221
|
+
log.info("Resolving Dynamic Verification Suite...");
|
|
206
222
|
const resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, process.cwd());
|
|
207
|
-
|
|
223
|
+
log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
|
|
208
224
|
|
|
209
225
|
const startTime = Date.now();
|
|
210
226
|
let testPassed = true;
|
|
@@ -221,23 +237,23 @@ export function runSelfAudit() {
|
|
|
221
237
|
};
|
|
222
238
|
|
|
223
239
|
if (resolvedCmds.testCmd) {
|
|
224
|
-
|
|
240
|
+
log.step("▶", `Running Test Verification: ${resolvedCmds.testCmd}`);
|
|
225
241
|
if (!runVerification(resolvedCmds.testCmd)) {
|
|
226
242
|
testPassed = false;
|
|
227
|
-
|
|
243
|
+
log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
|
|
228
244
|
}
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
if (testPassed && resolvedCmds.buildCmd) {
|
|
232
|
-
|
|
248
|
+
log.step("▶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
|
|
233
249
|
if (!runVerification(resolvedCmds.buildCmd)) {
|
|
234
250
|
testPassed = false;
|
|
235
|
-
|
|
251
|
+
log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
|
|
240
|
-
|
|
256
|
+
log.info("No build or test scripts found. Running git status check.");
|
|
241
257
|
runCommand("git status");
|
|
242
258
|
}
|
|
243
259
|
|
|
@@ -251,17 +267,150 @@ export function runSelfAudit() {
|
|
|
251
267
|
});
|
|
252
268
|
|
|
253
269
|
if (!testPassed) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
log.error("OODA SELF-HEALING FEEDBACK LOG:");
|
|
271
|
+
log.dim(failureLog.slice(-1500));
|
|
272
|
+
|
|
273
|
+
// OODA Circuit Breaker: Fingerprint failure trace using SHA-256
|
|
274
|
+
const normalizedError = failureLog
|
|
275
|
+
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/g, "TIMESTAMP")
|
|
276
|
+
.replace(/0x[0-9a-fA-F]+/g, "HEX")
|
|
277
|
+
.replace(/\d+/g, "NUM")
|
|
278
|
+
.trim();
|
|
279
|
+
const errorHash = crypto.createHash("sha256").update(normalizedError).digest("hex");
|
|
280
|
+
|
|
281
|
+
const circuitFile = path.resolve(process.cwd(), ".agent/history/ooda-circuit.json");
|
|
282
|
+
let prevCircuit = { hash: "", count: 0 };
|
|
283
|
+
if (fs.existsSync(circuitFile)) {
|
|
284
|
+
try {
|
|
285
|
+
prevCircuit = JSON.parse(fs.readFileSync(circuitFile, "utf-8"));
|
|
286
|
+
} catch (_) {}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (prevCircuit.hash === errorHash && prevCircuit.count >= 1) {
|
|
290
|
+
log.error("❌ OODA CIRCUIT BREAKER TRIPPED: Consecutive identical failure detected.");
|
|
291
|
+
log.error("Agent generated code with identical error trace twice in a row. Aborting auto-repair.");
|
|
292
|
+
try {
|
|
293
|
+
fs.writeFileSync(circuitFile, JSON.stringify({ hash: "", count: 0 }));
|
|
294
|
+
} catch (_) {}
|
|
295
|
+
process.exit(4);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
fs.writeFileSync(
|
|
300
|
+
circuitFile,
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
hash: errorHash,
|
|
303
|
+
count: prevCircuit.hash === errorHash ? prevCircuit.count + 1 : 1,
|
|
304
|
+
updatedAt: new Date().toISOString()
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
} catch (_) {}
|
|
308
|
+
|
|
309
|
+
if (process.env.CI && !process.env.ALLOW_AUTO_REPAIR) {
|
|
310
|
+
log.error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
|
|
311
|
+
process.exit(4);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let oodaRetries = 0;
|
|
315
|
+
try {
|
|
316
|
+
const history = execSync("git log -n 5 --format=%B", { encoding: "utf-8" });
|
|
317
|
+
const matches = history.match(/OODA Auto-Repair/gi);
|
|
318
|
+
if (matches) {
|
|
319
|
+
oodaRetries = matches.length;
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
// Ignorera git-fel här
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (oodaRetries < 3) {
|
|
326
|
+
log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
|
|
327
|
+
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.`;
|
|
328
|
+
|
|
329
|
+
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
330
|
+
try {
|
|
331
|
+
const env = {
|
|
332
|
+
...process.env,
|
|
333
|
+
BASE_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch
|
|
334
|
+
};
|
|
335
|
+
execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
|
|
336
|
+
stdio: "inherit",
|
|
337
|
+
env
|
|
338
|
+
});
|
|
339
|
+
} catch (dispatchErr) {
|
|
340
|
+
log.error(`Failed to trigger OODA repair dispatch: ${dispatchErr.message}`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
process.exit(4);
|
|
257
347
|
}
|
|
258
348
|
|
|
259
|
-
|
|
349
|
+
log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function runPreflightSandbox() {
|
|
353
|
+
log.header("Initializing Pre-Flight Sandbox (Steril Simulering)...");
|
|
354
|
+
|
|
355
|
+
const runId = crypto.randomBytes(4).toString("hex");
|
|
356
|
+
const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
|
|
357
|
+
const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
log.step("[1/4]", `Skapar isolerad miljö i ${sandboxDir}...`);
|
|
361
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
362
|
+
|
|
363
|
+
execSync(`git archive HEAD -o "${tempArchive}"`);
|
|
364
|
+
execSync(`tar -xf "${tempArchive}" -C "${sandboxDir}"`);
|
|
365
|
+
|
|
366
|
+
log.step("[2/4]", "Steriliserar miljövariabler...");
|
|
367
|
+
const sterileEnv = { ...process.env };
|
|
368
|
+
const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
|
|
369
|
+
for (const key of stripKeys) {
|
|
370
|
+
if (sterileEnv[key]) {
|
|
371
|
+
sterileEnv[key] = `mock-${key.toLowerCase()}`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
log.step("[3/4]", "Löser beroenden i sandlådan...");
|
|
376
|
+
const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
|
|
377
|
+
|
|
378
|
+
if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
|
|
379
|
+
log.step("[4/4]", `Kör verifiering: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
|
|
380
|
+
execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
|
|
381
|
+
cwd: sandboxDir,
|
|
382
|
+
env: sterileEnv,
|
|
383
|
+
stdio: "inherit"
|
|
384
|
+
});
|
|
385
|
+
log.success("Pre-Flight Passed. Zero epistemic drift detected.");
|
|
386
|
+
} else {
|
|
387
|
+
log.info("Inga test/bygg-kommandon hittades för verifiering.");
|
|
388
|
+
log.success("Pre-Flight Passed (No-Op).");
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
log.error("\nPRE-FLIGHT MISSLYCKADES!");
|
|
392
|
+
log.error("Agenten skulle förmodligen krascha (The 5-Minute Drop-off) i Web UI på grund av saknade beroenden.");
|
|
393
|
+
log.error(err.message);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
} finally {
|
|
396
|
+
try {
|
|
397
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
398
|
+
if (fs.existsSync(tempArchive)) {
|
|
399
|
+
fs.rmSync(tempArchive, { force: true });
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// Ignorera fel vid cleanup
|
|
403
|
+
}
|
|
404
|
+
}
|
|
260
405
|
}
|
|
261
406
|
|
|
262
407
|
// Execute when invoked directly from CLI
|
|
263
408
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
264
|
-
|
|
409
|
+
if (process.argv.includes("--preflight")) {
|
|
410
|
+
runPreflightSandbox();
|
|
411
|
+
} else {
|
|
412
|
+
runSelfAudit();
|
|
413
|
+
}
|
|
265
414
|
}
|
|
266
415
|
|
|
267
416
|
|
|
@@ -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
|
+
}
|