jules-orchestrator-kit 0.52.2 → 0.52.4

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 **806 unit tests across 91 suites passing in < 10.0s**.
146
+ * **Verified Test Suite:** Tested with **815 unit tests across 91 suites passing in < 10.0s**.
147
147
 
148
148
  <br/>
149
149
 
package/bin/agentctl.mjs CHANGED
@@ -427,7 +427,10 @@ async function main() {
427
427
  } else if (res.flakyVerdict?.verdict === "QUARANTINED") {
428
428
  console.log(`💡 Remediation Hint (Exit 8 Flaky Test Quarantined):`);
429
429
  console.log(` • This command has alternated between pass and fail across recent runs.`);
430
- console.log(` • Fix the test's non-determinism — re-running will not clear the verdict.\n`);
430
+ } else if (res.code === 188) {
431
+ console.log(`💡 Remediation Hint (Exit 188 Offline Network Violation / Infrastructure):`);
432
+ console.log(` • An unmocked network request was blocked by the preload network guard during verification.`);
433
+ console.log(` • Ensure dependencies are installed locally (run: npm install) and all network calls in tests are mocked.\n`);
431
434
  } else if (failedPhase === "verify" || failedPhase === "evidence") {
432
435
  console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
433
436
  console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.52.2",
3
+ "version": "0.52.4",
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/engine.mjs CHANGED
@@ -433,6 +433,14 @@ export async function gate(opts = {}) {
433
433
  // the one gate failure the operator is expected to fix themselves was the
434
434
  // only one that told them nothing about how. Both streams are already
435
435
  // redacted at the point they were captured.
436
+ const isNetViolation = failingCmd?.status === 188;
437
+ const netViolationDiagnostics = isNetViolation
438
+ ? [
439
+ "Offline Network Guard: Outbound network request was blocked (Exit 188).",
440
+ "Ensure dependencies are installed locally (run npm install) and tests do not perform unmocked network access.",
441
+ ]
442
+ : [];
443
+
436
444
  const verifyFailure = failingCmd
437
445
  ? {
438
446
  stageId: failingCmd.stageId || failingCmd.phase || "verify",
@@ -440,7 +448,7 @@ export async function gate(opts = {}) {
440
448
  exitCode: failingCmd.status ?? null,
441
449
  stdout: failingCmd.stdout || "",
442
450
  stderr: failingCmd.stderr || "",
443
- diagnostics: failingCmd.diagnostics || [],
451
+ diagnostics: [...netViolationDiagnostics, ...(failingCmd.diagnostics || [])],
444
452
  }
445
453
  : testTampered
446
454
  ? {
@@ -510,6 +518,10 @@ export async function gate(opts = {}) {
510
518
  }
511
519
 
512
520
  if (!verifyOk && opts.fix && failingCmd) {
521
+ if (isNetViolation) {
522
+ appendTelemetry(root, "gate_finished", { ok: false, code: 188, networkViolation: true });
523
+ return { ok: false, code: 188, phases, evidence: evidenceManifest };
524
+ }
513
525
  const repairs = await repair(failingCmd, { config, root, signal: opts.signal, progressBus, progressToken });
514
526
  const finalOk = repairs.ok;
515
527
  const finalCode = repairs.ok ? 0 : 4;
@@ -520,7 +532,7 @@ export async function gate(opts = {}) {
520
532
  return { ok: false, code: 4, phases, repairs, evidence: evidenceManifest };
521
533
  }
522
534
 
523
- const code = verifyOk ? 0 : testTampered ? 3 : 4;
535
+ const code = verifyOk ? 0 : isNetViolation ? 188 : testTampered ? 3 : 4;
524
536
  appendTelemetry(root, "gate_finished", { ok: verifyOk, code });
525
537
  return { ok: verifyOk, code, phases, evidence: evidenceManifest };
526
538
  }
package/src/git.mjs CHANGED
@@ -306,7 +306,10 @@ export function ensureBaseFetched(root = process.cwd(), baseRef = "main") {
306
306
  }
307
307
 
308
308
  export function resolveBase(root = process.cwd(), baseRef = "main") {
309
- const candidates = [`origin/${baseRef}`, `refs/remotes/origin/${baseRef}`, baseRef];
309
+ const isHeadRef = baseRef === "HEAD" || baseRef.startsWith("HEAD~") || baseRef.startsWith("HEAD^") || baseRef.startsWith("HEAD@");
310
+ const candidates = isHeadRef
311
+ ? [baseRef]
312
+ : [`origin/${baseRef}`, `refs/remotes/origin/${baseRef}`, baseRef];
310
313
  for (const ref of candidates) {
311
314
  try {
312
315
  const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
@@ -321,20 +324,22 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
321
324
  } catch (_) {}
322
325
  }
323
326
 
324
- ensureBaseFetched(root, baseRef);
327
+ if (!isHeadRef) {
328
+ ensureBaseFetched(root, baseRef);
325
329
 
326
- for (const ref of candidates) {
327
- try {
328
- const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
329
- cwd: root,
330
- encoding: "utf-8",
331
- shell: false,
332
- stdio: ["ignore", "pipe", "ignore"],
333
- });
334
- if (res && res.trim()) {
335
- return res.trim();
336
- }
337
- } catch (_) {}
330
+ for (const ref of candidates) {
331
+ try {
332
+ const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
333
+ cwd: root,
334
+ encoding: "utf-8",
335
+ shell: false,
336
+ stdio: ["ignore", "pipe", "ignore"],
337
+ });
338
+ if (res && res.trim()) {
339
+ return res.trim();
340
+ }
341
+ } catch (_) {}
342
+ }
338
343
  }
339
344
 
340
345
  throw new GateError(
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
@@ -953,7 +953,7 @@ export function createProvider(spec = "jules", config = {}) {
953
953
  if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
954
954
  if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
955
955
  const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
956
- const res = await this.getSession("", { ...ctx, customUrl: url });
956
+ const res = await this.getSession(sessionId, { ...ctx, customUrl: url });
957
957
  return {
958
958
  activities: res?.raw?.activities || [],
959
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,
package/src/security.mjs CHANGED
@@ -588,6 +588,11 @@ function secretScanVariants(addedLines) {
588
588
  // Collapse method concatenations like .concat("...") or .join("")
589
589
  dejoined = dejoined.replace(/\.concat\(\s*["'`]/g, "").replace(/\.join\(\s*["'`]{2}\s*\)/g, "");
590
590
 
591
+ // Collapse whitespace/newlines between adjacent base64 characters (including line-wrapped PEM/base64, template literals, and quoted string chunks)
592
+ const base64Dejoined = stripped
593
+ .replace(/([A-Za-z0-9+/=_-])\s*[\r\n]+\s*(?=[A-Za-z0-9+/=_-])/g, "$1")
594
+ .replace(/([A-Za-z0-9+/=_-])["'`]\s*(?:\+\s*)?[\r\n]+\s*["'`]?([A-Za-z0-9+/=_-])/g, "$1$2");
595
+
591
596
  const hexDecoded = tryHexDecodeTokens(dejoined);
592
597
  const pctDecoded = tryPercentDecode(dejoined);
593
598
  // Confusable / NFKD normalisation runs over both the raw text and the
@@ -597,8 +602,9 @@ function secretScanVariants(addedLines) {
597
602
  const confusableDejoined = normalizeSecretText(dejoined);
598
603
 
599
604
  return {
600
- all: [...new Set([addedLines, stripped, dejoined, hexDecoded, pctDecoded, confusable, confusableDejoined])],
605
+ all: [...new Set([addedLines, stripped, dejoined, base64Dejoined, hexDecoded, pctDecoded, confusable, confusableDejoined])],
601
606
  normalized: dejoined,
607
+ base64Normalized: base64Dejoined,
602
608
  };
603
609
  }
604
610
 
@@ -725,9 +731,22 @@ function decodeBase64Blobs(text, onDecoded) {
725
731
  * @returns {boolean}
726
732
  */
727
733
  export function hasEncodedSecret(text) {
734
+ if (!text) return false;
728
735
  const result = decodeBase64Blobs(text);
729
736
  if (result.capped) return true; // Fail closed if cap exceeded
730
- return result.decoded.some((plain) => hasHighConfidenceSecret(plain));
737
+ if (result.decoded.some((plain) => hasHighConfidenceSecret(plain))) return true;
738
+
739
+ if (text.includes("\n") || text.includes("\r")) {
740
+ const collapsed = text
741
+ .replace(/([A-Za-z0-9+/=_-])\s*[\r\n]+\s*(?=[A-Za-z0-9+/=_-])/g, "$1")
742
+ .replace(/([A-Za-z0-9+/=_-])["'`]\s*(?:\+\s*)?[\r\n]+\s*["'`]?([A-Za-z0-9+/=_-])/g, "$1$2");
743
+ if (collapsed !== text) {
744
+ const collapsedResult = decodeBase64Blobs(collapsed);
745
+ if (collapsedResult.capped) return true;
746
+ if (collapsedResult.decoded.some((plain) => hasHighConfidenceSecret(plain))) return true;
747
+ }
748
+ }
749
+ return false;
731
750
  }
732
751
 
733
752
  /**
@@ -784,14 +803,14 @@ function splitDiffByFile(diffText) {
784
803
  * @returns {{ severity: string, type: string, description: string, encoded: boolean }|null}
785
804
  */
786
805
  function classifyAddedLines(addedLines) {
787
- const { all: variants, normalized } = secretScanVariants(addedLines);
806
+ const { all: variants, normalized, base64Normalized } = secretScanVariants(addedLines);
788
807
  if (variants.some((v) => hasHighConfidenceSecret(v))) {
789
808
  return { severity: "CRITICAL", type: "HIGH_CONFIDENCE_SECRET", encoded: false, description: "High-confidence secret pattern detected in added diff lines" };
790
809
  }
791
810
  // Only worth decoding when nothing was found in the clear, and only against
792
811
  // the fully-normalised text: decoding is the expensive step, and the
793
812
  // intermediate variants differ from it in ways base64 blobs do not care about.
794
- if (hasEncodedSecret(normalized)) {
813
+ if (hasEncodedSecret(normalized) || (base64Normalized && hasEncodedSecret(base64Normalized))) {
795
814
  // Same type as the cleartext case: every gate that blocks on
796
815
  // HIGH_CONFIDENCE_SECRET should block on this too, and a new type would
797
816
  // have silently passed through the ones not updated. The description
@@ -868,6 +887,17 @@ export function checkTestTampering(diffOrText = "", options = {}) {
868
887
 
869
888
  const COMMENTED_ASSERTION = /^\+\s*(?:\/\/|\/\*)\s*(?:expect\(|assert\.|assert\(|t\.expect|t\.assert)/i;
870
889
 
890
+ const VACUOUS_ASSERTIONS = [
891
+ { pattern: /\bassert(?:\.ok)?\s*\(\s*true\s*(?:,[^)]*)?\)/i, desc: "Vacuous truth assertion (assert.ok(true))" },
892
+ { pattern: /\bassert\.(?:strictEqual|deepStrictEqual|equal|deepEqual)\s*\(\s*([^,]+?)\s*,\s*\1\s*(?:,[^)]*)?\)/i, desc: "Vacuous identity assertion (assert.equal(X, X))" },
893
+ { pattern: /\bexpect\s*\(\s*true\s*\)\s*\.(?:toBe|toEqual)\s*\(\s*true\s*\)/i, desc: "Vacuous truth expectation (expect(true).toBe(true))" },
894
+ { pattern: /\bexpect\s*\(\s*([^)]+?)\s*\)\s*\.(?:toBe|toEqual)\s*\(\s*\1\s*\)/i, desc: "Vacuous identity expectation (expect(X).toBe(X))" },
895
+ { pattern: /\bexpect\s*\(\s*true\s*\)\s*\.toBeTruthy\s*\(/i, desc: "Vacuous truth expectation (expect(true).toBeTruthy())" },
896
+ { pattern: /\bexpect\s*\(\s*false\s*\)\s*\.toBeFalsy\s*\(/i, desc: "Vacuous falsity expectation (expect(false).toBeFalsy())" },
897
+ { pattern: /\bassert\.(?:isTrue|isOk)\s*\(\s*true\s*(?:,[^)]*)?\)/i, desc: "Vacuous truth assertion (assert.isTrue(true))" },
898
+ { pattern: /\bassert\.(?:isFalse|isNotOk)\s*\(\s*false\s*(?:,[^)]*)?\)/i, desc: "Vacuous falsity assertion (assert.isFalse(false))" },
899
+ ];
900
+
871
901
  for (let i = 0; i < lines.length; i++) {
872
902
  const line = lines[i];
873
903
  if ((line.startsWith("+++ ") || line.startsWith("+++ b/") || line.startsWith("+++ /dev/null")) && !line.startsWith("++++")) {
@@ -902,6 +932,18 @@ export function checkTestTampering(diffOrText = "", options = {}) {
902
932
  }
903
933
  }
904
934
 
935
+ // Check vacuous assertions
936
+ for (const vacuousRule of VACUOUS_ASSERTIONS) {
937
+ if (vacuousRule.pattern.test(addedText)) {
938
+ violations.push({
939
+ file: currentFile,
940
+ line: currentLineNo,
941
+ type: "VACUOUS_ASSERTION",
942
+ reason: `Test Tamper Guard: ${vacuousRule.desc} detected in ${currentFile}${currentLineNo ? `:${currentLineNo}` : ""}`,
943
+ });
944
+ }
945
+ }
946
+
905
947
  // Check commented-out assertions
906
948
  if (COMMENTED_ASSERTION.test(line)) {
907
949
  violations.push({
@@ -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)) {
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")) {
@@ -123,7 +123,7 @@ export function planInit(root = process.cwd(), options = {}) {
123
123
  const tierName = options.tier || FALLBACK_TIER;
124
124
  // An unrecognised name resolves the same way loadConfig() resolves it, so the
125
125
  // scaffolded limits always match what the runtime will later enforce.
126
- const limits = TIER_PROFILES[tierName] || TIER_PROFILES[FALLBACK_TIER];
126
+ const tierPresetLimits = TIER_PROFILES[tierName] || TIER_PROFILES[FALLBACK_TIER];
127
127
 
128
128
  // Preserve existing config if present
129
129
  let existingConfig = {};
@@ -134,6 +134,21 @@ export function planInit(root = process.cwd(), options = {}) {
134
134
  } catch (_) {}
135
135
  }
136
136
 
137
+ // Preserve existing jules.yml if present
138
+ let existingJules = {};
139
+ const existingJulesPath = join(root, ".agent", "jules.yml");
140
+ if (existsSync(existingJulesPath)) {
141
+ try {
142
+ existingJules = parseYaml(readFileSync(existingJulesPath, "utf-8")) || {};
143
+ } catch (_) {}
144
+ }
145
+
146
+ const customLimits = options.limits || existingConfig.limits;
147
+ const isCustomLimits = customLimits && Object.entries(customLimits).some(
148
+ ([k, v]) => v !== undefined && tierPresetLimits[k] !== undefined && Number(v) !== Number(tierPresetLimits[k])
149
+ );
150
+ const limits = isCustomLimits ? { ...tierPresetLimits, ...customLimits } : tierPresetLimits;
151
+
137
152
  const verify = {
138
153
  test: options.testCmd || existingConfig.verify?.test || oracle.candidates.testCmd || "",
139
154
  build: options.buildCmd || existingConfig.verify?.build || oracle.candidates.buildCmd || "",
@@ -143,19 +158,17 @@ export function planInit(root = process.cwd(), options = {}) {
143
158
 
144
159
  const selectedPresets = options.presets || existingConfig.presets || ["nightly-security-audit", "flaky-test-quarantine"];
145
160
 
161
+ const limitsBlock = isCustomLimits
162
+ ? `\nlimits:\n concurrency: ${limits.concurrency}\n daily_tasks: ${limits.daily_tasks}\n stagger_ms: ${limits.stagger_ms}\n diff_kb: ${limits.diff_kb}\n`
163
+ : "";
164
+
146
165
  const configYaml = `# Google Jules Orchestrator Kit Config (v${KIT_VERSION})
147
166
  version: 1
148
167
  provider: ${existingConfig.provider || "jules"}
149
168
  tier: ${tierName}
150
169
  base_branch: ${options.baseBranch || existingConfig.base_branch || "main"}
151
170
  branch_prefix: ${existingConfig.branch_prefix || "agent/"}
152
-
153
- limits:
154
- concurrency: ${limits.concurrency}
155
- daily_tasks: ${limits.daily_tasks}
156
- stagger_ms: ${limits.stagger_ms}
157
- diff_kb: ${limits.diff_kb}
158
-
171
+ ${limitsBlock}
159
172
  verify:
160
173
  test: "${verify.test}"
161
174
  build: "${verify.build}"
@@ -166,10 +179,23 @@ presets:
166
179
  ${selectedPresets.map((p) => ` - ${p}`).join("\n")}
167
180
  `;
168
181
 
169
- const julesYaml = `# Google Jules Agent Compatibility Manifest
170
- version: 1
182
+ const forbiddenPaths = options.forbiddenPaths || existingJules.forbidden_paths || [
183
+ ".github/**",
184
+ "**/secrets/**",
185
+ "**/*.pem",
186
+ "**/lock-manager/**",
187
+ "scripts/jules-self-audit.mjs",
188
+ ".agent/jules.yml",
189
+ ];
190
+ const allowPaths = options.allowPaths || existingJules.allow_paths || [];
191
+
192
+ const julesYaml = `# Google Jules Repository Configuration (Version 2)
193
+ version: 2
171
194
  test_cmd: "${verify.test}"
172
195
  build_cmd: "${verify.build}"
196
+ forbidden_paths:
197
+ ${forbiddenPaths.map((p) => ` - "${p}"`).join("\n")}
198
+ allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` - "${p}"`).join("\n") : "[]"}
173
199
  `;
174
200
 
175
201
  return {