jules-orchestrator-kit 0.52.1 → 0.52.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/README.md CHANGED
@@ -143,7 +143,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
143
143
  * **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
144
144
  * **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
145
145
  * **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
146
- * **Verified Test Suite:** Tested with **802 unit tests across 91 suites passing in < 10.0s**.
146
+ * **Verified Test Suite:** Tested with **809 unit tests across 91 suites passing in < 10.0s**.
147
147
 
148
148
  <br/>
149
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.52.1",
3
+ "version": "0.52.3",
4
4
  "description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/coverage.mjs CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, readdirSync, mkdtempSync, rmSync, existsSync, realpathSyn
2
2
  import { join, resolve, relative, isAbsolute } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
- import { execSync } from "node:child_process";
5
+ import { runCmd } from "./git.mjs";
6
6
  import { resolveVerify } from "./config.mjs";
7
7
 
8
8
  /**
@@ -136,23 +136,15 @@ export function runV8Coverage(testCmd, options = {}) {
136
136
  }
137
137
  }
138
138
 
139
- let stdout = "";
140
- let stderr = "";
141
- let exitCode = 0;
142
-
143
- try {
144
- stdout = execSync(cmd, {
145
- cwd: root,
146
- env,
147
- timeout: timeoutMs,
148
- stdio: ["ignore", "pipe", "pipe"],
149
- encoding: "utf-8",
150
- });
151
- } catch (err) {
152
- exitCode = err.status || 1;
153
- stdout = err.stdout ? String(err.stdout) : "";
154
- stderr = err.stderr ? String(err.stderr) : err.message;
155
- }
139
+ const res = runCmd(cmd, {
140
+ cwd: root,
141
+ env,
142
+ timeout: timeoutMs,
143
+ ignoreError: true,
144
+ });
145
+ const exitCode = res.status;
146
+ const stdout = res.stdout;
147
+ const stderr = res.stderr;
156
148
 
157
149
  const coverageByFile = new Map();
158
150
  const files = existsSync(tempCoverageDir) ? readdirSync(tempCoverageDir) : [];
package/src/git.mjs CHANGED
@@ -412,3 +412,49 @@ export function worktreeRemove(root = process.cwd(), targetDir = "") {
412
412
  export function worktreePrune(root = process.cwd()) {
413
413
  return git(["worktree", "prune"], { cwd: root });
414
414
  }
415
+
416
+ /**
417
+ * Extract 'owner/repo' from a git remote URL if it points to GitHub.
418
+ * Handles SSH (git@github.com:owner/repo.git), HTTPS (https://github.com/owner/repo.git),
419
+ * protocol prefixes (ssh://, git://) and URL-embedded credentials.
420
+ * @param {string} remoteUrl
421
+ * @returns {string} 'owner/repo' or '' if invalid/not GitHub
422
+ */
423
+ export function parseGitHubRepo(remoteUrl) {
424
+ if (!remoteUrl || typeof remoteUrl !== "string") return "";
425
+ const trimmed = remoteUrl.trim();
426
+
427
+ // Match git@github.com:owner/repo(.git)
428
+ const sshMatch = trimmed.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i);
429
+ if (sshMatch) {
430
+ return `${sshMatch[1]}/${sshMatch[2]}`;
431
+ }
432
+
433
+ // Match https://github.com/owner/repo(.git), http, ssh://, git://
434
+ try {
435
+ const urlObj = new URL(trimmed.startsWith("git@") ? `ssh://${trimmed}` : trimmed);
436
+ if (urlObj.hostname.toLowerCase() === "github.com") {
437
+ const parts = urlObj.pathname.replace(/^\/+/, "").replace(/\.git$/i, "").split("/");
438
+ if (parts.length >= 2 && parts[0] && parts[1]) {
439
+ return `${parts[0]}/${parts[1]}`;
440
+ }
441
+ }
442
+ } catch (_) {}
443
+
444
+ return "";
445
+ }
446
+
447
+ /**
448
+ * Attempt to resolve 'owner/repo' from the local repository's git remote origin.
449
+ * @param {string} [root=process.cwd()]
450
+ * @returns {string}
451
+ */
452
+ export function resolveGitRemoteOrigin(root = process.cwd()) {
453
+ try {
454
+ const raw = git(["config", "--get", "remote.origin.url"], { cwd: root, ignoreError: true });
455
+ return parseGitHubRepo(raw);
456
+ } catch (_) {
457
+ return "";
458
+ }
459
+ }
460
+
package/src/mutation.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
2
  import { resolve, isAbsolute } from "node:path";
3
- import { execSync } from "node:child_process";
4
3
  import { createHash } from "node:crypto";
5
- import { diffText } from "./git.mjs";
4
+ import { diffText, runCmd } from "./git.mjs";
6
5
 
7
6
  /**
8
7
  * @typedef {Object} MutationCandidate
@@ -574,22 +573,17 @@ export function executeMutant(mutant, options = {}) {
574
573
  stdout = execResult.stdout || "";
575
574
  stderr = execResult.stderr || "";
576
575
  } else {
577
- try {
578
- stdout = execSync(testCmd, {
579
- cwd: root,
580
- timeout: timeoutMs,
581
- stdio: ["ignore", "pipe", "pipe"],
582
- env: { ...process.env, CI: "true", JULES_MUTATION_RUN: "true" },
583
- encoding: "utf-8",
584
- });
585
- exitCode = 0;
586
- } catch (execErr) {
587
- exitCode = execErr.status || execErr.statusCode || 1;
588
- stdout = execErr.stdout ? execErr.stdout.toString() : "";
589
- stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
590
- if (execErr.code === "ETIMEDOUT" || execErr.killed) {
591
- status = "TIMEOUT";
592
- }
576
+ const execResult = runCmd(testCmd, {
577
+ cwd: root,
578
+ timeout: timeoutMs,
579
+ env: { ...process.env, CI: "true", JULES_MUTATION_RUN: "true" },
580
+ ignoreError: true,
581
+ });
582
+ exitCode = execResult.status;
583
+ stdout = execResult.stdout;
584
+ stderr = execResult.stderr;
585
+ if (execResult.status === 124 || stderr.includes("ETIMEDOUT")) {
586
+ status = "TIMEOUT";
593
587
  }
594
588
  }
595
589
 
package/src/perf.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { monitorEventLoopDelay } from "node:perf_hooks";
2
- import { execSync } from "node:child_process";
2
+ import { runCmd } from "./git.mjs";
3
3
  import { resolveVerify } from "./config.mjs";
4
4
 
5
5
  /**
@@ -41,19 +41,15 @@ export function measureEventLoopDelay(target, options = {}) {
41
41
  }
42
42
  }
43
43
 
44
- try {
45
- stdout = execSync(cmd, {
46
- cwd: root,
47
- env,
48
- timeout: timeoutMs,
49
- stdio: ["ignore", "pipe", "pipe"],
50
- encoding: "utf-8",
51
- });
52
- } catch (err) {
53
- exitCode = err.status || 1;
54
- stdout = err.stdout ? String(err.stdout) : "";
55
- stderr = err.stderr ? String(err.stderr) : err.message;
56
- }
44
+ const execResult = runCmd(cmd, {
45
+ cwd: root,
46
+ env,
47
+ timeout: timeoutMs,
48
+ ignoreError: true,
49
+ });
50
+ exitCode = execResult.status;
51
+ stdout = execResult.stdout;
52
+ stderr = execResult.stderr;
57
53
  }
58
54
  } finally {
59
55
  histogram.disable();
package/src/provider.mjs CHANGED
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join, extname } from "node:path";
4
4
  import { redactSecrets } from "./security.mjs";
5
- import { resolveWindowsSpawn } from "./git.mjs";
5
+ import { resolveWindowsSpawn, resolveGitRemoteOrigin } from "./git.mjs";
6
6
 
7
7
  // Extensions node --check can parse as a script. .mjs/.cjs are unambiguous;
8
8
  // plain .js is checked as a script too — a stray top-level ESM import would
@@ -301,7 +301,9 @@ export function createProvider(spec = "jules", config = {}) {
301
301
  }
302
302
 
303
303
  const isRepoless = Boolean(task.repoless || ctx.repoless);
304
- const rawRepo = task.source || ctx.source || config.source || process.env.JULES_REPO || "";
304
+ const root = ctx.root || config.root || process.cwd();
305
+ const detectedRepo = resolveGitRemoteOrigin(root);
306
+ const rawRepo = task.source || ctx.source || config.source || process.env.JULES_REPO || detectedRepo || "";
305
307
  if (!rawRepo && !ctx.dryRun && providerSpec.name === "jules" && !isRepoless) {
306
308
  throw new Error("Missing connected Jules repository source. Set JULES_REPO or pass source/repoless option.");
307
309
  }
@@ -951,7 +953,7 @@ export function createProvider(spec = "jules", config = {}) {
951
953
  if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
952
954
  if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
953
955
  const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
954
- const res = await this.getSession("", { ...ctx, customUrl: url });
956
+ const res = await this.getSession(sessionId, { ...ctx, customUrl: url });
955
957
  return {
956
958
  activities: res?.raw?.activities || [],
957
959
  nextPageToken: res?.raw?.nextPageToken,
@@ -285,7 +285,7 @@ export function createWhackAMoleDetector(opts = {}) {
285
285
  const oscillatingTests = [...cycleTests];
286
286
 
287
287
  const testSummary = oscillatingTests.length > 0 ? oscillatingTests.join(" <-> ") : "tests";
288
- const promptDirective = `[WHACK_A_MOLE_WARNING] You are trapped in a local optimization cycle where fixing one test breaks another (${testSummary}). Do not add more conditional edge-case band-aids. Revert recent patches and refactor the core logic cleanly.`;
288
+ const promptDirective = `[WHACK_A_MOLE_WARNING] You are trapped in a local optimization cycle where fixing one test breaks another (<UNTRUSTED>${testSummary}</UNTRUSTED>). Do not add more conditional edge-case band-aids. Revert recent patches and refactor the core logic cleanly.`;
289
289
 
290
290
  return {
291
291
  whackAMole: true,
@@ -68,17 +68,25 @@ export async function extractSessionPatch(sessionId, opts = {}) {
68
68
  // 2. Inspect activity artifacts and logs if patch wasn't in top-level output
69
69
  for (const act of activities) {
70
70
  if (act.pullRequest) pr = act.pullRequest;
71
- if (act.gitPatch || act.patch || act.diff) {
72
- patch = patch || act.gitPatch || act.patch || act.diff;
71
+ if (act.gitPatch || act.patch || act.diff || act.changeSet?.gitPatch?.unidiffPatch) {
72
+ patch = patch || act.gitPatch || act.patch || act.diff || act.changeSet?.gitPatch?.unidiffPatch;
73
73
  }
74
- if (act.artifacts && Array.isArray(act.artifacts)) {
75
- for (const art of act.artifacts) {
76
- if (art.patch || art.diff || (typeof art.content === "string" && art.content.startsWith("diff --git"))) {
77
- patch = patch || art.patch || art.diff || art.content;
78
- }
79
- if (art.path || art.filename) {
80
- files.add(art.path || art.filename);
81
- }
74
+ const artifactList = [
75
+ ...(Array.isArray(act.artifacts) ? act.artifacts : []),
76
+ ...(Array.isArray(act.artifactsCreated?.artifacts) ? act.artifactsCreated.artifacts : []),
77
+ ];
78
+ for (const art of artifactList) {
79
+ const artPatch =
80
+ art.patch ||
81
+ art.diff ||
82
+ art.changeSet?.gitPatch?.unidiffPatch ||
83
+ art.gitPatch?.unidiffPatch ||
84
+ (typeof art.content === "string" && art.content.startsWith("diff --git") ? art.content : "");
85
+ if (artPatch) {
86
+ patch = patch || artPatch;
87
+ }
88
+ if (art.path || art.filename) {
89
+ files.add(art.path || art.filename);
82
90
  }
83
91
  }
84
92
  if (act.changedFiles && Array.isArray(act.changedFiles)) {
@@ -663,8 +663,28 @@ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
663
663
  const detected = detectPolyglotStack(root);
664
664
  const configPath = join(root, ".agent", "config.yml");
665
665
  const hasConfig = existsSync(configPath);
666
+ let existingTestCmd = "";
667
+ if (hasConfig) {
668
+ try {
669
+ const raw = readFileSync(configPath, "utf-8");
670
+ const match = raw.match(/^\s*test:\s*["']?(.*?)["']?\s*$/m) || raw.match(/^\s*test_cmd:\s*["']?(.*?)["']?\s*$/m);
671
+ if (match) {
672
+ existingTestCmd = match[1].trim();
673
+ }
674
+ } catch (_) {}
675
+ }
676
+
677
+ let hasPkgTest = false;
678
+ if (existsSync(join(root, "package.json"))) {
679
+ try {
680
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
681
+ hasPkgTest = Boolean(pkg?.scripts?.test);
682
+ } catch (_) {}
683
+ }
684
+
666
685
  const hasRealTestSuite =
667
- hasConfig ||
686
+ (hasConfig && Boolean(existingTestCmd)) ||
687
+ hasPkgTest ||
668
688
  existsSync(join(root, "phpunit.xml")) ||
669
689
  existsSync(join(root, "pest.php")) ||
670
690
  existsSync(join(root, "turbo.json")) ||
@@ -672,7 +692,7 @@ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
672
692
  existsSync(join(root, "nx.json"));
673
693
 
674
694
  if (!options.force && hasRealTestSuite) {
675
- return { bootstrapped: false, reason: "EXISTING_VERIFICATION_ORACLE", testCmd: detected.testCmd };
695
+ return { bootstrapped: false, reason: "EXISTING_VERIFICATION_ORACLE", testCmd: existingTestCmd || (hasPkgTest ? detected.testCmd : "") };
676
696
  }
677
697
 
678
698
  let testCmd = "";
@@ -697,8 +717,41 @@ export function bootstrapZeroTestRepo(root = process.cwd(), options = {}) {
697
717
  mkdirSync(join(root, ".agent"), { recursive: true });
698
718
  } catch (_) {}
699
719
 
700
- const cfg = `version: 1\nprovider: jules\ntier: ultra\nverify:\n test: "${testCmd}"\n build: "${detected.buildCmd || ""}"\nlimits:\n diff_kb: 75\n daily_tasks: 300\n repair_attempts: 3\nbranch_prefix: agent/\nbase_branch: main\n`;
701
- writeFileSync(configPath, cfg, "utf-8");
720
+ if (hasConfig) {
721
+ try {
722
+ let rawConfig = readFileSync(configPath, "utf-8");
723
+ if (/^\s*test:\s*.*$/m.test(rawConfig)) {
724
+ rawConfig = rawConfig.replace(/^\s*test:\s*.*$/m, ` test: "${testCmd}"`);
725
+ } else if (/^\s*verify:\s*$/m.test(rawConfig)) {
726
+ rawConfig = rawConfig.replace(/^\s*verify:\s*$/m, `verify:\n test: "${testCmd}"`);
727
+ } else {
728
+ rawConfig += `\nverify:\n test: "${testCmd}"\n`;
729
+ }
730
+ if (detected.buildCmd && /^\s*build:\s*["']?["']?\s*$/m.test(rawConfig)) {
731
+ rawConfig = rawConfig.replace(/^\s*build:\s*.*$/m, ` build: "${detected.buildCmd}"`);
732
+ }
733
+ writeFileSync(configPath, rawConfig, "utf-8");
734
+ } catch (_) {}
735
+ } else {
736
+ const cfg = `version: 1\nprovider: jules\ntier: free\nverify:\n test: "${testCmd}"\n build: "${detected.buildCmd || ""}"\nlimits:\n diff_kb: 75\n daily_tasks: 15\n repair_attempts: 3\nbranch_prefix: agent/\nbase_branch: main\n`;
737
+ writeFileSync(configPath, cfg, "utf-8");
738
+ }
739
+
740
+ const julesPath = join(root, ".agent", "jules.yml");
741
+ if (existsSync(julesPath)) {
742
+ try {
743
+ let rawJules = readFileSync(julesPath, "utf-8");
744
+ if (/^\s*test_cmd:\s*.*$/m.test(rawJules)) {
745
+ rawJules = rawJules.replace(/^\s*test_cmd:\s*.*$/m, `test_cmd: "${testCmd}"`);
746
+ } else {
747
+ rawJules += `\ntest_cmd: "${testCmd}"\n`;
748
+ }
749
+ if (detected.buildCmd && /^\s*build_cmd:\s*["']?["']?\s*$/m.test(rawJules)) {
750
+ rawJules = rawJules.replace(/^\s*build_cmd:\s*.*$/m, `build_cmd: "${detected.buildCmd}"`);
751
+ }
752
+ writeFileSync(julesPath, rawJules, "utf-8");
753
+ } catch (_) {}
754
+ }
702
755
 
703
756
  return { bootstrapped: true, stack: detected.stack, testCmd, configPath };
704
757
  }
package/src/webhook.mjs CHANGED
@@ -480,25 +480,27 @@ export function verifySignature(payload, signatureHeader, secret) {
480
480
  if (!/^[0-9a-fA-F]{64}$/.test(hexSignature)) return false;
481
481
 
482
482
  const hmac = createHmac("sha256", secret);
483
- const bodyBuf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf-8");
483
+ const encoder = new TextEncoder();
484
+ const bodyBuf = payload instanceof Uint8Array ? payload : encoder.encode(payload);
484
485
  const digest = `sha256=${hmac.update(bodyBuf).digest("hex")}`;
485
486
 
486
- const sigBuf = Buffer.from(signatureHeader, "utf-8");
487
- const digestBuf = Buffer.from(digest, "utf-8");
487
+ const sigBuf = encoder.encode(signatureHeader);
488
+ const digestBuf = encoder.encode(digest);
488
489
 
489
- if (sigBuf.length !== digestBuf.length) return false;
490
+ if (sigBuf.byteLength !== digestBuf.byteLength) return false;
490
491
  return timingSafeEqual(sigBuf, digestBuf);
491
492
  }
492
493
 
493
494
  /**
494
495
  * Parse incoming webhook body buffer into a JS object.
495
- * @param {Buffer} bodyBuf
496
+ * @param {Buffer|Uint8Array|string} bodyBuf
496
497
  * @param {string} contentType
497
498
  * @returns {object} Parsed JSON payload
498
499
  */
499
500
  export function parseWebhookPayload(bodyBuf, contentType = "application/json") {
500
- if (!bodyBuf || bodyBuf.length === 0) return {};
501
- const str = bodyBuf.toString("utf-8").trim();
501
+ if (!bodyBuf || (bodyBuf.length === 0 && bodyBuf.byteLength === 0)) return {};
502
+ const decoder = new TextDecoder();
503
+ const str = (bodyBuf instanceof Uint8Array ? decoder.decode(bodyBuf) : String(bodyBuf)).trim();
502
504
  if (!str) return {};
503
505
 
504
506
  if (contentType.includes("application/x-www-form-urlencoded")) {