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.
- package/.agent/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/.agent/rules/dynamic-guardrails.json +0 -4
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +7 -2
- package/README.md +113 -21
- package/bin/agentctl.mjs +274 -0
- package/bin/init.js +1 -1
- package/index.mjs +34 -6
- package/package.json +22 -6
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -174
- package/scripts/jules-create.mjs +12 -51
- package/scripts/jules-dispatch.mjs +103 -349
- package/scripts/jules-merge-swarm.mjs +30 -0
- package/scripts/jules-nightly.mjs +7 -124
- package/scripts/jules-patch.mjs +23 -0
- package/scripts/jules-queue-runner.mjs +28 -203
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +76 -578
- package/scripts/jules-status.mjs +42 -65
- package/scripts/jules-swarm.mjs +27 -232
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -170
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +118 -252
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +209 -0
- package/src/webhook.mjs +138 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
package/scripts/jules-create.mjs
CHANGED
|
@@ -1,67 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
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
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
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
|
|
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
|
-
|
|
62
|
-
log
|
|
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
|
-
|
|
26
|
+
console.error(`[Shim] Failed to create task: ${err.message}`);
|
|
66
27
|
process.exit(1);
|
|
67
28
|
}
|
|
@@ -1,379 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
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 (
|
|
71
|
-
|
|
44
|
+
if (p.includes("perf") || p.includes("optimiz") || p.includes("fast")) {
|
|
45
|
+
rules.push("Performance Guidance (Bolt)");
|
|
72
46
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
47
|
+
if (p.includes("clean") || p.includes("refactor") || p.includes("lint")) {
|
|
48
|
+
rules.push("Clean Code Guidance (Janitor)");
|
|
75
49
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
50
|
+
if (p.includes("db") || p.includes("sql") || p.includes("database") || p.includes("schema")) {
|
|
51
|
+
rules.push("Alchemist: DATABASE GUARDRAILS");
|
|
78
52
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
53
|
+
if (p.includes("css") || p.includes("tailwind") || p.includes("theme")) {
|
|
54
|
+
rules.push("CSS & DESIGN GUARDRAILS");
|
|
81
55
|
}
|
|
82
|
-
return
|
|
56
|
+
return rules.join("\n");
|
|
83
57
|
}
|
|
84
58
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
}
|