jules-orchestrator-kit 0.2.9 ā 0.3.0
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/.github/workflows/publish.yml +0 -3
- package/README.md +15 -27
- package/package.json +5 -2
- package/scripts/jules-dispatch.mjs +18 -4
- package/scripts/jules-nightly.mjs +7 -1
- package/scripts/jules-queue-runner.mjs +18 -2
- package/scripts/jules-self-audit.mjs +12 -8
- package/scripts/jules-swarm.mjs +42 -3
- package/.agent/history/2026-07-27-dispatch-test-swarm-task.md +0 -9
package/README.md
CHANGED
|
@@ -54,8 +54,7 @@ Instead of blindly trusting AI code mutations, the Orchestrator acts as a strict
|
|
|
54
54
|
2. **Secret & Path Defense:** Hides passwords, API keys (`entropy > 3.6`), and blocks path traversal (`../`).
|
|
55
55
|
3. **Jules Proposes Code:** Google Jules mutates code in an isolated environment.
|
|
56
56
|
4. **Tiered Verification:** Enforces scope bounds (`git diff`), runs fast-fail linters/type-checks, then executes full test/build suites.
|
|
57
|
-
5. **Self-
|
|
58
|
-
6. **Clean Pull Request:** Once verified green, commits & pushes clean code to GitHub.
|
|
57
|
+
5. **Self-Healing PR Gatekeeper (OODA Feedback):** Parses execution stderr traces, logs diagnostic telemetry to `.agent/history/metrics.jsonl`, and enforces strict exit code 1 to block bad code from merging.
|
|
59
58
|
|
|
60
59
|
<details>
|
|
61
60
|
<summary><b>š View System Architecture Diagram (For Power Users)</b></summary>
|
|
@@ -75,33 +74,22 @@ sequenceDiagram
|
|
|
75
74
|
Orc->>Git: Provision Worktree (`git worktree add`)
|
|
76
75
|
Orc->>Jules: Dispatch Context, Invariants & Target Scope
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Orc->>Git: Run Fast-Fail Checks (Lint / Typecheck)
|
|
87
|
-
opt Pass Static Checks
|
|
88
|
-
Orc->>Git: Run Heavy Suite (`build_cmd` & `test_cmd`)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
alt Verification Gates Pass
|
|
93
|
-
Orc->>Git: Commit, Push & Cleanup Worktree
|
|
94
|
-
Orc->>CLI: Return Success + Metrics (.agent/history/metrics.jsonl)
|
|
95
|
-
note over Jules,Git: Exit Loop
|
|
96
|
-
else Verification Gates Fail (Attempts < 4)
|
|
97
|
-
Git-->>Orc: Execution Trace (stdout/stderr / Diff)
|
|
98
|
-
Orc->>Jules: Inject OODA Feedback & Error Context
|
|
99
|
-
end
|
|
77
|
+
Jules->>Git: Propose Code Mutations
|
|
78
|
+
|
|
79
|
+
note over Orc,Git: Phase 2: Tiered Verification Gatekeeper
|
|
80
|
+
Orc->>Git: Scope Audit (`git diff -z --name-only` vs forbidden_paths)
|
|
81
|
+
alt Scope Breach
|
|
82
|
+
Git-->>Orc: Scope Violation Error (Exit 1)
|
|
83
|
+
else Scope OK
|
|
84
|
+
Orc->>Git: Run Verification Suite (`test_cmd` & `build_cmd`)
|
|
100
85
|
end
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
Orc->>Git:
|
|
104
|
-
Orc->>CLI: Return
|
|
87
|
+
alt Verification Gates Pass
|
|
88
|
+
Orc->>Git: Commit, Push & Log Telemetry (.agent/history/metrics.jsonl)
|
|
89
|
+
Orc->>CLI: Return Success (Exit 0)
|
|
90
|
+
else Verification Gates Fail
|
|
91
|
+
Git-->>Orc: Execution Trace (stdout/stderr / Diff)
|
|
92
|
+
Orc->>CLI: Log OODA Diagnostic Telemetry & Block PR (Exit 1)
|
|
105
93
|
end
|
|
106
94
|
```
|
|
107
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/",
|
|
19
19
|
"scripts/",
|
|
20
|
-
".agent/",
|
|
20
|
+
".agent/jules.yml",
|
|
21
|
+
".agent/rules/",
|
|
22
|
+
".agent/workflows/",
|
|
23
|
+
".agent/jules-queue/README.md",
|
|
21
24
|
".github/",
|
|
22
25
|
".env.example",
|
|
23
26
|
"JULES_RULES_TEMPLATE.md",
|
|
@@ -28,7 +28,7 @@ function loadEnv() {
|
|
|
28
28
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
29
29
|
const eqIdx = trimmed.indexOf("=");
|
|
30
30
|
if (eqIdx > 0) {
|
|
31
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
31
|
+
const key = trimmed.slice(0, eqIdx).trim().replace(/^export\s+/, "");
|
|
32
32
|
let val = trimmed.slice(eqIdx + 1).trim();
|
|
33
33
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
34
34
|
val = val.slice(1, -1);
|
|
@@ -162,9 +162,23 @@ const repo = process.env.JULES_REPO || "";
|
|
|
162
162
|
|
|
163
163
|
if (isMainModule) {
|
|
164
164
|
const possiblePath = path.resolve(process.cwd(), taskPromptArg);
|
|
165
|
-
if (fs.existsSync(possiblePath)
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
if (fs.existsSync(possiblePath)) {
|
|
166
|
+
try {
|
|
167
|
+
const verifiedPath = assertPathWithinWorkspace(possiblePath);
|
|
168
|
+
const fd = fs.openSync(verifiedPath, "r");
|
|
169
|
+
try {
|
|
170
|
+
const stat = fs.fstatSync(fd);
|
|
171
|
+
if (stat.isFile()) {
|
|
172
|
+
rawPrompt = fs.readFileSync(fd, "utf-8");
|
|
173
|
+
} else {
|
|
174
|
+
rawPrompt = taskPromptArg;
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
fs.closeSync(fd);
|
|
178
|
+
}
|
|
179
|
+
} catch (_) {
|
|
180
|
+
rawPrompt = taskPromptArg;
|
|
181
|
+
}
|
|
168
182
|
} else {
|
|
169
183
|
rawPrompt = taskPromptArg;
|
|
170
184
|
}
|
|
@@ -110,7 +110,13 @@ function main() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
logNightlyHistory(results, isDryRun);
|
|
113
|
-
|
|
113
|
+
const hasFailures = results.some((r) => r.status && (r.status.includes("Failed") || r.status.includes("error")));
|
|
114
|
+
if (hasFailures) {
|
|
115
|
+
console.error("\nā Nightly Jules audit suite completed with task errors.");
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
} else {
|
|
118
|
+
console.log("\nā
Nightly Jules audit suite execution completed cleanly.");
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
main();
|
|
@@ -11,6 +11,8 @@ const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
|
11
11
|
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
12
12
|
const queueLogFile = path.join(queueDir, "queue.jsonl");
|
|
13
13
|
|
|
14
|
+
const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
|
|
15
|
+
|
|
14
16
|
if (!fs.existsSync(queueDir)) {
|
|
15
17
|
console.error(`ā Queue directory not found: ${queueDir}`);
|
|
16
18
|
process.exit(1);
|
|
@@ -20,6 +22,10 @@ if (!fs.existsSync(completedDir)) {
|
|
|
20
22
|
fs.mkdirSync(completedDir, { recursive: true });
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
if (!fs.existsSync(processingDir)) {
|
|
26
|
+
fs.mkdirSync(processingDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function logQueueState(file, status, error = null) {
|
|
24
30
|
const entry = JSON.stringify({
|
|
25
31
|
timestamp: new Date().toISOString(),
|
|
@@ -43,6 +49,16 @@ console.log(`š Found ${files.length} tasks in the queue. Processing...`);
|
|
|
43
49
|
|
|
44
50
|
files.forEach((file, index) => {
|
|
45
51
|
const filePath = path.join(queueDir, file);
|
|
52
|
+
const processingPath = path.join(processingDir, file);
|
|
53
|
+
|
|
54
|
+
// Atomic claim: move from queueDir to processingDir before dispatching
|
|
55
|
+
try {
|
|
56
|
+
fs.renameSync(filePath, processingPath);
|
|
57
|
+
} catch (claimErr) {
|
|
58
|
+
// Another runner already claimed this task file
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
// Generate title from filename (e.g. "TASK-001-auth-spec.md" -> "TASK 001 auth spec")
|
|
47
63
|
const title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
48
64
|
|
|
@@ -51,13 +67,13 @@ files.forEach((file, index) => {
|
|
|
51
67
|
logQueueState(file, "RUNNING");
|
|
52
68
|
|
|
53
69
|
try {
|
|
54
|
-
execFileSync("node", [dispatchScript, title,
|
|
70
|
+
execFileSync("node", [dispatchScript, title, processingPath], {
|
|
55
71
|
stdio: "inherit",
|
|
56
72
|
});
|
|
57
73
|
|
|
58
74
|
// Move to completed
|
|
59
75
|
const destPath = path.join(completedDir, file);
|
|
60
|
-
fs.renameSync(
|
|
76
|
+
fs.renameSync(processingPath, destPath);
|
|
61
77
|
logQueueState(file, "COMPLETED");
|
|
62
78
|
console.log(`ā
Moved ${file} to completed/`);
|
|
63
79
|
} catch (error) {
|
|
@@ -17,7 +17,9 @@ function runCommand(cmd, ignoreError = false) {
|
|
|
17
17
|
|
|
18
18
|
export function matchGlob(filepath, globPattern) {
|
|
19
19
|
const cleanPath = filepath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
20
|
-
|
|
20
|
+
// Normalize consecutive glob wildcards (e.g., "**/**" -> "**")
|
|
21
|
+
const normalizedGlob = globPattern.replace(/\\/g, "/").replace(/(?:\*\*\/)+/g, "**/");
|
|
22
|
+
const segments = normalizedGlob.split("/");
|
|
21
23
|
const regexParts = [];
|
|
22
24
|
for (let i = 0; i < segments.length; i++) {
|
|
23
25
|
const seg = segments[i];
|
|
@@ -33,12 +35,14 @@ export function matchGlob(filepath, globPattern) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
let patternStr = regexParts.join("/");
|
|
36
|
-
patternStr = patternStr.replace(/(
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
38
|
+
patternStr = patternStr.replace(/(?:^|\/)\*\*(?:\/|$)/g, (m) => {
|
|
39
|
+
if (m === "**") return ".*";
|
|
40
|
+
if (m === "/**/") return "(?:/|/.*/)";
|
|
41
|
+
if (m === "/**") return "(?:/.*)?";
|
|
42
|
+
if (m === "**/") return "(?:.*/)?";
|
|
40
43
|
return ".*";
|
|
41
44
|
});
|
|
45
|
+
patternStr = patternStr.replace(/\*\*/g, ".*");
|
|
42
46
|
return new RegExp(`^${patternStr}$`, "i").test(cleanPath);
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -166,9 +170,9 @@ export function runSelfAudit() {
|
|
|
166
170
|
|
|
167
171
|
console.log(`š Merge-Base Hash: ${mergeBase}`);
|
|
168
172
|
|
|
169
|
-
const rawDiffFiles = runCommand(`git diff --name-only ${mergeBase}...HEAD`)
|
|
170
|
-
.split("\
|
|
171
|
-
.map((f) => f.trim())
|
|
173
|
+
const rawDiffFiles = runCommand(`git diff -z --name-only ${mergeBase}...HEAD`)
|
|
174
|
+
.split("\0")
|
|
175
|
+
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
172
176
|
.filter(Boolean);
|
|
173
177
|
|
|
174
178
|
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -38,31 +38,70 @@ process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
|
38
38
|
|
|
39
39
|
console.log(`š Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const activeWorktrees = new Set();
|
|
42
|
+
|
|
43
|
+
function cleanupAllWorktreesSync() {
|
|
44
|
+
for (const item of Array.from(activeWorktrees)) {
|
|
45
|
+
if (item.wtDir && fs.existsSync(item.wtDir)) {
|
|
46
|
+
try {
|
|
47
|
+
fs.chmodSync(item.wtDir, 0o755);
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
if (item.branchName) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
activeWorktrees.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process.on("SIGINT", () => {
|
|
63
|
+
console.warn("\nā ļø SIGINT received. Cleaning up active Git worktrees...");
|
|
64
|
+
cleanupAllWorktreesSync();
|
|
65
|
+
process.exit(130);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
process.on("SIGTERM", () => {
|
|
69
|
+
console.warn("\nā ļø SIGTERM received. Cleaning up active Git worktrees...");
|
|
70
|
+
cleanupAllWorktreesSync();
|
|
71
|
+
process.exit(143);
|
|
72
|
+
});
|
|
42
73
|
|
|
43
74
|
async function createWorktree(taskId) {
|
|
44
75
|
const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
45
76
|
const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
|
|
46
77
|
const branchName = `jules/${slug}`;
|
|
78
|
+
let item = null;
|
|
47
79
|
try {
|
|
48
80
|
if (!fs.existsSync(path.dirname(wtDir))) {
|
|
49
81
|
fs.mkdirSync(path.dirname(wtDir), { recursive: true });
|
|
50
82
|
}
|
|
51
83
|
await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
|
|
52
|
-
|
|
84
|
+
item = { wtDir, branchName };
|
|
53
85
|
} catch (err) {
|
|
54
86
|
try {
|
|
55
87
|
await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
|
|
56
|
-
|
|
88
|
+
item = { wtDir, branchName: null };
|
|
57
89
|
} catch (fallbackErr) {
|
|
58
90
|
console.warn(`ā ļø Failed to create worktree for ${taskId}:`, fallbackErr.message);
|
|
59
91
|
return null;
|
|
60
92
|
}
|
|
61
93
|
}
|
|
94
|
+
if (item) activeWorktrees.add(item);
|
|
95
|
+
return item;
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
async function removeWorktree(wtDir, branchName) {
|
|
65
99
|
if (!wtDir) return;
|
|
100
|
+
for (const item of Array.from(activeWorktrees)) {
|
|
101
|
+
if (item.wtDir === wtDir) {
|
|
102
|
+
activeWorktrees.delete(item);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
66
105
|
try {
|
|
67
106
|
await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
|
|
68
107
|
} catch (err) {
|