jules-orchestrator-kit 0.1.9 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.1.9",
3
+ "version": "0.2.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
@@ -18,6 +18,8 @@
18
18
  "bin/",
19
19
  "scripts/",
20
20
  ".agent/",
21
+ ".github/",
22
+ ".env.example",
21
23
  "JULES_RULES_TEMPLATE.md",
22
24
  "README.md",
23
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
- // 12. Makefile
167
- if (fs.existsSync(path.join(projectRoot, "Makefile"))) {
168
- return {
169
- testCmd: "make test",
170
- buildCmd: "make build",
171
- source: "Makefile",
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
- if (affectedPkgs.size === 0) return baseCmds;
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 taskTitle = process.argv[2];
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
- if (!taskTitle || !taskPromptArg) {
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 Pre-Flight Secret Redaction Gate
43
- function redactSecrets(text) {
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
- // 1. Resolve prompt content (file path or inline string)
65
- let rawPrompt = "";
66
- const possiblePath = path.resolve(process.cwd(), taskPromptArg);
67
- if (fs.existsSync(possiblePath) && fs.statSync(possiblePath).isFile()) {
68
- rawPrompt = fs.readFileSync(possiblePath, "utf-8");
69
- } else {
70
- rawPrompt = taskPromptArg;
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
- try {
80
- const data = JSON.parse(fs.readFileSync(guardrailsPath, "utf-8"));
81
- if (!data.rules || !Array.isArray(data.rules)) return "";
82
-
83
- const active = [];
84
- for (const item of data.rules) {
85
- if (new RegExp(item.trigger, "i").test(promptText)) {
86
- active.push(item.guardrail);
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
- // 3. Dynamic Command Resolution (Language & Framework Detection)
96
- const resolvedCmds = resolveProjectCommands(process.cwd());
97
- const verifyDirective = resolvedCmds.testCmd || resolvedCmds.buildCmd
98
- ? `\n\n## đŸ› ī¸ MANDATORY VERIFICATION COMMANDS\nBefore submitting PR, you MUST execute: \`${[resolvedCmds.testCmd, resolvedCmds.buildCmd].filter(Boolean).join(" && ")}\``
99
- : "";
100
-
101
- // 4. Load Base Rules / Guardrails
102
- const rulesPath = fs.existsSync(path.resolve(process.cwd(), "AGENTS.md"))
103
- ? path.resolve(process.cwd(), "AGENTS.md")
104
- : path.resolve(process.cwd(), "JULES_RULES_TEMPLATE.md");
105
-
106
- const baseRules = fs.existsSync(rulesPath)
107
- ? fs.readFileSync(rulesPath, "utf-8")
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
- console.log(`🚀 Dispatching task to Google Jules: "${taskTitle}"...`);
138
- console.log(`📝 Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
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
- // 6. Dispatch via REST API or Ephemeral Payload File (Avoid ARG_MAX shell limits)
141
- const apiKey = process.env.JULES_API_KEY;
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
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
146
- const tmpPayloadFile = path.join(tmpDir, `payload_${payloadHash}.json`);
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
- res = await fetch("https://jules.googleapis.com/v1alpha/sessions", {
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
- console.error("❌ Fatal unhandled rejection in dispatch:", err);
245
- process.exit(1);
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", ["scripts/jules-dispatch.mjs", title, filePath], {
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
  });