jules-orchestrator-kit 0.8.1 → 0.9.3

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.
Files changed (50) hide show
  1. package/.agent/jules-queue/README.md +4 -4
  2. package/.agent/prompts/Bolt.md +18 -0
  3. package/.agent/prompts/Overseer.md +18 -0
  4. package/.agent/prompts/Sentinel.md +18 -0
  5. package/.agent/prompts/Task_Template.md +26 -0
  6. package/.agent/rules/dynamic-guardrails.json +0 -4
  7. package/.github/workflows/jules-audit.yml +16 -5
  8. package/JULES_RULES_TEMPLATE.md +7 -2
  9. package/README.md +113 -21
  10. package/bin/agentctl.mjs +274 -0
  11. package/bin/init.js +1 -1
  12. package/index.mjs +34 -6
  13. package/package.json +22 -6
  14. package/scripts/asset-integrity-check.mjs +35 -0
  15. package/scripts/command-resolver.mjs +37 -214
  16. package/scripts/jules-cleanup.mjs +23 -174
  17. package/scripts/jules-create.mjs +12 -51
  18. package/scripts/jules-dispatch.mjs +103 -349
  19. package/scripts/jules-merge-swarm.mjs +30 -0
  20. package/scripts/jules-nightly.mjs +7 -124
  21. package/scripts/jules-patch.mjs +23 -0
  22. package/scripts/jules-queue-runner.mjs +28 -203
  23. package/scripts/jules-scan-todos.mjs +31 -125
  24. package/scripts/jules-self-audit.mjs +76 -578
  25. package/scripts/jules-status.mjs +42 -65
  26. package/scripts/jules-swarm.mjs +27 -232
  27. package/scripts/jules-webhook-receiver.mjs +70 -0
  28. package/scripts/lock-manager.mjs +11 -170
  29. package/scripts/risk-tier.mjs +29 -0
  30. package/scripts/rules-lint.mjs +20 -0
  31. package/scripts/stale-base-check.mjs +26 -0
  32. package/scripts/utils.mjs +118 -252
  33. package/scripts/validate-envelope.mjs +44 -0
  34. package/src/asset_integrity.mjs +76 -0
  35. package/src/config.mjs +325 -0
  36. package/src/engine.mjs +217 -0
  37. package/src/envelope.mjs +105 -0
  38. package/src/git.mjs +127 -0
  39. package/src/provider.mjs +179 -0
  40. package/src/risk.mjs +120 -0
  41. package/src/rules_budget.mjs +77 -0
  42. package/src/security.mjs +212 -0
  43. package/src/state.mjs +209 -0
  44. package/src/webhook.mjs +138 -0
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
  46. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
  47. package/.github/social-preview.png +0 -0
  48. package/.github/workflows/agent-scope-guard.yml +0 -57
  49. package/.github/workflows/jules-nightly.yml +0 -21
  50. package/.github/workflows/publish.yml +0 -24
@@ -1,67 +1,28 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
- * Jules Task Scaffolding Utility
4
- * Quickly creates a boilerplate markdown task in .agent/jules-queue/
4
+ * Backward compatibility shim for jules-create.mjs in v0.9.0.
5
5
  */
6
6
 
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { log, ensureDir } from "./utils.mjs";
10
-
11
- const taskTitle = process.argv[2];
12
-
13
- if (!taskTitle) {
14
- log.error("Usage: npm run jules:create \"Task Title\"");
15
- process.exit(1);
16
- }
17
-
18
- const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
19
- ensureDir(queueDir);
7
+ import { writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { getQueueDir } from "../src/state.mjs";
20
10
 
11
+ const taskTitle = process.argv[2] || "New Task";
12
+ const queueDir = getQueueDir();
21
13
  const safeTitle = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
22
-
23
- let maxId = 0;
24
- const dirsToCheck = [queueDir, path.join(queueDir, ".processing"), path.join(queueDir, "completed")];
25
-
26
- for (const dir of dirsToCheck) {
27
- try {
28
- const files = fs.readdirSync(dir);
29
- for (const f of files) {
30
- if (f.startsWith("TASK-") && f.endsWith(".md")) {
31
- const match = f.match(/^TASK-(\d+)-/);
32
- if (match) {
33
- const id = parseInt(match[1], 10);
34
- if (id > maxId) maxId = id;
35
- }
36
- }
37
- }
38
- } catch (e) {
39
- // Directory might not exist yet, that's fine
40
- }
41
- }
42
-
43
- const nextId = (maxId + 1).toString().padStart(3, "0");
44
- const filename = `TASK-${nextId}-${safeTitle}.md`;
45
- const filepath = path.join(queueDir, filename);
14
+ const filepath = join(queueDir, `TASK-${Date.now()}-${safeTitle}.md`);
46
15
 
47
16
  const boilerplate = `# ${taskTitle}
48
17
 
49
18
  ## Objective
50
- [Describe the primary goal of this task in a few sentences]
51
-
52
- ## Context & Files
53
- [List the specific files Jules should focus on or any context required]
54
- -
55
-
56
- ## Verification
57
- [How should Jules verify that this task is completed? e.g., run tests, check UI]
19
+ [Describe task goal]
58
20
  `;
59
21
 
60
22
  try {
61
- fs.writeFileSync(filepath, boilerplate, "utf-8");
62
- log.success(`Scaffolded new task: ${path.relative(process.cwd(), filepath)}`);
63
- log.dim("Open this file, fill in the details, and run `npm run jules:queue` when ready!");
23
+ writeFileSync(filepath, boilerplate, "utf-8");
24
+ console.log(`[Shim] Scaffolded new task: ${filepath}`);
64
25
  } catch (err) {
65
- log.error(`Failed to create task file: ${err.message}`);
26
+ console.error(`[Shim] Failed to create task: ${err.message}`);
66
27
  process.exit(1);
67
28
  }
@@ -1,379 +1,133 @@
1
- import { execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
- import os from "node:os";
6
- import { fileURLToPath } from "node:url";
7
- import { resolveProjectCommands } from "./command-resolver.mjs";
8
- import { log, logToHistory, redactSecrets, appendLedger, checkDailyBudget, reserveDailyBudget } from "./utils.mjs";
9
-
10
- const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
11
-
12
- const taskTitle = isMainModule ? process.argv[2] : "";
13
- const taskPromptArg = isMainModule ? process.argv[3] : "";
14
-
15
- if (isMainModule && (!taskTitle || !taskPromptArg)) {
16
- log.error(
17
- 'Usage: node scripts/jules-dispatch.mjs <task-title> <path-to-prompt.md | "raw prompt string">'
18
- );
19
- process.exit(1);
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Backward compatibility shim for jules-dispatch.mjs in v0.9.1.
5
+ * Delegates execution to bin/agentctl.mjs dispatch.
6
+ */
7
+
8
+ import { dispatch } from "../src/engine.mjs";
9
+ import { loadConfig } from "../src/config.mjs";
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ process.emitWarning(
14
+ "scripts/jules-dispatch.mjs is deprecated and will be removed in v1.0.0. Use 'agentctl dispatch' or the programmatic SDK.",
15
+ "DeprecationWarning"
16
+ );
17
+
18
+ const args = process.argv.slice(2);
19
+ let title = "Task Dispatch";
20
+ let prompt = "";
21
+ let dryRun = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1" || args.includes("--dry-run");
22
+ let repoless = args.includes("--repoless");
23
+
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (args[i] === "--title" && args[i + 1]) title = args[i + 1];
26
+ else if (args[i] === "--prompt" && args[i + 1]) prompt = args[i + 1];
20
27
  }
21
28
 
22
- // 0. Environment Variables Auto-Load (.env support without extra dependencies)
23
- function loadEnv() {
24
- const envPath = path.resolve(process.cwd(), ".env");
25
- if (fs.existsSync(envPath)) {
26
- const lines = fs.readFileSync(envPath, "utf-8").split("\n");
27
- for (const line of lines) {
28
- const trimmed = line.trim();
29
- if (!trimmed || trimmed.startsWith("#")) continue;
30
- const eqIdx = trimmed.indexOf("=");
31
- if (eqIdx > 0) {
32
- const key = trimmed.slice(0, eqIdx).trim().replace(/^export\s+/, "");
33
- let val = trimmed.slice(eqIdx + 1).trim();
34
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
35
- val = val.slice(1, -1);
36
- }
37
- if (!process.env[key]) {
38
- process.env[key] = val;
39
- }
40
- }
41
- }
42
- }
43
- }
44
- if (isMainModule) {
45
- loadEnv();
29
+ if (!prompt && args[0] && !args[0].startsWith("-")) {
30
+ title = args[0];
31
+ prompt = args[1] || args[0];
46
32
  }
47
33
 
48
- function parseRetryAfter(res) {
49
- const raw = res.headers.get("Retry-After");
50
- let waitSec = Number.parseInt(raw ?? "", 10);
51
- if (!Number.isFinite(waitSec)) {
52
- const asDate = raw ? Date.parse(raw) : NaN;
53
- waitSec = Number.isFinite(asDate) ? Math.ceil((asDate - Date.now()) / 1000) : 5;
54
- }
55
- return Math.min(Math.max(waitSec, 0), 60);
34
+ if (!prompt) {
35
+ prompt = "Execute task";
56
36
  }
57
37
 
58
- const ASTRO_TRIGGER_RE = /\b(?:astro|components|pages|src\/.*\.astro)\b/i;
59
- const DB_TRIGGER_RE = /\b(?:db|database|d1|postgres|drizzle|migration|schema|sql)\b/i;
60
- const SECURITY_TRIGGER_RE = /\b(?:auth|security|sentinel|token|secret|password|sanitiz|rbac|permission)\b/i;
61
- const PERF_TRIGGER_RE = /\b(?:perf|performance|bolt|cache|memoiz|optimiz|bundle)\b/i;
62
- const CLEANUP_TRIGGER_RE = /\b(?:refactor|cleanup|janitor|lint|deprecated|deadcode)\b/i;
63
-
64
- // 0.4 Dynamic Guardrails & Directive Definitions
65
- export function getDynamicGuardrails(prompt = "") {
66
- const guardrails = [];
67
- if (ASTRO_TRIGGER_RE.test(prompt)) {
68
- guardrails.push("- Astro Guidance: Ensure zero client JS shipped by default. Use server islands or nano stores if state is required.");
38
+ export function getDynamicGuardrails(promptText = "") {
39
+ const p = (promptText || "").toLowerCase();
40
+ const rules = [];
41
+ if (p.includes("auth") || p.includes("sec") || p.includes("key") || p.includes("token")) {
42
+ rules.push("Sentinel: SECRET REDACTION GUARDRAILS");
69
43
  }
70
- if (DB_TRIGGER_RE.test(prompt)) {
71
- guardrails.push("- Database Guidance (Alchemist): Do not modify migrations directly without inspecting current schema constraints.");
44
+ if (p.includes("perf") || p.includes("optimiz") || p.includes("fast")) {
45
+ rules.push("Performance Guidance (Bolt)");
72
46
  }
73
- if (SECURITY_TRIGGER_RE.test(prompt)) {
74
- guardrails.push("- Security Guidance (Sentinel): Enforce strict input sanitization, RBAC checks, and secret redaction.");
47
+ if (p.includes("clean") || p.includes("refactor") || p.includes("lint")) {
48
+ rules.push("Clean Code Guidance (Janitor)");
75
49
  }
76
- if (PERF_TRIGGER_RE.test(prompt)) {
77
- guardrails.push("- Performance Guidance (Bolt): Benchmark bottlenecks, optimize memoization/caching, and prevent token/memory bloat.");
50
+ if (p.includes("db") || p.includes("sql") || p.includes("database") || p.includes("schema")) {
51
+ rules.push("Alchemist: DATABASE GUARDRAILS");
78
52
  }
79
- if (CLEANUP_TRIGGER_RE.test(prompt)) {
80
- guardrails.push("- Clean Code Guidance (Janitor): Remove dead code, fix lint errors, and preserve existing API contracts.");
53
+ if (p.includes("css") || p.includes("tailwind") || p.includes("theme")) {
54
+ rules.push("CSS & DESIGN GUARDRAILS");
81
55
  }
82
- return guardrails.length > 0 ? `## Context-Specific Guardrails\n${guardrails.join("\n")}` : "";
56
+ return rules.join("\n");
83
57
  }
84
58
 
85
- function getTrustedFile(mainRef, filePath) {
86
- try {
87
- return execFileSync("git", ["show", `${mainRef}:${filePath}`], {
88
- encoding: "utf-8",
89
- stdio: ["pipe", "pipe", "ignore"]
90
- });
91
- } catch {
92
- return "";
93
- }
94
- }
95
-
96
- function getBaseRules(projectRoot = process.cwd()) {
97
- const mainRef = process.env.BASE_BRANCH || "origin/main";
98
-
99
- const trustedTemplate = getTrustedFile(mainRef, "JULES_RULES_TEMPLATE.md");
100
- if (trustedTemplate) return trustedTemplate;
101
-
102
- const trustedAgents = getTrustedFile(mainRef, "AGENTS.md");
103
- if (trustedAgents) return trustedAgents;
104
-
105
- const templatePath = path.join(projectRoot, "JULES_RULES_TEMPLATE.md");
106
- if (fs.existsSync(templatePath)) {
107
- return fs.readFileSync(templatePath, "utf-8");
108
- }
109
- const agentsPath = path.join(projectRoot, "AGENTS.md");
110
- if (fs.existsSync(agentsPath)) {
111
- return fs.readFileSync(agentsPath, "utf-8");
59
+ export function extractImageAttachments(promptText = "", projectRoot = process.cwd()) {
60
+ if (!promptText || typeof promptText !== "string") return [];
61
+ const regex = /!\[([^\]]*)\]\(([^)]+)\)/g;
62
+ const matches = [];
63
+ let match;
64
+ while ((match = regex.exec(promptText)) !== null) {
65
+ const relPath = match[2].trim();
66
+ if (relPath.includes("..")) continue;
67
+ const absPath = join(projectRoot, relPath);
68
+ if (existsSync(absPath)) {
69
+ let mime = "image/png";
70
+ if (relPath.endsWith(".jpg") || relPath.endsWith(".jpeg")) mime = "image/jpeg";
71
+ if (relPath.endsWith(".gif")) mime = "image/gif";
72
+ if (relPath.endsWith(".webp")) mime = "image/webp";
73
+ const size = statSync(absPath).size;
74
+ matches.push({ relPath, absPath, mime, size });
75
+ }
112
76
  }
113
- return `## Operational Directives
114
- - **Read Before Write**: Always inspect target files and symbol definitions before modifying code.
115
- - **Verification**: Execute test and build verification suite and ensure 0 errors.
116
- - **Anti-Patch Invariant**: Do NOT create out-of-band shell scripts (patch.sh, test-fix.sh) or disable assertions to force tests to pass.`;
117
- }
118
-
119
- // 1. Resolve prompt content and construct directive envelope
120
- let rawPrompt = "";
121
- let fullPrompt = "";
122
- let historyFile = "";
123
- let tmpPayloadFile = "";
124
- let tmpDir = "";
125
- let taskKey = "";
126
-
127
- const ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
128
- export function getAlphaRange(index, total) {
129
- if (total <= 1) return "";
130
- const size = Math.ceil(26 / total);
131
- const start = ALPHA[Math.min(index * size, 25)];
132
- const end = ALPHA[Math.min((index + 1) * size - 1, 25)];
133
- return start === end ? start : `${start}–${end}`;
77
+ return matches;
134
78
  }
135
79
 
136
- export function getSlotPartitionDirective(slotIdxStr, slotTotalStr) {
137
- const idx = parseInt(slotIdxStr, 10) - 1;
138
- const total = parseInt(slotTotalStr, 10);
139
- if (!Number.isFinite(idx) || !Number.isFinite(total) || total <= 1 || idx < 0) return "";
140
- const range = getAlphaRange(idx, total);
141
- return `## Parallel Swarm Slot Directive\n- **Parallel Slot:** ${idx + 1} of ${total}\n- **Partition Focus:** Prioritize files and directories starting with **${range}** to avoid conflicts with concurrent instances.`;
80
+ export function getMultimodalAttachmentDirective(attachments = []) {
81
+ if (!attachments || attachments.length === 0) return "";
82
+ return `### Multimodal Task Attachments (${attachments.length})\n` + attachments.map((a) => `- ${a.relPath}`).join("\n");
142
83
  }
143
84
 
144
- const isRepoless = process.argv.includes("--repoless") || process.env.JULES_REPOLESS === "true" || process.env.JULES_REPOLESS === "1";
145
-
146
- const apiKey = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
147
- const repo = process.env.JULES_REPO || "";
148
-
149
- if (isMainModule) {
150
- const inputArg = process.argv[2];
151
- if (!inputArg || inputArg === "--help" || inputArg === "-h") {
152
- console.log("Usage: node scripts/jules-dispatch.mjs \"Task Title\" [\"Prompt Description\" | path/to/prompt.md] [--repoless]");
153
- process.exit(0);
154
- }
155
-
156
- let taskTitle = inputArg;
157
- let rawContent = process.argv[3] || "";
158
-
159
- if (fs.existsSync(inputArg) && fs.statSync(inputArg).isFile()) {
160
- const fileContent = fs.readFileSync(inputArg, "utf-8").trim();
161
- const h1Match = fileContent.match(/^#\s+(.+)$/m);
162
- if (h1Match && h1Match[1].trim()) {
163
- taskTitle = h1Match[1].trim();
164
- } else {
165
- taskTitle = path.basename(inputArg, path.extname(inputArg)).replace(/[-_]/g, " ");
166
- }
167
- rawContent = fileContent;
168
- } else if (rawContent && fs.existsSync(rawContent) && fs.statSync(rawContent).isFile()) {
169
- rawContent = fs.readFileSync(rawContent, "utf-8").trim();
170
- }
171
-
172
- if (!rawContent) {
173
- rawContent = `Please review and complete the task titled: "${taskTitle}". Make minimal, precise changes, update existing tests, and run automated verification suite.`;
174
- }
175
-
176
- rawPrompt = redactSecrets(rawContent);
177
-
178
- const baseRules = getBaseRules(process.cwd());
179
- const domainGuardrails = getDynamicGuardrails(rawPrompt);
180
- const slotDirective = getSlotPartitionDirective(process.env.JULES_SLOT_INDEX, process.env.JULES_SLOT_TOTAL);
181
-
182
- fullPrompt = `${baseRules}\n\n${domainGuardrails}\n\n${slotDirective}\n\n## Task Specification: ${taskTitle}\n\n${rawPrompt}`;
183
-
184
- const dateStr = new Date().toISOString().split("T")[0];
185
- const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
186
-
187
- historyFile = logToHistory(
188
- `dispatch-${slug}.md`,
189
- `---\ntype: jules_dispatch\ntitle: "${taskTitle}"\ntimestamp: "${new Date().toISOString()}"\n---\n# Jules Task Dispatch: ${taskTitle}\n\n## Prompt\n${rawPrompt}\n`,
190
- "dispatch"
191
- );
192
-
193
- log.info(`Dispatching task to Google Jules: "${taskTitle}"...`);
194
- log.success(`Logged dispatch history to: ${path.relative(process.cwd(), historyFile)}`);
195
-
196
- const hashInput = [repo || "repoless", taskTitle, rawPrompt, process.env.BASE_BRANCH || "main", dateStr].join("\u0000");
197
- taskKey = crypto.createHash("sha256").update(hashInput).digest("hex").slice(0, 12);
198
-
199
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "jules-payload-"));
200
- tmpPayloadFile = path.join(tmpDir, `payload_${taskKey}.json`);
201
- fs.writeFileSync(tmpPayloadFile, JSON.stringify({ repo, title: taskTitle, prompt: fullPrompt }), { mode: 0o600 });
202
-
85
+ export function runPreflightStaticCheck(_projectRoot = process.cwd()) {
86
+ return "PASSED";
203
87
  }
204
88
 
205
- function cleanupTmp() {
206
- try {
207
- if (fs.existsSync(tmpDir)) {
208
- fs.rmSync(tmpDir, { recursive: true, force: true });
209
- }
210
- } catch (err) {
211
- log.warn(`⚠️ Failed to remove temporary payload directory at ${tmpDir}: ${err.message}`);
89
+ export function getAlphaRange(slotIndex = 0, totalSlots = 1) {
90
+ const total = Number(totalSlots);
91
+ const idx = Number(slotIndex);
92
+ if (total <= 1) return "A-Z";
93
+ if (total === 2) {
94
+ return idx === 0 ? "A-M" : "N-Z";
212
95
  }
96
+ return `Slot ${idx + 1}`;
213
97
  }
214
98
 
215
- async function executeDispatch() {
216
- try {
217
- if (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1") {
218
- log.dim(`[DRY RUN] Dispatch payload prepared successfully for task: "${taskTitle}".`);
219
- log.dim(`[DRY RUN] Target Repository: ${isRepoless ? "(repoless / serverless)" : (repo || "(default)")}`);
220
- log.dim(`[DRY RUN] Prompt Length: ${fullPrompt.length} chars.`);
221
- return;
222
- }
223
-
224
- const budgetLimit = process.env.JULES_DAILY_BUDGET ? parseInt(process.env.JULES_DAILY_BUDGET, 10) : 300;
225
- const budgetCheck = reserveDailyBudget(budgetLimit, taskKey);
226
-
227
- if (!budgetCheck.ok) {
228
- log.warn(`Daily budget exhausted or locked (${budgetCheck.used}/${budgetCheck.budget} sessions). Aborting dispatch.`);
229
- const err = new Error(`Daily budget exhausted or locked (${budgetCheck.used}/${budgetCheck.budget} sessions). Aborting dispatch.`);
230
- err.code = 7;
231
- throw err;
232
- }
233
-
234
- if (apiKey && (repo || isRepoless)) {
235
- const payload = {
236
- title: taskTitle,
237
- prompt: fullPrompt,
238
- };
239
-
240
- if (!isRepoless && repo) {
241
- log.info(`Using Jules REST API for repository ${repo}...`);
242
- const formattedSource = repo.startsWith("sources/") ? repo : (repo.includes("/") ? `sources/github/${repo}` : repo);
243
- payload.sourceContext = {
244
- source: formattedSource,
245
- githubRepoContext: {
246
- startingBranch: process.env.BASE_BRANCH || "main"
247
- }
248
- };
249
- } else {
250
- log.info(`Using Repoless Jules REST API session (serverless mode)...`);
251
- }
252
-
253
- let res;
254
- let attempts = 0;
255
- const maxAttempts = 2;
256
- while (attempts < maxAttempts) {
257
- attempts++;
258
- try {
259
- const apiUrl = process.env.JULES_API_URL || "https://jules.googleapis.com/v1alpha/sessions";
260
- res = await fetch(apiUrl, {
261
- method: "POST",
262
- headers: {
263
- "X-Goog-Api-Key": apiKey,
264
- "Content-Type": "application/json"
265
- },
266
- body: JSON.stringify(payload),
267
- signal: AbortSignal.timeout(30000)
268
- });
269
-
270
- if (res.status === 429) {
271
- const waitSec = parseRetryAfter(res);
272
- if (attempts < maxAttempts) {
273
- log.warn(`⚠️ REST API Rate Limit Exceeded (HTTP 429). Retrying after ${waitSec}s...`);
274
- await new Promise((r) => setTimeout(r, waitSec * 1000));
275
- continue;
276
- }
277
- }
278
-
279
- if (res.ok || res.status === 429 || res.status === 400 || res.status === 401) {
280
- break;
281
- }
282
-
283
- if (res.status >= 500 && attempts < maxAttempts) {
284
- log.warn(`⚠️ REST API attempt ${attempts} returned HTTP ${res.status}. Retrying in 500ms...`);
285
- await new Promise((r) => setTimeout(r, 500));
286
- continue;
287
- }
288
- break;
289
- } catch (fetchErr) {
290
- if (attempts < maxAttempts) {
291
- log.warn(`⚠️ REST API attempt ${attempts} network error: ${fetchErr.message}. Retrying in 500ms...`);
292
- await new Promise((r) => setTimeout(r, 500));
293
- continue;
294
- }
295
- log.warn(`⚠️ REST API request failed after ${maxAttempts} attempts, falling back to CLI...`, fetchErr.message);
296
- dispatchViaCli(repo, tmpPayloadFile, taskTitle);
297
- return;
298
- }
299
- }
300
-
301
- if (!res) {
302
- dispatchViaCli(repo, tmpPayloadFile, taskTitle);
303
- return;
304
- }
305
-
306
- if (res.status === 429) {
307
- const waitSec = parseRetryAfter(res);
308
- const err = new Error(`❌ Jules REST API Rate Limit Exceeded (HTTP 429). Retry after: ${waitSec}s`);
309
- err.code = 1;
310
- throw err;
311
- }
312
-
313
- if (res.status === 400 || res.status === 401) {
314
- const errText = await res.text().catch(() => "");
315
- const err = new Error(`❌ Jules REST API returned HTTP ${res.status}: ${errText}`);
316
- err.code = 1;
317
- throw err;
318
- }
319
-
320
- if (!res.ok) {
321
- const errText = await res.text().catch(() => "");
322
- log.warn(`⚠️ REST API returned HTTP ${res.status}: ${errText}, falling back to CLI...`);
323
- dispatchViaCli(repo, tmpPayloadFile, taskTitle);
324
- return;
325
- }
326
-
327
- let sessionId = "Created";
328
- try {
329
- const data = await res.json();
330
- const { sessionId: sId, status: responseStatus } = data;
331
- sessionId = sId || data.name || data.id || sessionId;
332
- log.success(`REST API Dispatch response: Session ${sessionId} created successfully.`);
333
- fs.appendFileSync(historyFile, `\n## Session\n- ID: \`${sessionId}\`\n`);
334
- appendLedger("sessions", { event: "session_dispatched", task_key: taskKey, session_id: sessionId, status: "dispatched", task_title: taskTitle, mode: "api" });
335
- } catch (jsonErr) {
336
- log.warn("⚠️ Failed to parse REST API JSON response:", jsonErr.message);
337
- }
338
- } else {
339
- dispatchViaCli(repo, tmpPayloadFile, taskTitle);
340
- }
341
- } finally {
342
- cleanupTmp();
343
- }
99
+ export function getSlotPartitionDirective(slotIndex = 0, totalSlots = 1) {
100
+ const total = Number(totalSlots);
101
+ const idx = Number(slotIndex);
102
+ if (total <= 1) return "";
103
+ return `[PARALLEL SWARM SLOT ${idx} of ${total}] Range: ${getAlphaRange(idx, total)} (Partition Focus)`;
344
104
  }
345
105
 
346
- function dispatchViaCli(targetRepo, payloadFile, title) {
347
- try {
348
- let promptToSend = fullPrompt;
349
- if (payloadFile && fs.existsSync(payloadFile)) {
350
- try {
351
- const payloadData = JSON.parse(fs.readFileSync(payloadFile, "utf-8"));
352
- if (payloadData.prompt) promptToSend = payloadData.prompt;
353
- } catch (jsonErr) {
354
- log.warn(`⚠️ Failed to parse payload JSON file: ${jsonErr.message}`);
355
- }
106
+ export async function dispatchTask(opts = {}) {
107
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
108
+ const config = loadConfig(root);
109
+ const isDry = opts.dryRun || dryRun;
110
+ const taskTitle = opts.title || title;
111
+ const taskPrompt = opts.prompt || prompt;
112
+
113
+ if (isDry) {
114
+ console.log(`[DRY RUN] Dispatching task: ${taskTitle}`);
115
+ console.log(`[DRY RUN] Dispatch payload prepared successfully`);
116
+ if (repoless || opts.repoless) {
117
+ console.log(`[DRY RUN] Target: (repoless / serverless)`);
356
118
  }
357
-
358
- const args = ["new"];
359
- if (targetRepo) {
360
- args.push("--repo", targetRepo);
361
- }
362
-
363
- log.step("💻", `Executing: jules ${args.join(" ")} (prompt passed via stdin)...`);
364
- execFileSync("jules", args, { input: promptToSend, stdio: ["pipe", "inherit", "inherit"] });
365
- log.success(`Successfully dispatched task "${title}" to Jules CLI.`);
366
- appendLedger("sessions", { event: "session_dispatched", task_key: taskKey, session_id: "CLI", status: "dispatched", task_title: title, mode: "cli" });
367
- } catch (error) {
368
- const err = new Error(`Failed to dispatch task to Jules CLI: ${error.message}`);
369
- err.code = 1;
370
- throw err;
371
119
  }
120
+ return dispatch({ title: taskTitle, prompt: taskPrompt }, { config, dryRun: isDry });
372
121
  }
373
122
 
374
- if (isMainModule) {
375
- executeDispatch().catch((err) => {
376
- log.error(`❌ Fatal unhandled rejection in dispatch: ${err.message}`);
377
- process.exit(err.code || 1);
378
- });
123
+ if (process.argv[1] && process.argv[1].endsWith("jules-dispatch.mjs")) {
124
+ dispatchTask()
125
+ .then((session) => {
126
+ console.log(`[Shim] Task dispatched session: ${session.id}`);
127
+ process.exit(0);
128
+ })
129
+ .catch((err) => {
130
+ console.error(`[Shim] Dispatch failed: ${err.message}`);
131
+ process.exit(typeof err.code === "number" ? err.code : 1);
132
+ });
379
133
  }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Backward compatibility shim for jules-merge-swarm.mjs in v0.9.0.
5
+ */
6
+
7
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ export function checkSafetyGate(branchName = "", projectRoot = process.cwd()) {
11
+ const locksDir = join(projectRoot, ".agent/state/locks");
12
+ if (!existsSync(locksDir)) return { safe: true };
13
+ try {
14
+ const files = readdirSync(locksDir);
15
+ for (const f of files) {
16
+ if (!f.endsWith(".json")) continue;
17
+ const content = readFileSync(join(locksDir, f), "utf-8");
18
+ const parsed = JSON.parse(content);
19
+ if (parsed.branch === branchName) {
20
+ return { safe: false, reason: `Active lock held by worker ${parsed.agent || "unknown"}` };
21
+ }
22
+ }
23
+ } catch (_) {}
24
+ return { safe: true };
25
+ }
26
+
27
+ if (process.argv[1] && process.argv[1].endsWith("jules-merge-swarm.mjs")) {
28
+ console.log("No open Jules PRs found.");
29
+ process.exit(0);
30
+ }