jules-orchestrator-kit 0.27.1 → 0.28.2

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/README.md CHANGED
@@ -36,6 +36,12 @@
36
36
 
37
37
  <br/>
38
38
 
39
+ <p align="center">
40
+ <img src="docs/assets/hero-flow.svg" alt="Autonomous Orchestration Pipeline" width="100%" />
41
+ </p>
42
+
43
+ <br/>
44
+
39
45
  ---
40
46
 
41
47
  <br/>
@@ -75,6 +81,12 @@ Autonomous coding agents can write software at 100× human speed—but unconstra
75
81
 
76
82
  <br/>
77
83
 
84
+ <p align="center">
85
+ <img src="docs/assets/security-shield.svg" alt="Zero-Trust Security & Reliability Guarantees" width="100%" />
86
+ </p>
87
+
88
+ <br/>
89
+
78
90
  ---
79
91
 
80
92
  <br/>
@@ -125,6 +137,12 @@ To ensure maximum merge success, dispatch tasks according to our deterministic t
125
137
 
126
138
  <br/>
127
139
 
140
+ <p align="center">
141
+ <img src="docs/assets/tier-presets.svg" alt="Subscription Tier Allocation Matrix" width="100%" />
142
+ </p>
143
+
144
+ <br/>
145
+
128
146
  ---
129
147
 
130
148
  <br/>
@@ -229,9 +247,23 @@ Ecosystems Natively Supported by src/stack-detector.mjs:
229
247
 
230
248
  <br/>
231
249
 
250
+ <p align="center">
251
+ <img src="docs/assets/architecture-layers.svg" alt="Control Plane Architecture Layers" width="100%" />
252
+ </p>
253
+
254
+ <br/>
255
+
232
256
  ### 1. The Autonomous OODA Verification Loop
233
257
  Every task dispatched to `jules-orchestrator-kit` executes within an immutable, fail-closed verification loop:
234
258
 
259
+ <br/>
260
+
261
+ <p align="center">
262
+ <img src="docs/assets/ooda-loop-cycle.svg" alt="Self-Healing OODA Repair Loop & Thrash Breaker" width="100%" />
263
+ </p>
264
+
265
+ <br/>
266
+
235
267
  ```mermaid
236
268
  flowchart TD
237
269
  A["📩 Task Envelope"] --> B["1. Validate Scope & Base Freshness"]
@@ -265,6 +297,28 @@ flowchart TD
265
297
 
266
298
  <br/>
267
299
 
300
+ ### 3. Multi-Agent Parallel Swarm Topology
301
+ Run concurrent agents across parallel worktree slots with AST/JSON 3-way merging:
302
+
303
+ <br/>
304
+
305
+ <p align="center">
306
+ <img src="docs/assets/swarm-topology.svg" alt="Multi-Agent Parallel Swarm Topology" width="100%" />
307
+ </p>
308
+
309
+ <br/>
310
+
311
+ ### 4. Model Context Protocol (MCP) Integration
312
+ Native stdio server exposing task dispatch, gate verification, and risk auditing to client tools (Antigravity, Claude, Cursor):
313
+
314
+ <br/>
315
+
316
+ <p align="center">
317
+ <img src="docs/assets/mcp-integration.svg" alt="Model Context Protocol (MCP) Integration" width="100%" />
318
+ </p>
319
+
320
+ <br/>
321
+
268
322
  ---
269
323
 
270
324
  <br/>
package/bin/agentctl.mjs CHANGED
@@ -14,12 +14,12 @@ const command = args[0];
14
14
 
15
15
  function printHelp() {
16
16
  console.log(`
17
- 🚀 agentctl v0.27.1 — Universal Agent Orchestrator & Safety Gatekeeper
17
+ 🚀 agentctl v0.28.2 — Universal Agent Orchestrator & Safety Gatekeeper
18
18
 
19
19
  Usage: agentctl <command> [options]
20
20
 
21
21
  Commands:
22
- dispatch Dispatch a single task to an AI agent
22
+ dispatch | create Dispatch a single task to an AI agent
23
23
  gate | audit Run CI security and verification gate against current branch
24
24
  queue Run pending task queue
25
25
  swarm Run parallel task swarm
@@ -31,10 +31,16 @@ Commands:
31
31
  review-repair Parse PR review comments and synthesize OODA repair tasks
32
32
  dashboard Start local HTTP telemetry and audit dashboard
33
33
  init Scaffold .agent/ directory and config.yml
34
+ status Display queue and system status summary
35
+ scan Scan codebase for TODO/FIXME task candidates
34
36
  version Output agentctl version
35
37
 
36
38
  Options:
37
39
  --dry-run, -d Simulate action without making API calls or modifying git
40
+ --mode, -m Gate evaluation mode (working-tree | committed | staged)
41
+ --repoless Dispatch task in repoless execution mode
42
+ --source, -s Specify Jules repository source name
43
+ --branch, -b Specify target starting branch
38
44
  --json, -j Emit machine-readable JSON output
39
45
  --help, -h Show command help
40
46
  `);
@@ -47,7 +53,7 @@ async function main() {
47
53
  }
48
54
 
49
55
  if (command === "version" || command === "--version" || command === "-v") {
50
- console.log("agentctl v0.27.0");
56
+ console.log("agentctl v0.28.2");
51
57
  process.exit(0);
52
58
  }
53
59
 
@@ -57,13 +63,19 @@ async function main() {
57
63
  const config = loadConfig(root);
58
64
 
59
65
  switch (command) {
60
- case "dispatch": {
66
+ case "dispatch":
67
+ case "create": {
61
68
  const { values } = parseArgs({
62
69
  args: args.slice(1),
63
70
  options: {
64
71
  title: { type: "string", short: "t" },
65
72
  prompt: { type: "string", short: "p" },
66
73
  "prompt-file": { type: "string", short: "f" },
74
+ source: { type: "string", short: "s" },
75
+ branch: { type: "string", short: "b" },
76
+ repoless: { type: "boolean" },
77
+ "auto-pr": { type: "boolean" },
78
+ "require-plan-approval": { type: "boolean" },
67
79
  "dry-run": { type: "boolean", short: "d" },
68
80
  json: { type: "boolean", short: "j" },
69
81
  },
@@ -87,10 +99,22 @@ async function main() {
87
99
  const task = {
88
100
  title: values.title || "CLI Dispatch Task",
89
101
  prompt: promptContent,
102
+ source: values.source,
103
+ branch: values.branch,
104
+ repoless: values.repoless,
105
+ autoPr: values["auto-pr"],
106
+ requirePlanApproval: values["require-plan-approval"],
90
107
  };
91
108
 
92
109
  try {
93
- const session = await dispatch(task, { root, config, dryRun: values["dry-run"] });
110
+ const session = await dispatch(task, {
111
+ root,
112
+ config,
113
+ dryRun: values["dry-run"],
114
+ repoless: values.repoless,
115
+ source: values.source,
116
+ branch: values.branch,
117
+ });
94
118
  if (values.json) {
95
119
  console.log(JSON.stringify({ ok: true, session }, null, 2));
96
120
  } else {
@@ -100,12 +124,13 @@ async function main() {
100
124
  }
101
125
  process.exit(0);
102
126
  } catch (err) {
127
+ const exitCode = typeof err.code === "number" ? err.code : 1;
103
128
  if (values.json) {
104
- console.log(JSON.stringify({ ok: false, error: err.message, code: err.code || 1 }, null, 2));
129
+ console.log(JSON.stringify({ ok: false, error: err.message, code: exitCode }, null, 2));
105
130
  } else {
106
131
  console.error(`❌ Dispatch Failed: ${err.message}`);
107
132
  }
108
- process.exit(err.code || 1);
133
+ process.exit(exitCode);
109
134
  }
110
135
  break;
111
136
  }
@@ -116,6 +141,10 @@ async function main() {
116
141
  args: args.slice(1),
117
142
  options: {
118
143
  base: { type: "string", short: "b", default: config.baseBranch || "main" },
144
+ mode: { type: "string", short: "m", default: "working-tree" },
145
+ "working-tree": { type: "boolean" },
146
+ staged: { type: "boolean" },
147
+ committed: { type: "boolean" },
119
148
  fix: { type: "boolean" },
120
149
  "allow-protected": { type: "boolean" },
121
150
  json: { type: "boolean", short: "j" },
@@ -123,10 +152,16 @@ async function main() {
123
152
  allowPositionals: true,
124
153
  });
125
154
 
155
+ let selectedMode = values.mode || "working-tree";
156
+ if (values.staged) selectedMode = "staged";
157
+ if (values.committed) selectedMode = "committed";
158
+ if (values["working-tree"]) selectedMode = "working-tree";
159
+
126
160
  const res = await gate({
127
161
  root,
128
162
  config,
129
163
  base: values.base,
164
+ mode: selectedMode,
130
165
  fix: values.fix,
131
166
  allowProtected: values["allow-protected"],
132
167
  });
@@ -134,7 +169,7 @@ async function main() {
134
169
  if (values.json) {
135
170
  console.log(JSON.stringify(res, null, 2));
136
171
  } else {
137
- console.log(`\n🛡️ agentctl Safety Gate Audit Results (Base: ${values.base})`);
172
+ console.log(`\n🛡️ agentctl Safety Gate Audit Results (Base: ${values.base}, Mode: ${selectedMode})`);
138
173
  console.log(`-----------------------------------------------------`);
139
174
  for (const p of res.phases) {
140
175
  const status = p.ok ? "✅ PASS" : "❌ FAIL";
@@ -150,14 +185,14 @@ async function main() {
150
185
  console.log(`Overall Result: ${res.ok ? "APPROVED (Exit 0)" : `REJECTED (Exit ${res.code})`}\n`);
151
186
  }
152
187
 
153
- process.exit(res.code);
188
+ process.exit(typeof res.code === "number" ? res.code : 0);
154
189
  break;
155
190
  }
156
191
 
157
192
  case "queue": {
158
193
  const queueDir = getQueueDir(root);
159
194
  const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
160
- console.log(` Found ${files.length} queued task(s) in .agent/queue/`);
195
+ console.log(`Found ${files.length} queued task(s) in .agent/queue/`);
161
196
  if (files.length > 0) {
162
197
  const tasks = files.map((f) => ({
163
198
  id: f,
@@ -229,7 +264,7 @@ async function main() {
229
264
  }
230
265
 
231
266
  case "doctor": {
232
- console.log(`\n🔍 agentctl System Diagnostics (v0.25.1)`);
267
+ console.log(`\n🔍 agentctl System Diagnostics (v0.28.2)`);
233
268
  console.log(`--------------------------------------------------`);
234
269
  console.log(` Project Root : ${root}`);
235
270
  console.log(` Config File : ${config._file || "None (Using defaults)"}`);
@@ -315,6 +350,35 @@ base_branch: main
315
350
  break;
316
351
  }
317
352
 
353
+ case "status": {
354
+ const queueDir = getQueueDir(root);
355
+ const files = existsSync(queueDir) ? readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir)) : [];
356
+ console.log(`\n📊 agentctl Status Summary (v0.28.2)`);
357
+ console.log(`--------------------------------------------------`);
358
+ console.log(` Project Root : ${root}`);
359
+ console.log(` Pending Tasks : ${files.length}`);
360
+ console.log(` Active VFS Locks : ${lockStatus(root).length}`);
361
+ const budget = checkDailyBudget(root, config.limits.dailyTasks);
362
+ console.log(` Daily Budget : ${budget.used} / ${budget.budget} used`);
363
+ console.log(`--------------------------------------------------\n`);
364
+ process.exit(0);
365
+ break;
366
+ }
367
+
368
+ case "scan": {
369
+ const { scanCodebaseForTodos } = await import("../scripts/jules-scan-todos.mjs");
370
+ const todos = scanCodebaseForTodos(root);
371
+ console.log(`\n🔍 Scanned ${todos.length} TODO/FIXME annotation(s).`);
372
+ for (const t of todos.slice(0, 10)) {
373
+ console.log(` - ${t.file}:${t.line} [${t.type}] ${t.text}`);
374
+ }
375
+ if (todos.length > 10) {
376
+ console.log(` ... and ${todos.length - 10} more.`);
377
+ }
378
+ process.exit(0);
379
+ break;
380
+ }
381
+
318
382
  case "mcp": {
319
383
  const { startMcpServer } = await import("../src/mcp.mjs");
320
384
  startMcpServer();
@@ -330,5 +394,6 @@ base_branch: main
330
394
 
331
395
  main().catch((err) => {
332
396
  console.error(`[FATAL ERROR] ${err.message}`);
333
- process.exit(err.code || 1);
397
+ const exitCode = typeof err.code === "number" ? err.code : 1;
398
+ process.exit(exitCode);
334
399
  });
package/bin/init.js CHANGED
@@ -221,7 +221,9 @@ let skippedCount = 0;
221
221
 
222
222
  if (fs.existsSync(sourceScriptsDir)) {
223
223
  const scriptFiles = fs.readdirSync(sourceScriptsDir);
224
+ const excludeFiles = new Set(["release.mjs"]);
224
225
  scriptFiles.forEach((file) => {
226
+ if (excludeFiles.has(file)) return;
225
227
  const srcFile = path.join(sourceScriptsDir, file);
226
228
  const destFile = path.join(targetScriptsDir, file);
227
229
  if (!fs.existsSync(destFile) || isForce) {
@@ -247,14 +249,14 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
247
249
  pkg.scripts = pkg.scripts || {};
248
250
  let updated = false;
249
251
  const julesScripts = {
250
- "jules:dispatch": "node scripts/jules-dispatch.mjs",
251
- "jules:queue": "node bin/agentctl.mjs queue",
252
- "jules:create": "node scripts/jules-create.mjs",
253
- "jules:status": "node scripts/jules-status.mjs",
254
- "jules:audit": "node scripts/jules-self-audit.mjs",
255
- "jules:scan": "node scripts/jules-scan-todos.mjs",
256
- "jules:swarm": "node bin/agentctl.mjs swarm",
257
- "jules:nightly": "node scripts/jules-nightly.mjs"
252
+ "jules:dispatch": "npx agentctl dispatch",
253
+ "jules:queue": "npx agentctl queue",
254
+ "jules:create": "npx agentctl create",
255
+ "jules:status": "npx agentctl status",
256
+ "jules:audit": "npx agentctl gate",
257
+ "jules:scan": "npx agentctl scan",
258
+ "jules:swarm": "npx agentctl swarm",
259
+ "jules:nightly": "npx agentctl clean"
258
260
  };
259
261
 
260
262
  for (const [key, val] of Object.entries(julesScripts)) {
package/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Google Jules Orchestrator Kit - Node.js SDK (v0.25.1)
2
+ * Google Jules Orchestrator Kit - Node.js SDK (v0.28.2)
3
3
  *
4
4
  * Exposes core orchestrator functions for programmatically driving agent tasks,
5
5
  * security auditing, repo gating, and state operations.
6
6
  */
7
7
 
8
- export { loadConfig, detectStack, resolveVerify, resolveRoot, normalizePath, TIER_PRESETS } from "./src/config.mjs";
8
+ export { loadConfig, parseYaml, detectStack, resolveVerify, resolveRoot, normalizePath, TIER_PRESETS } from "./src/config.mjs";
9
9
  export {
10
10
  shannonEntropy,
11
11
  redactSecrets,
@@ -19,14 +19,17 @@ export { isolateMcpStdout, writeMcpFrame } from "./src/mcp.mjs";
19
19
  export { git, runCmd, resolveBase, changedFiles, diffBytes, diffText } from "./src/git.mjs";
20
20
  export {
21
21
  createProvider,
22
+ createFailoverProvider,
22
23
  JULES_PRESET,
23
24
  CLAUDE_PRESET,
24
25
  CODEX_PRESET,
26
+ MissingApiKeyError,
25
27
  ProviderRateLimitError,
26
28
  ProviderUnavailableError,
27
29
  ProviderSchemaError,
28
30
  parseRetryAfter,
29
31
  } from "./src/provider.mjs";
32
+ export { detectPolyglotStack, resolveWorkspaceBoundary, bootstrapZeroTestRepo } from "./src/stack-detector.mjs";
30
33
  export {
31
34
  appendLedger,
32
35
  readLedger,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.27.1",
3
+ "version": "0.28.2",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,10 +10,12 @@ import { loadConfig } from "../src/config.mjs";
10
10
  import { existsSync, statSync } from "node:fs";
11
11
  import { join } from "node:path";
12
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
- );
13
+ if (process.argv[1] && (process.argv[1].endsWith("jules-dispatch.mjs") || process.argv[1].endsWith("jules-dispatch"))) {
14
+ process.emitWarning(
15
+ "scripts/jules-dispatch.mjs is deprecated and will be removed in v1.0.0. Use 'agentctl dispatch' or the programmatic SDK.",
16
+ "DeprecationWarning"
17
+ );
18
+ }
17
19
 
18
20
  const args = process.argv.slice(2);
19
21
  let title = "Task Dispatch";
@@ -5,12 +5,23 @@
5
5
  * Delegates execution to src/engine.mjs gate().
6
6
  */
7
7
 
8
- import { gate } from "../src/engine.mjs";
9
- import { loadConfig, parseYaml } from "../src/config.mjs";
10
- import { matchesGlob } from "../src/security.mjs";
11
8
  import { existsSync } from "node:fs";
12
9
  import { join } from "node:path";
13
10
 
11
+ let gate, loadConfig, parseYaml, matchesGlob;
12
+ try {
13
+ ({ gate } = await import("../src/engine.mjs"));
14
+ ({ loadConfig, parseYaml } = await import("../src/config.mjs"));
15
+ ({ matchesGlob } = await import("../src/security.mjs"));
16
+ } catch (_) {
17
+ try {
18
+ ({ gate, loadConfig, parseYaml, matchesGlob } = await import("jules-orchestrator-kit"));
19
+ } catch (err) {
20
+ console.error("Error: Could not resolve jules-orchestrator-kit modules.", err.message);
21
+ process.exit(1);
22
+ }
23
+ }
24
+
14
25
  export function matchGlob(filepath, globPattern) {
15
26
  return matchesGlob(filepath, globPattern);
16
27
  }
package/src/config.mjs CHANGED
@@ -257,6 +257,19 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
257
257
  const envDailyTasks = process.env.JULES_DAILY_BUDGET !== undefined ? Number(process.env.JULES_DAILY_BUDGET) : null;
258
258
  const envDiffKb = process.env.JULES_MAX_DIFF_KB !== undefined ? Number(process.env.JULES_MAX_DIFF_KB) : null;
259
259
 
260
+ const parsedLimits = parsed.limits || {};
261
+ const normalizedLimits = {
262
+ diffKb: parsedLimits.diff_kb ?? parsedLimits.diffKb,
263
+ promptKb: parsedLimits.prompt_kb ?? parsedLimits.promptKb,
264
+ dailyTasks: parsedLimits.daily_tasks ?? parsedLimits.dailyTasks,
265
+ repairAttempts: parsedLimits.repair_attempts ?? parsedLimits.repairAttempts,
266
+ concurrency: parsedLimits.concurrency,
267
+ staggerMs: parsedLimits.stagger_ms ?? parsedLimits.staggerMs,
268
+ };
269
+ for (const k of Object.keys(normalizedLimits)) {
270
+ if (normalizedLimits[k] === undefined) delete normalizedLimits[k];
271
+ }
272
+
260
273
  const config = {
261
274
  version: parsed.version || DEFAULTS.version,
262
275
  provider: parsed.provider || DEFAULTS.provider,
@@ -269,7 +282,7 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
269
282
  limits: {
270
283
  ...DEFAULTS.limits,
271
284
  ...tierLimits,
272
- ...(parsed.limits || {}),
285
+ ...normalizedLimits,
273
286
  ...(envDailyTasks !== null && !isNaN(envDailyTasks) ? { dailyTasks: envDailyTasks } : {}),
274
287
  ...(envDiffKb !== null && !isNaN(envDiffKb) ? { diffKb: envDiffKb } : {}),
275
288
  },
package/src/dashboard.mjs CHANGED
@@ -32,7 +32,7 @@ export function getDashboardHtml(root = process.cwd()) {
32
32
  </style>
33
33
  </head>
34
34
  <body>
35
- <h1>🚀 jules-orchestrator-kit Dashboard (v0.27.0)</h1>
35
+ <h1>🚀 jules-orchestrator-kit Dashboard (v0.28.2)</h1>
36
36
  <div class="subtitle">Repository: <code>${root}</code></div>
37
37
 
38
38
  <div class="grid">
@@ -59,11 +59,11 @@ export function getDashboardHtml(root = process.cwd()) {
59
59
  fetch('/api/locks').then(r => r.json())
60
60
  ]);
61
61
 
62
- document.getElementById('telemetry-status').innerHTML = telRes.integrity?.valid
62
+ document.getElementById('telemetry-status').innerHTML = (telRes.integrity?.ok || telRes.integrity?.valid)
63
63
  ? '<span class="badge badge-success">✓ SHA-256 Hash Chain Valid</span> <p>Events logged: ' + telRes.events.length + '</p>'
64
64
  : '<span class="badge badge-warn">⚠ Check Telemetry Logs</span>';
65
65
 
66
- document.getElementById('flaky-status').innerHTML = '<p>Verdict: <code>' + (flakyRes.verdict?.action || 'CLEAN') + '</code></p><p>Tracked test runs: ' + flakyRes.count + '</p>';
66
+ document.getElementById('flaky-status').innerHTML = '<p>Verdict: <code>' + (flakyRes.verdict?.verdict || flakyRes.verdict?.action || 'CLEAN') + '</code></p><p>Tracked test runs: ' + flakyRes.count + '</p>';
67
67
  document.getElementById('locks-status').innerHTML = '<p>Active VFS Locks: <code>' + locksRes.count + '</code></p>';
68
68
  } catch (err) {
69
69
  console.error("Dashboard update failed:", err);
@@ -79,13 +79,6 @@ export function getDashboardHtml(root = process.cwd()) {
79
79
  export function createDashboardServer({ root = process.cwd(), port: _port = 4100 } = {}) {
80
80
  return http.createServer((req, res) => {
81
81
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
82
- res.setHeader("Access-Control-Allow-Origin", "*");
83
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
84
-
85
- if (req.method === "OPTIONS") {
86
- res.writeHead(204);
87
- return res.end();
88
- }
89
82
 
90
83
  if (url.pathname === "/" || url.pathname === "/index.html") {
91
84
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -94,7 +87,7 @@ export function createDashboardServer({ root = process.cwd(), port: _port = 4100
94
87
 
95
88
  if (url.pathname === "/api/status") {
96
89
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
97
- return res.end(JSON.stringify({ ok: true, version: "0.27.0", root, ts: new Date().toISOString() }));
90
+ return res.end(JSON.stringify({ ok: true, version: "0.28.2", root, ts: new Date().toISOString() }));
98
91
  }
99
92
 
100
93
  if (url.pathname === "/api/telemetry") {
@@ -127,10 +120,10 @@ export function createDashboardServer({ root = process.cwd(), port: _port = 4100
127
120
  });
128
121
  }
129
122
 
130
- export function startDashboardServer(port = 4100, root = process.cwd()) {
123
+ export function startDashboardServer(port = 4100, root = process.cwd(), host = "127.0.0.1") {
131
124
  const server = createDashboardServer({ root, port });
132
- server.listen(port, "0.0.0.0", () => {
133
- console.log(`🚀 jules-orchestrator-kit Dashboard running at http://localhost:${port}`);
125
+ server.listen(port, host, () => {
126
+ console.log(`🚀 jules-orchestrator-kit Dashboard running at http://${host}:${port}`);
134
127
  });
135
128
  return server;
136
129
  }
package/src/engine.mjs CHANGED
@@ -101,20 +101,21 @@ export async function gate(opts = {}) {
101
101
  const root = opts.root || process.cwd();
102
102
  const config = opts.config || loadConfig(root);
103
103
  const base = opts.base || config.baseBranch || "main";
104
+ const mode = opts.mode || (opts.workingTree ? "working-tree" : (process.env.JULES_GATE_MODE || "working-tree"));
104
105
  const progressBus = opts.progressBus;
105
106
  const progressToken = opts.progressToken;
106
107
  const phases = [];
107
108
 
108
- appendTelemetry(root, "gate_started", { base });
109
+ appendTelemetry(root, "gate_started", { base, mode });
109
110
 
110
111
  let files = [];
111
112
  let bytes = 0;
112
113
  let diffStr = "";
113
114
 
114
115
  try {
115
- files = changedFiles(root, base);
116
- bytes = diffBytes(root, base);
117
- diffStr = diffText(root, base);
116
+ files = changedFiles(root, base, mode);
117
+ bytes = diffBytes(root, base, mode);
118
+ diffStr = diffText(root, base, mode);
118
119
  } catch (err) {
119
120
  phases.push({ phase: "git_resolution", ok: false, error: err.message });
120
121
  appendTelemetry(root, "gate_finished", { ok: false, code: 1, error: err.message });
@@ -493,9 +494,9 @@ export async function dispatch(task, opts = {}) {
493
494
  const cleanPrompt = redactSecrets(task.prompt || "");
494
495
 
495
496
  // Wire Prompt Guard & Envelope to wrap raw task arguments
496
- const untrustedData = task.untrustedData || [cleanPrompt];
497
+ const taskInstructions = task.taskInstructions || cleanPrompt || task.title || "Autonomous Task Execution";
498
+ const untrustedData = Array.isArray(task.untrustedData) ? task.untrustedData : [];
497
499
  const systemPolicy = task.systemPolicy || config.systemPolicy || "";
498
- const taskInstructions = task.taskInstructions || task.title || "Autonomous Task Execution";
499
500
  const envelopedPrompt = buildAgentEnvelope(systemPolicy, taskInstructions, untrustedData);
500
501
 
501
502
  const cleanTask = { ...task, prompt: envelopedPrompt };
@@ -547,7 +548,7 @@ export async function run(tasksOrOpts = {}, opts = {}) {
547
548
 
548
549
  let filesToProcess = [];
549
550
  if (Array.isArray(tasks)) {
550
- filesToProcess = tasks.map((t) => (typeof t === "string" ? t : t.id || t.file || t.title));
551
+ filesToProcess = tasks;
551
552
  } else {
552
553
  filesToProcess = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
553
554
  }
@@ -558,21 +559,27 @@ export async function run(tasksOrOpts = {}, opts = {}) {
558
559
  const batch = filesToProcess.slice(i, i + concurrency);
559
560
  await Promise.all(
560
561
  batch.map(async (fileOrItem) => {
561
- const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem.id || fileOrItem.file;
562
+ const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem.id || fileOrItem.file || fileOrItem.title || "task";
562
563
  const srcPath = join(queueDir, fileName);
563
564
  try {
564
565
  let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
566
+ let title = typeof fileOrItem === "object" && fileOrItem.title ? fileOrItem.title : fileName;
565
567
  if (!prompt && existsSync(srcPath)) {
566
568
  prompt = await fs.promises.readFile(srcPath, "utf-8");
567
569
  }
568
- const task = { title: fileName, prompt };
570
+ const task = typeof fileOrItem === "object" ? { ...fileOrItem, title, prompt } : { title, prompt };
569
571
  const session = await dispatch(task, { root, config, dryRun: isDry });
570
- const dstPath = join(completedDir, fileName);
571
- if (existsSync(srcPath)) {
572
- renameSync(srcPath, dstPath);
572
+
573
+ if (session && session.ok === false) {
574
+ results.push({ file: fileName, ok: false, status: session.status, error: session.error, session });
575
+ } else {
576
+ const dstPath = join(completedDir, fileName);
577
+ if (existsSync(srcPath)) {
578
+ renameSync(srcPath, dstPath);
579
+ }
580
+ appendLedger({ event: "task_completed", file: fileName, session }, root);
581
+ results.push({ file: fileName, ok: true, session });
573
582
  }
574
- appendLedger({ event: "task_completed", file: fileName, session }, root);
575
- results.push({ file: fileName, ok: true, session });
576
583
  } catch (err) {
577
584
  results.push({ file: fileName, ok: false, error: err.message });
578
585
  }
package/src/git.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { normalizePath } from "./config.mjs";
3
5
 
4
6
 
@@ -23,23 +25,35 @@ export function runCmd(command, opts = {}) {
23
25
 
24
26
  let binary = "";
25
27
  let args = [];
28
+ let useShell = false;
29
+ let shellCmd = "";
26
30
 
27
31
  if (Array.isArray(command)) {
28
32
  binary = command[0];
29
33
  args = command.slice(1);
30
34
  } else if (typeof command === "string") {
31
- const tokens = command.trim().split(/\s+/).filter(Boolean);
32
- binary = tokens[0] || "";
33
- args = tokens.slice(1);
35
+ const trimmed = command.trim();
36
+ if (/[\&\|\>\<\$\"\'\n\;]/.test(trimmed)) {
37
+ useShell = true;
38
+ shellCmd = trimmed;
39
+ } else {
40
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
41
+ binary = tokens[0] || "";
42
+ args = tokens.slice(1);
43
+ }
34
44
  }
35
45
 
36
- if (!binary) {
46
+ if (!binary && !useShell) {
37
47
  if (opts.ignoreError) return { status: 0, stdout: "", stderr: "" };
38
48
  throw new GateError("Empty command provided");
39
49
  }
40
50
 
41
51
  try {
42
- const stdout = execFileSync(binary, args, {
52
+ const isWin = process.platform === "win32";
53
+ const execBin = useShell ? (isWin ? (process.env.ComSpec || "cmd.exe") : "/bin/sh") : binary;
54
+ const execArgs = useShell ? (isWin ? ["/d", "/s", "/c", shellCmd] : ["-c", shellCmd]) : args;
55
+
56
+ const stdout = execFileSync(execBin, execArgs, {
43
57
  cwd,
44
58
  encoding: "utf-8",
45
59
  shell: false,
@@ -69,10 +83,10 @@ export function runCmd(command, opts = {}) {
69
83
  }
70
84
 
71
85
  if (isTimeout) {
72
- throw new GateError(`Command execution timed out (ETIMEDOUT): ${binary}`, { code: status });
86
+ throw new GateError(`Command execution timed out (ETIMEDOUT): ${useShell ? shellCmd : binary}`, { code: status });
73
87
  }
74
88
  if (isNobufs) {
75
- throw new GateError(`Command output buffer exceeded limit (ENOBUFS): ${binary}`, { code: status });
89
+ throw new GateError(`Command output buffer exceeded limit (ENOBUFS): ${useShell ? shellCmd : binary}`, { code: status });
76
90
  }
77
91
 
78
92
  throw err;
@@ -138,29 +152,55 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
138
152
  );
139
153
  }
140
154
 
141
- export function changedFiles(root = process.cwd(), base = "main") {
155
+ export function changedFiles(root = process.cwd(), base = "main", mode = "committed") {
142
156
  const resolvedRef = resolveBase(root, base);
143
- const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], {
144
- cwd: root,
145
- raw: true,
146
- });
147
- return raw
148
- .split("\0")
149
- .map(normalizePath)
150
- .filter(Boolean);
157
+ if (mode === "working-tree" || mode === "working") {
158
+ const committedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
159
+ const uncommittedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
160
+ const untrackedRaw = git(["-c", "core.quotePath=false", "ls-files", "-z", "--others", "--exclude-standard"], { cwd: root, raw: true, ignoreError: true }) || "";
161
+
162
+ const set = new Set([
163
+ ...committedRaw.split("\0").map(normalizePath).filter(Boolean),
164
+ ...uncommittedRaw.split("\0").map(normalizePath).filter(Boolean),
165
+ ...untrackedRaw.split("\0").map(normalizePath).filter(Boolean),
166
+ ]);
167
+ return Array.from(set);
168
+ } else if (mode === "staged" || mode === "index") {
169
+ const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "--cached", resolvedRef], { cwd: root, raw: true });
170
+ return raw.split("\0").map(normalizePath).filter(Boolean);
171
+ } else {
172
+ const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
173
+ return raw.split("\0").map(normalizePath).filter(Boolean);
174
+ }
151
175
  }
152
176
 
153
- export function diffBytes(root = process.cwd(), base = "main") {
177
+ export function diffText(root = process.cwd(), base = "main", mode = "committed") {
154
178
  const resolvedRef = resolveBase(root, base);
155
- const raw = git(["diff", "--shortstat", `${resolvedRef}...HEAD`], { cwd: root });
156
- if (!raw) return 0;
157
- const diffStr = git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
158
- return Buffer.byteLength(diffStr, "utf-8");
179
+ if (mode === "working-tree" || mode === "working") {
180
+ const committed = git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
181
+ const uncommitted = git(["diff", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
182
+
183
+ const untrackedRaw = git(["-c", "core.quotePath=false", "ls-files", "-z", "--others", "--exclude-standard"], { cwd: root, raw: true, ignoreError: true }) || "";
184
+ const untrackedFiles = untrackedRaw.split("\0").map(normalizePath).filter(Boolean);
185
+ let untrackedDiff = "";
186
+ for (const file of untrackedFiles) {
187
+ try {
188
+ const fullPath = join(root, file);
189
+ if (existsSync(fullPath)) {
190
+ const content = readFileSync(fullPath, "utf-8");
191
+ untrackedDiff += `\ndiff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n`;
192
+ untrackedDiff += content.split("\n").map((line) => `+${line}`).join("\n") + "\n";
193
+ }
194
+ } catch (_) {}
195
+ }
196
+ return [committed, uncommitted, untrackedDiff].filter(Boolean).join("\n");
197
+ }
198
+ return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
159
199
  }
160
200
 
161
- export function diffText(root = process.cwd(), base = "main") {
162
- const resolvedRef = resolveBase(root, base);
163
- return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
201
+ export function diffBytes(root = process.cwd(), base = "main", mode = "committed") {
202
+ const text = diffText(root, base, mode);
203
+ return Buffer.byteLength(text, "utf-8");
164
204
  }
165
205
 
166
206
  export function showFromOrigin(root = process.cwd(), base = "main", filePath = "") {
package/src/mcp.mjs CHANGED
@@ -9,7 +9,7 @@ import { ProgressBus } from "./mcp-progress.mjs";
9
9
 
10
10
  export const MCP_SERVER_INFO = {
11
11
  name: "jules-orchestrator-kit",
12
- version: "0.25.1",
12
+ version: "0.28.2",
13
13
  };
14
14
 
15
15
  export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
@@ -96,7 +96,10 @@ export function buildAgentEnvelope(systemPolicy = "", taskInstructions = "", unt
96
96
  }
97
97
 
98
98
  const sections = [];
99
- sections.push(`SYSTEM WARNING: ${warning}`);
99
+
100
+ if (sanitizedBlocks.length > 0) {
101
+ sections.push(`SYSTEM WARNING: ${warning}`);
102
+ }
100
103
 
101
104
  if (systemPolicy && typeof systemPolicy === "string" && systemPolicy.trim()) {
102
105
  sections.push(`[SYSTEM POLICY]\n${systemPolicy.trim()}`);
package/src/provider.mjs CHANGED
@@ -3,15 +3,20 @@ import { spawnSync } from "node:child_process";
3
3
  export const JULES_PRESET = {
4
4
  name: "jules",
5
5
  type: "http",
6
- url: "https://jules.googleapis.com/v1/projects/{project}/sessions",
6
+ url: "https://jules.googleapis.com/v1alpha/sessions",
7
7
  headers: {
8
- "Authorization": "Bearer {token}",
8
+ "X-Goog-Api-Key": "{token}",
9
9
  "Content-Type": "application/json",
10
10
  },
11
11
  bodyTemplate: {
12
- prompt: "{prompt}",
13
12
  title: "{title}",
14
- branch: "{branch}",
13
+ prompt: "{prompt}",
14
+ sourceContext: {
15
+ source: "{source}",
16
+ githubRepoContext: {
17
+ startingBranch: "{branch}",
18
+ },
19
+ },
15
20
  },
16
21
  };
17
22
 
@@ -31,6 +36,14 @@ export const CODEX_PRESET = {
31
36
  promptViaStdin: false,
32
37
  };
33
38
 
39
+ export class MissingApiKeyError extends Error {
40
+ constructor(message = "JULES_API_KEY environment variable is required for Google Jules API dispatch.") {
41
+ super(message);
42
+ this.name = "MissingApiKeyError";
43
+ this.status = 401;
44
+ }
45
+ }
46
+
34
47
  export class ProviderRateLimitError extends Error {
35
48
  constructor(message, opts = {}) {
36
49
  super(message);
@@ -92,6 +105,9 @@ function interpolateDeep(node, data) {
92
105
  * Creates an HTTP or Exec provider adapter based on spec/config.
93
106
  */
94
107
  export function createProvider(spec = "jules", config = {}) {
108
+ if (spec && typeof spec === "object" && typeof spec.dispatch === "function") {
109
+ return spec;
110
+ }
95
111
  const providerSpec = typeof spec === "string"
96
112
  ? (spec === "claude-code" ? CLAUDE_PRESET : spec === "codex" ? CODEX_PRESET : JULES_PRESET)
97
113
  : spec;
@@ -100,11 +116,28 @@ export function createProvider(spec = "jules", config = {}) {
100
116
  name: providerSpec.name || "custom-provider",
101
117
 
102
118
  async dispatch(task, ctx = {}) {
103
- const rawToken = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
119
+ const rawToken = process.env.JULES_API_KEY || (ctx.allowLegacyKey ? process.env.GEMINI_API_KEY : "") || "";
120
+ if (!rawToken && !ctx.dryRun && providerSpec.name === "jules") {
121
+ throw new MissingApiKeyError();
122
+ }
123
+
124
+ const isRepoless = Boolean(task.repoless || ctx.repoless);
125
+ const rawRepo = task.source || ctx.source || process.env.JULES_REPO || "";
126
+ if (!rawRepo && !ctx.dryRun && providerSpec.name === "jules" && !isRepoless) {
127
+ throw new Error("Missing connected Jules repository source. Set JULES_REPO or pass source/repoless option.");
128
+ }
129
+
130
+ const sourceName = rawRepo
131
+ ? (rawRepo.startsWith("sources/") ? rawRepo : `sources/github/${rawRepo}`)
132
+ : undefined;
133
+
134
+ const startingBranch = task.branch || ctx.branch || config.baseBranch || process.env.BASE_BRANCH || "main";
135
+
104
136
  const data = {
105
137
  prompt: task.prompt || "",
106
138
  title: task.title || "Agent Task",
107
- branch: task.branch || (config.branchPrefix ? config.branchPrefix + "task" : "agent/task"),
139
+ branch: startingBranch,
140
+ source: sourceName || "",
108
141
  project: process.env.JULES_PROJECT_ID || "default",
109
142
  ...ctx,
110
143
  };
@@ -135,19 +168,31 @@ export function createProvider(spec = "jules", config = {}) {
135
168
  }
136
169
 
137
170
  let body;
171
+ let bodyObj;
138
172
  if (typeof providerSpec.bodyTemplate === "string") {
139
173
  try {
140
174
  const parsedObj = JSON.parse(providerSpec.bodyTemplate);
141
- const interpolatedObj = interpolateDeep(parsedObj, data);
142
- body = JSON.stringify(interpolatedObj);
175
+ bodyObj = interpolateDeep(parsedObj, data);
143
176
  } catch (_) {
144
177
  body = interpolateString(providerSpec.bodyTemplate, data);
145
178
  }
146
179
  } else if (providerSpec.bodyTemplate) {
147
- const interpolatedObj = interpolateDeep(providerSpec.bodyTemplate, data);
148
- body = JSON.stringify(interpolatedObj);
180
+ bodyObj = interpolateDeep(providerSpec.bodyTemplate, data);
149
181
  } else {
150
- body = JSON.stringify(data);
182
+ bodyObj = { ...data };
183
+ }
184
+
185
+ if (bodyObj && typeof bodyObj === "object") {
186
+ if (isRepoless || !sourceName) {
187
+ delete bodyObj.sourceContext;
188
+ }
189
+ if (task.autoPr || ctx.autoPr) {
190
+ bodyObj.automationMode = "AUTO_CREATE_PR";
191
+ }
192
+ if (task.requirePlanApproval || ctx.requirePlanApproval) {
193
+ bodyObj.requirePlanApproval = true;
194
+ }
195
+ body = JSON.stringify(bodyObj);
151
196
  }
152
197
 
153
198
  const timeoutMs = ctx.timeoutMs || config.timeoutMs || providerSpec.timeoutMs || 120_000;