jules-orchestrator-kit 0.1.2 â 0.2.8
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/history/2026-07-27-dispatch-test-swarm-task.md +9 -0
- package/.agent/jules-queue/README.md +8 -1
- package/.env.example +17 -0
- package/.github/workflows/jules-audit.yml +32 -0
- package/.github/workflows/jules-nightly.yml +21 -0
- package/.github/workflows/publish.yml +24 -0
- package/JULES_RULES_TEMPLATE.md +7 -3
- package/README.md +200 -73
- package/bin/init.js +93 -7
- package/package.json +11 -2
- package/scripts/command-resolver.mjs +88 -131
- package/scripts/jules-dispatch.mjs +162 -93
- package/scripts/jules-queue-runner.mjs +20 -2
- package/scripts/jules-self-audit.mjs +72 -5
- package/scripts/jules-swarm.mjs +85 -7
- package/.agent/history/2026-07-27-dispatch-nightly-accessibility-aria-audit.md +0 -12
- package/.agent/history/2026-07-27-dispatch-nightly-dead-code-unused-exports-audit.md +0 -12
- package/.agent/history/2026-07-27-dispatch-nightly-security-secret-leaks-audit.md +0 -12
- package/.agent/history/2026-07-27-dispatch-nightly-unused-environment-variable-audit.md +0 -12
- package/.agent/history/2026-07-27-nightly-audit.md +0 -13
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
|
|
7
|
+
},
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/FullThrottle83/jules-orchestrator-kit/issues"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/FullThrottle83/jules-orchestrator-kit#readme",
|
|
5
12
|
"type": "module",
|
|
6
13
|
"bin": {
|
|
7
14
|
"jules-init": "bin/init.js",
|
|
@@ -11,6 +18,8 @@
|
|
|
11
18
|
"bin/",
|
|
12
19
|
"scripts/",
|
|
13
20
|
".agent/",
|
|
21
|
+
".github/",
|
|
22
|
+
".env.example",
|
|
14
23
|
"JULES_RULES_TEMPLATE.md",
|
|
15
24
|
"README.md",
|
|
16
25
|
"LICENSE"
|
|
@@ -39,137 +39,90 @@ export function resolveProjectCommands(projectRoot = process.cwd()) {
|
|
|
39
39
|
buildCmd: cfg.buildCmd || "",
|
|
40
40
|
source: "jules.config.json",
|
|
41
41
|
};
|
|
42
|
-
} catch (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// 2. Bun (bunfig.toml or bun.lockb)
|
|
46
|
-
if (
|
|
47
|
-
fs.existsSync(path.join(projectRoot, "bunfig.toml")) ||
|
|
48
|
-
fs.existsSync(path.join(projectRoot, "bun.lockb"))
|
|
49
|
-
) {
|
|
50
|
-
return {
|
|
51
|
-
testCmd: "bun test",
|
|
52
|
-
buildCmd: "bun run build",
|
|
53
|
-
source: "Bun Manifest",
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 3. Deno (deno.json / deno.jsonc)
|
|
58
|
-
if (
|
|
59
|
-
fs.existsSync(path.join(projectRoot, "deno.json")) ||
|
|
60
|
-
fs.existsSync(path.join(projectRoot, "deno.jsonc"))
|
|
61
|
-
) {
|
|
62
|
-
return {
|
|
63
|
-
testCmd: "deno test",
|
|
64
|
-
buildCmd: "deno task build",
|
|
65
|
-
source: "Deno Manifest",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 4. JavaScript / TypeScript (package.json)
|
|
70
|
-
const pkgPath = path.join(projectRoot, "package.json");
|
|
71
|
-
if (fs.existsSync(pkgPath)) {
|
|
72
|
-
try {
|
|
73
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
74
|
-
const scripts = pkg.scripts || {};
|
|
75
|
-
const verifyScript = scripts["check:all"]
|
|
76
|
-
? "npm run check:all"
|
|
77
|
-
: scripts.test
|
|
78
|
-
? "npm test"
|
|
79
|
-
: "";
|
|
80
|
-
const buildScript = scripts.build ? "npm run build" : "";
|
|
81
|
-
return {
|
|
82
|
-
testCmd: verifyScript,
|
|
83
|
-
buildCmd: buildScript,
|
|
84
|
-
source: "package.json",
|
|
85
|
-
};
|
|
86
|
-
} catch (_) {}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 5. Rust (Cargo.toml)
|
|
90
|
-
if (fs.existsSync(path.join(projectRoot, "Cargo.toml"))) {
|
|
91
|
-
return {
|
|
92
|
-
testCmd: "cargo test --workspace",
|
|
93
|
-
buildCmd: "cargo build",
|
|
94
|
-
source: "Cargo.toml",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 6. Go (go.mod)
|
|
99
|
-
if (fs.existsSync(path.join(projectRoot, "go.mod"))) {
|
|
100
|
-
return {
|
|
101
|
-
testCmd: "go test ./...",
|
|
102
|
-
buildCmd: "go build ./...",
|
|
103
|
-
source: "go.mod",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 7. Python (pyproject.toml / requirements.txt / setup.py)
|
|
108
|
-
if (
|
|
109
|
-
fs.existsSync(path.join(projectRoot, "pyproject.toml")) ||
|
|
110
|
-
fs.existsSync(path.join(projectRoot, "requirements.txt")) ||
|
|
111
|
-
fs.existsSync(path.join(projectRoot, "setup.py"))
|
|
112
|
-
) {
|
|
113
|
-
return {
|
|
114
|
-
testCmd: "pytest",
|
|
115
|
-
buildCmd: "",
|
|
116
|
-
source: "Python Manifest",
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 8. Elixir (mix.exs)
|
|
121
|
-
if (fs.existsSync(path.join(projectRoot, "mix.exs"))) {
|
|
122
|
-
return {
|
|
123
|
-
testCmd: "mix test",
|
|
124
|
-
buildCmd: "mix compile",
|
|
125
|
-
source: "mix.exs",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 9. Ruby (Gemfile)
|
|
130
|
-
if (fs.existsSync(path.join(projectRoot, "Gemfile"))) {
|
|
131
|
-
return {
|
|
132
|
-
testCmd: "bundle exec rake test",
|
|
133
|
-
buildCmd: "",
|
|
134
|
-
source: "Gemfile",
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 10. Swift (Package.swift)
|
|
139
|
-
if (fs.existsSync(path.join(projectRoot, "Package.swift"))) {
|
|
140
|
-
return {
|
|
141
|
-
testCmd: "swift test",
|
|
142
|
-
buildCmd: "swift build",
|
|
143
|
-
source: "Package.swift",
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 11. Java (Maven pom.xml / Gradle build.gradle)
|
|
148
|
-
if (fs.existsSync(path.join(projectRoot, "pom.xml"))) {
|
|
149
|
-
return {
|
|
150
|
-
testCmd: "mvn test",
|
|
151
|
-
buildCmd: "mvn compile",
|
|
152
|
-
source: "pom.xml",
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
if (
|
|
156
|
-
fs.existsSync(path.join(projectRoot, "build.gradle")) ||
|
|
157
|
-
fs.existsSync(path.join(projectRoot, "build.gradle.kts"))
|
|
158
|
-
) {
|
|
159
|
-
return {
|
|
160
|
-
testCmd: "./gradlew test",
|
|
161
|
-
buildCmd: "./gradlew assemble",
|
|
162
|
-
source: "build.gradle",
|
|
163
|
-
};
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn("â ī¸ Failed to parse jules.config.json:", err.message);
|
|
44
|
+
}
|
|
164
45
|
}
|
|
165
46
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
buildCmd: "
|
|
171
|
-
|
|
172
|
-
|
|
47
|
+
// 2. Dynamic Detector Strategy List
|
|
48
|
+
const detectors = [
|
|
49
|
+
{
|
|
50
|
+
files: ["bunfig.toml", "bun.lockb"],
|
|
51
|
+
resolve: () => ({ testCmd: "bun test", buildCmd: "bun run build", source: "Bun Manifest" }),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
files: ["deno.json", "deno.jsonc"],
|
|
55
|
+
resolve: () => ({ testCmd: "deno test", buildCmd: "deno task build", source: "Deno Manifest" }),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
files: ["package.json"],
|
|
59
|
+
resolve: (root) => {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
|
|
62
|
+
const scripts = pkg.scripts || {};
|
|
63
|
+
let verifyScript = scripts["check:all"] ? "npm run check:all" : "";
|
|
64
|
+
if (!verifyScript) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (scripts.lint) parts.push("npm run lint");
|
|
67
|
+
else if (scripts.check) parts.push("npm run check");
|
|
68
|
+
if (scripts.test) parts.push("npm test");
|
|
69
|
+
verifyScript = parts.join(" && ");
|
|
70
|
+
}
|
|
71
|
+
const buildScript = scripts.build ? "npm run build" : "";
|
|
72
|
+
return {
|
|
73
|
+
testCmd: verifyScript || "npm test",
|
|
74
|
+
buildCmd: buildScript,
|
|
75
|
+
source: "package.json",
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn("â ī¸ Failed to parse package.json:", err.message);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
files: ["Cargo.toml"],
|
|
85
|
+
resolve: () => ({ testCmd: "cargo test --workspace", buildCmd: "cargo build", source: "Cargo.toml" }),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
files: ["go.mod"],
|
|
89
|
+
resolve: () => ({ testCmd: "go test ./...", buildCmd: "go build ./...", source: "go.mod" }),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
files: ["pyproject.toml", "requirements.txt", "setup.py"],
|
|
93
|
+
resolve: () => ({ testCmd: "pytest", buildCmd: "", source: "Python Manifest" }),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
files: ["mix.exs"],
|
|
97
|
+
resolve: () => ({ testCmd: "mix test", buildCmd: "mix compile", source: "mix.exs" }),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
files: ["Gemfile"],
|
|
101
|
+
resolve: () => ({ testCmd: "bundle exec rake test", buildCmd: "", source: "Gemfile" }),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
files: ["Package.swift"],
|
|
105
|
+
resolve: () => ({ testCmd: "swift test", buildCmd: "swift build", source: "Package.swift" }),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
files: ["pom.xml"],
|
|
109
|
+
resolve: () => ({ testCmd: "mvn test", buildCmd: "mvn compile", source: "pom.xml" }),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
files: ["build.gradle", "build.gradle.kts"],
|
|
113
|
+
resolve: () => ({ testCmd: "./gradlew test", buildCmd: "./gradlew assemble", source: "build.gradle" }),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
files: ["Makefile"],
|
|
117
|
+
resolve: () => ({ testCmd: "make test", buildCmd: "make build", source: "Makefile" }),
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const detector of detectors) {
|
|
122
|
+
if (detector.files.some((f) => fs.existsSync(path.join(projectRoot, f)))) {
|
|
123
|
+
const res = detector.resolve(projectRoot);
|
|
124
|
+
if (res) return res;
|
|
125
|
+
}
|
|
173
126
|
}
|
|
174
127
|
|
|
175
128
|
return {
|
|
@@ -185,6 +138,9 @@ export function resolveProjectCommands(projectRoot = process.cwd()) {
|
|
|
185
138
|
*/
|
|
186
139
|
export function resolveWorkspaceExecutionBoundary(modifiedFiles = [], projectRoot = process.cwd()) {
|
|
187
140
|
const baseCmds = resolveProjectCommands(projectRoot);
|
|
141
|
+
if (baseCmds.source === ".agent/jules.yml" || baseCmds.source === "jules.config.json") {
|
|
142
|
+
return baseCmds;
|
|
143
|
+
}
|
|
188
144
|
if (!modifiedFiles || modifiedFiles.length === 0) return baseCmds;
|
|
189
145
|
|
|
190
146
|
// Find affected package names by walking up from modified files to nearest manifest
|
|
@@ -211,8 +167,9 @@ export function resolveWorkspaceExecutionBoundary(modifiedFiles = [], projectRoo
|
|
|
211
167
|
}
|
|
212
168
|
}
|
|
213
169
|
|
|
214
|
-
|
|
215
|
-
const targets = Array.from(affectedPkgs);
|
|
170
|
+
const SAFE_PKG_NAME = /^[@a-zA-Z0-9._\/-]+$/;
|
|
171
|
+
const targets = Array.from(affectedPkgs).filter((t) => SAFE_PKG_NAME.test(t));
|
|
172
|
+
if (targets.length === 0) return baseCmds;
|
|
216
173
|
|
|
217
174
|
// 1. Turborepo (turbo.json)
|
|
218
175
|
if (fs.existsSync(path.join(projectRoot, "turbo.json"))) {
|
|
@@ -3,12 +3,15 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import os from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { resolveProjectCommands } from "./command-resolver.mjs";
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const taskPromptArg = process.argv[3];
|
|
9
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const taskTitle = isMainModule ? process.argv[2] : "";
|
|
12
|
+
const taskPromptArg = isMainModule ? process.argv[3] : "";
|
|
13
|
+
|
|
14
|
+
if (isMainModule && (!taskTitle || !taskPromptArg)) {
|
|
12
15
|
console.error(
|
|
13
16
|
'Usage: node scripts/jules-dispatch.mjs <task-title> <path-to-prompt.md | "raw prompt string">'
|
|
14
17
|
);
|
|
@@ -39,9 +42,41 @@ function loadEnv() {
|
|
|
39
42
|
}
|
|
40
43
|
loadEnv();
|
|
41
44
|
|
|
42
|
-
// 0.1
|
|
43
|
-
function
|
|
45
|
+
// 0.1 Shannon Entropy Calculator
|
|
46
|
+
export function calculateEntropy(str) {
|
|
47
|
+
if (!str) return 0;
|
|
48
|
+
const len = str.length;
|
|
49
|
+
const frequencies = {};
|
|
50
|
+
for (let i = 0; i < len; i++) {
|
|
51
|
+
const char = str[i];
|
|
52
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
53
|
+
}
|
|
54
|
+
let entropy = 0;
|
|
55
|
+
for (const char in frequencies) {
|
|
56
|
+
const p = frequencies[char] / len;
|
|
57
|
+
entropy -= p * Math.log2(p);
|
|
58
|
+
}
|
|
59
|
+
return entropy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 0.2 Pre-Flight Secret Redaction Gate (RegEx + Shannon Entropy + Env Denylist)
|
|
63
|
+
export function redactSecrets(text) {
|
|
44
64
|
if (!text) return "";
|
|
65
|
+
let sanitized = text;
|
|
66
|
+
|
|
67
|
+
// Redact active env secrets from denylist keys (*_KEY, *_SECRET, *_TOKEN, etc.)
|
|
68
|
+
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
69
|
+
if (
|
|
70
|
+
envVal &&
|
|
71
|
+
envVal.length >= 6 &&
|
|
72
|
+
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH/i.test(envKey)
|
|
73
|
+
) {
|
|
74
|
+
if (sanitized.includes(envVal)) {
|
|
75
|
+
sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
const patterns = [
|
|
46
81
|
/gh[pusr]_[a-zA-Z0-9]{36}/g,
|
|
47
82
|
/github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
|
|
@@ -52,99 +87,123 @@ function redactSecrets(text) {
|
|
|
52
87
|
/sk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}/g,
|
|
53
88
|
/xox[baprs]-[a-zA-Z0-9\-]{10,}/g,
|
|
54
89
|
/eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g,
|
|
55
|
-
/-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY
|
|
90
|
+
/-----BEGIN (?:RSA|OPENSSH|EC|PRIVATE) KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PRIVATE) KEY-----/g,
|
|
56
91
|
];
|
|
57
|
-
let sanitized = text;
|
|
58
92
|
for (const pat of patterns) {
|
|
59
93
|
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
60
94
|
}
|
|
95
|
+
// Exclude file paths (tokens with slashes or common extensions) from entropy redaction
|
|
96
|
+
sanitized = sanitized.replace(/[a-zA-Z0-9_\-\.]{20,}/g, (token) => {
|
|
97
|
+
if (
|
|
98
|
+
token.startsWith("[REDACTED_") ||
|
|
99
|
+
token.includes("/") ||
|
|
100
|
+
/\.(?:ts|js|jsx|tsx|json|md|py|rs|go|yml|yaml|toml|html|css|sh|config)$/i.test(token)
|
|
101
|
+
) {
|
|
102
|
+
return token;
|
|
103
|
+
}
|
|
104
|
+
const entropy = calculateEntropy(token);
|
|
105
|
+
if (entropy > 3.6) {
|
|
106
|
+
return "[REDACTED_ENTROPY_KEY]";
|
|
107
|
+
}
|
|
108
|
+
return token;
|
|
109
|
+
});
|
|
61
110
|
return sanitized;
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
// 0.3 Hardened Path Traversal & Symlink Defense
|
|
114
|
+
export function assertPathWithinWorkspace(targetPath, workspaceRoot = process.cwd()) {
|
|
115
|
+
const rootReal = fs.existsSync(workspaceRoot) ? fs.realpathSync(path.resolve(workspaceRoot)) : path.resolve(workspaceRoot);
|
|
116
|
+
const resolvedTarget = path.resolve(rootReal, targetPath);
|
|
117
|
+
const targetReal = fs.existsSync(resolvedTarget) ? fs.realpathSync(resolvedTarget) : resolvedTarget;
|
|
118
|
+
const normalizedRoot = rootReal.replace(/\\/g, "/");
|
|
119
|
+
const normalizedTarget = targetReal.replace(/\\/g, "/");
|
|
120
|
+
if (!normalizedTarget.startsWith(normalizedRoot)) {
|
|
121
|
+
throw new Error(`FATAL: Sandboxed directory traversal breach blocked for path: ${targetPath}`);
|
|
122
|
+
}
|
|
123
|
+
return targetReal;
|
|
71
124
|
}
|
|
72
|
-
rawPrompt = redactSecrets(rawPrompt);
|
|
73
|
-
|
|
74
|
-
// 2. Dynamic Guardrail Composition (DGC) from .agent/rules/dynamic-guardrails.json
|
|
75
|
-
function getDynamicGuardrails(promptText) {
|
|
76
|
-
const guardrailsPath = path.resolve(process.cwd(), ".agent/rules/dynamic-guardrails.json");
|
|
77
|
-
if (!fs.existsSync(guardrailsPath)) return "";
|
|
78
125
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return active.join("\n\n");
|
|
90
|
-
} catch (e) {
|
|
91
|
-
return "";
|
|
126
|
+
// 0.4 Dynamic Guardrails & Directive Definitions
|
|
127
|
+
export function getDynamicGuardrails(prompt = "") {
|
|
128
|
+
const guardrails = [];
|
|
129
|
+
if (/\b(?:astro|components|pages|src\/.*\.astro)\b/i.test(prompt)) {
|
|
130
|
+
guardrails.push("- Astro Guidance: Ensure zero client JS shipped by default. Use server islands or nano stores if state is required.");
|
|
131
|
+
}
|
|
132
|
+
if (/\b(?:db|database|d1|postgres|drizzle|migration|schema)\b/i.test(prompt)) {
|
|
133
|
+
guardrails.push("- Database Guidance: Do not modify migrations directly without inspecting current schema constraints.");
|
|
92
134
|
}
|
|
135
|
+
return guardrails.length > 0 ? `## Context-Specific Guardrails\n${guardrails.join("\n")}` : "";
|
|
93
136
|
}
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
: "";
|
|
109
|
-
|
|
110
|
-
const dynamicRules = getDynamicGuardrails(rawPrompt);
|
|
111
|
-
|
|
112
|
-
// Machine XML Directives
|
|
113
|
-
const envelope = `
|
|
114
|
-
<MCP_DIRECTIVE>
|
|
115
|
-
<system_state>HEADLESS_CI_MODE</system_state>
|
|
116
|
-
<strict_invariants>
|
|
117
|
-
<rule>1. READ-BEFORE-WRITE (ZERO HALLUCINATION): You are FORBIDDEN from guessing internal API signatures. Before editing, use search tools or MCP doc tools to verify exact signatures.</rule>
|
|
118
|
-
<rule>2. VERIFICATION LOOP: After patching code, you MUST execute the verification commands and ensure 0 errors.</rule>
|
|
119
|
-
<rule>3. ABORT CONDITION: On repeated unresolvable test failures, output <status>ABORT_UNRESOLVABLE</status> and terminate.</rule>
|
|
120
|
-
</strict_invariants>
|
|
121
|
-
</MCP_DIRECTIVE>
|
|
122
|
-
`.trim();
|
|
123
|
-
|
|
124
|
-
const fullPrompt = redactSecrets(`MCP DIRECTIVE: ${rawPrompt.trim()}${verifyDirective}\n\n---\n\n${envelope}\n\n---\n\n${dynamicRules ? `${dynamicRules}\n\n---\n\n` : ""}${baseRules.trim()}`);
|
|
125
|
-
|
|
126
|
-
// 5. Log Dispatch History
|
|
127
|
-
const dateStr = new Date().toISOString().split("T")[0];
|
|
128
|
-
const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
129
|
-
|
|
130
|
-
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
131
|
-
if (!fs.existsSync(historyDir)) {
|
|
132
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
138
|
+
function getBaseRules(projectRoot = process.cwd()) {
|
|
139
|
+
const templatePath = path.join(projectRoot, "JULES_RULES_TEMPLATE.md");
|
|
140
|
+
if (fs.existsSync(templatePath)) {
|
|
141
|
+
return fs.readFileSync(templatePath, "utf-8");
|
|
142
|
+
}
|
|
143
|
+
const agentsPath = path.join(projectRoot, "AGENTS.md");
|
|
144
|
+
if (fs.existsSync(agentsPath)) {
|
|
145
|
+
return fs.readFileSync(agentsPath, "utf-8");
|
|
146
|
+
}
|
|
147
|
+
return `## Operational Directives
|
|
148
|
+
- **Read Before Write**: Always inspect target files and symbol definitions before modifying code.
|
|
149
|
+
- **Verification**: Execute test and build verification suite and ensure 0 errors.
|
|
150
|
+
- **Anti-Patch Invariant**: Do NOT create out-of-band shell scripts (patch.sh, test-fix.sh) or disable assertions to force tests to pass.`;
|
|
133
151
|
}
|
|
134
|
-
const historyFile = path.join(historyDir, `${dateStr}-dispatch-${slug}.md`);
|
|
135
|
-
fs.writeFileSync(historyFile, `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`, "utf-8");
|
|
136
152
|
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
// 1. Resolve prompt content and construct directive envelope
|
|
154
|
+
let rawPrompt = "";
|
|
155
|
+
let fullPrompt = "";
|
|
156
|
+
let historyFile = "";
|
|
157
|
+
let tmpPayloadFile = "";
|
|
158
|
+
let tmpDir = "";
|
|
159
|
+
|
|
160
|
+
const apiKey = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
|
|
161
|
+
const repo = process.env.JULES_REPO || "";
|
|
162
|
+
|
|
163
|
+
if (isMainModule) {
|
|
164
|
+
const possiblePath = path.resolve(process.cwd(), taskPromptArg);
|
|
165
|
+
if (fs.existsSync(possiblePath) && fs.statSync(possiblePath).isFile()) {
|
|
166
|
+
assertPathWithinWorkspace(possiblePath);
|
|
167
|
+
rawPrompt = fs.readFileSync(possiblePath, "utf-8");
|
|
168
|
+
} else {
|
|
169
|
+
rawPrompt = taskPromptArg;
|
|
170
|
+
}
|
|
171
|
+
rawPrompt = redactSecrets(rawPrompt);
|
|
172
|
+
|
|
173
|
+
const dynamicRules = getDynamicGuardrails(rawPrompt);
|
|
174
|
+
const baseRules = getBaseRules(process.cwd());
|
|
175
|
+
const envelope = `## Envelope Directives
|
|
176
|
+
- **Zero Hallucination:** Inspect exact symbol definitions before editing.
|
|
177
|
+
- **Verification:** Execute project verification suite and ensure 0 errors.
|
|
178
|
+
- **Anti-Patch / Zero Out-of-band Scripts:** Do NOT create workaround runner scripts (e.g. patch.sh, test-fix.sh) or disable assertions to pass tests.`;
|
|
179
|
+
|
|
180
|
+
const { testCmd, buildCmd } = resolveProjectCommands(process.cwd());
|
|
181
|
+
const verifyDirective = testCmd || buildCmd
|
|
182
|
+
? `\n\nVERIFICATION DIRECTIVE: Execute \`${[testCmd, buildCmd].filter(Boolean).join(" && ")}\` after patching.`
|
|
183
|
+
: "";
|
|
184
|
+
|
|
185
|
+
fullPrompt = redactSecrets(`MCP DIRECTIVE: ${rawPrompt.trim()}${verifyDirective}\n\n---\n\n${envelope}\n\n---\n\n${dynamicRules ? `${dynamicRules}\n\n---\n\n` : ""}${baseRules.trim()}`);
|
|
186
|
+
|
|
187
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
188
|
+
const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
189
|
+
|
|
190
|
+
// Resolve history directory relative to main workspace root, avoiding temporary worktrees
|
|
191
|
+
const mainWorkspaceRoot = process.env.JULES_PROJECT_ROOT || process.env.INIT_CWD || process.cwd();
|
|
192
|
+
const historyDir = path.resolve(mainWorkspaceRoot, ".agent/history");
|
|
193
|
+
if (!fs.existsSync(historyDir)) {
|
|
194
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
historyFile = path.join(historyDir, `${dateStr}-dispatch-${slug}.md`);
|
|
197
|
+
fs.writeFileSync(historyFile, `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`, "utf-8");
|
|
139
198
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const repo = process.env.JULES_REPO;
|
|
199
|
+
console.log(`đ Dispatching task to Google Jules: "${taskTitle}"...`);
|
|
200
|
+
console.log(`đ Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
|
|
143
201
|
|
|
144
|
-
const payloadHash = crypto.createHash("sha256").update(taskTitle + Date.now()).digest("hex").slice(0, 12);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
|
|
202
|
+
const payloadHash = crypto.createHash("sha256").update(taskTitle + Date.now()).digest("hex").slice(0, 12);
|
|
203
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
|
|
204
|
+
tmpPayloadFile = path.join(tmpDir, `payload_${payloadHash}.json`);
|
|
205
|
+
fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
|
|
206
|
+
}
|
|
148
207
|
|
|
149
208
|
function cleanupTmp() {
|
|
150
209
|
try {
|
|
@@ -156,6 +215,13 @@ function cleanupTmp() {
|
|
|
156
215
|
|
|
157
216
|
async function executeDispatch() {
|
|
158
217
|
try {
|
|
218
|
+
if (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1") {
|
|
219
|
+
console.log(`[DRY RUN] Dispatch payload prepared successfully for task: "${taskTitle}".`);
|
|
220
|
+
console.log(`[DRY RUN] Target Repository: ${repo || "(default)"}`);
|
|
221
|
+
console.log(`[DRY RUN] Prompt Length: ${fullPrompt.length} chars.`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
159
225
|
if (apiKey && repo) {
|
|
160
226
|
console.log(`đ Using Jules REST API for repository ${repo}...`);
|
|
161
227
|
const formattedSource = repo.startsWith("sources/") ? repo : (repo.includes("/") ? `sources/github/${repo}` : repo);
|
|
@@ -172,7 +238,8 @@ async function executeDispatch() {
|
|
|
172
238
|
|
|
173
239
|
let res;
|
|
174
240
|
try {
|
|
175
|
-
|
|
241
|
+
const apiUrl = process.env.JULES_API_URL || "https://jules.googleapis.com/v1alpha/sessions";
|
|
242
|
+
res = await fetch(apiUrl, {
|
|
176
243
|
method: "POST",
|
|
177
244
|
headers: {
|
|
178
245
|
"X-Goog-Api-Key": apiKey,
|
|
@@ -202,7 +269,9 @@ async function executeDispatch() {
|
|
|
202
269
|
try {
|
|
203
270
|
const data = await res.json();
|
|
204
271
|
sessionId = data.name || data.id || sessionId;
|
|
205
|
-
} catch (
|
|
272
|
+
} catch (jsonErr) {
|
|
273
|
+
console.warn("â ī¸ Failed to parse REST API JSON response:", jsonErr.message);
|
|
274
|
+
}
|
|
206
275
|
|
|
207
276
|
console.log(`â
REST API Dispatch response: Session ${sessionId} created successfully.`);
|
|
208
277
|
fs.appendFileSync(historyFile, `\n## Session\n- ID: \`${sessionId}\`\n`);
|
|
@@ -214,7 +283,6 @@ async function executeDispatch() {
|
|
|
214
283
|
}
|
|
215
284
|
}
|
|
216
285
|
|
|
217
|
-
|
|
218
286
|
function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
219
287
|
try {
|
|
220
288
|
let promptToSend = fullPrompt;
|
|
@@ -222,7 +290,9 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
|
222
290
|
try {
|
|
223
291
|
const payloadData = JSON.parse(fs.readFileSync(payloadFile, "utf-8"));
|
|
224
292
|
if (payloadData.prompt) promptToSend = payloadData.prompt;
|
|
225
|
-
} catch (
|
|
293
|
+
} catch (jsonErr) {
|
|
294
|
+
console.warn("â ī¸ Failed to parse payload JSON file:", jsonErr.message);
|
|
295
|
+
}
|
|
226
296
|
}
|
|
227
297
|
|
|
228
298
|
const args = ["new"];
|
|
@@ -239,10 +309,9 @@ function dispatchViaCli(targetRepo, payloadFile, title) {
|
|
|
239
309
|
}
|
|
240
310
|
}
|
|
241
311
|
|
|
242
|
-
|
|
243
|
-
executeDispatch().catch((err) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
|
|
312
|
+
if (isMainModule) {
|
|
313
|
+
executeDispatch().catch((err) => {
|
|
314
|
+
console.error("â Fatal unhandled rejection in dispatch:", err);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { execFileSync } from "node:child_process";
|
|
4
5
|
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const dispatchScript = path.resolve(__dirname, "jules-dispatch.mjs");
|
|
9
|
+
|
|
5
10
|
const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
6
11
|
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
12
|
+
const queueLogFile = path.join(queueDir, "queue.jsonl");
|
|
7
13
|
|
|
8
14
|
if (!fs.existsSync(queueDir)) {
|
|
9
15
|
console.error(`â Queue directory not found: ${queueDir}`);
|
|
@@ -14,11 +20,20 @@ if (!fs.existsSync(completedDir)) {
|
|
|
14
20
|
fs.mkdirSync(completedDir, { recursive: true });
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
function logQueueState(file, status, error = null) {
|
|
24
|
+
const entry = JSON.stringify({
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
file,
|
|
27
|
+
status,
|
|
28
|
+
error: error ? error.message || String(error) : null,
|
|
29
|
+
}) + "\n";
|
|
30
|
+
fs.appendFileSync(queueLogFile, entry, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
const files = fs.readdirSync(queueDir).filter(
|
|
18
34
|
(f) => f.endsWith(".md") && f !== "README.md" && !f.startsWith("_") && !f.startsWith(".")
|
|
19
35
|
);
|
|
20
36
|
|
|
21
|
-
|
|
22
37
|
if (files.length === 0) {
|
|
23
38
|
console.log("âšī¸ No tasks found in the queue.");
|
|
24
39
|
process.exit(0);
|
|
@@ -33,17 +48,20 @@ files.forEach((file, index) => {
|
|
|
33
48
|
|
|
34
49
|
console.log(`\n----------------------------------------`);
|
|
35
50
|
console.log(`[${index + 1}/${files.length}] Dispatching queued task: ${title}`);
|
|
51
|
+
logQueueState(file, "RUNNING");
|
|
36
52
|
|
|
37
53
|
try {
|
|
38
|
-
execFileSync("node", [
|
|
54
|
+
execFileSync("node", [dispatchScript, title, filePath], {
|
|
39
55
|
stdio: "inherit",
|
|
40
56
|
});
|
|
41
57
|
|
|
42
58
|
// Move to completed
|
|
43
59
|
const destPath = path.join(completedDir, file);
|
|
44
60
|
fs.renameSync(filePath, destPath);
|
|
61
|
+
logQueueState(file, "COMPLETED");
|
|
45
62
|
console.log(`â
Moved ${file} to completed/`);
|
|
46
63
|
} catch (error) {
|
|
64
|
+
logQueueState(file, "FAILED", error);
|
|
47
65
|
console.error(`â ī¸ Failed to dispatch task [${title}]:`, error.message);
|
|
48
66
|
}
|
|
49
67
|
});
|