jules-orchestrator-kit 0.72.2 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
  2. package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
  3. package/.agent/prompts/Debugger.md +25 -0
  4. package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
  5. package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
  6. package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
  7. package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
  8. package/.agent/prompts/Resilience.md +20 -0
  9. package/.agent/prompts/Security.md +21 -0
  10. package/.agent/prompts/Testing.md +30 -0
  11. package/.agent/prompts/Types.md +19 -0
  12. package/.agent/rules/jules-protocol.md +4 -3
  13. package/AGENTS.md +78 -96
  14. package/CHANGELOG.md +193 -0
  15. package/JULES_RULES_TEMPLATE.md +83 -96
  16. package/LICENSE +1 -1
  17. package/README.md +77 -439
  18. package/ROADMAP_V1.md +22 -132
  19. package/bin/agentctl.mjs +443 -144
  20. package/bin/init.js +6 -3
  21. package/index.mjs +9 -6
  22. package/package.json +1 -1
  23. package/scripts/asset-integrity-check.mjs +1 -1
  24. package/scripts/doc-sync-check.mjs +47 -0
  25. package/scripts/generate-command-reference.mjs +39 -0
  26. package/scripts/jules-dispatch.mjs +12 -113
  27. package/scripts/jules-merge-swarm.mjs +8 -196
  28. package/scripts/jules-patch.mjs +7 -8
  29. package/scripts/jules-queue-runner.mjs +6 -8
  30. package/scripts/jules-scan-todos.mjs +10 -38
  31. package/scripts/jules-self-audit.mjs +8 -139
  32. package/scripts/jules-status.mjs +32 -38
  33. package/scripts/jules-webhook-receiver.mjs +1 -1
  34. package/src/assertions.mjs +5 -50
  35. package/src/bidi-guard.mjs +36 -0
  36. package/src/budget.mjs +3 -14
  37. package/src/config.mjs +2 -6
  38. package/src/dashboard.mjs +7 -9
  39. package/src/dispatch.mjs +212 -0
  40. package/src/engine.mjs +40 -16
  41. package/src/evidence.mjs +10 -41
  42. package/src/execution-envelope.mjs +13 -1
  43. package/src/flaky-ledger.mjs +1 -1
  44. package/src/fs-atomic.mjs +72 -0
  45. package/src/git.mjs +298 -27
  46. package/src/mcp.mjs +296 -7
  47. package/src/memory.mjs +0 -0
  48. package/src/merge-swarm.mjs +202 -0
  49. package/src/ops/cli-intent.mjs +1 -0
  50. package/src/ops/command-registry.mjs +796 -70
  51. package/src/ops/doctor-registry.mjs +134 -47
  52. package/src/ops/handover.mjs +3 -27
  53. package/src/ops/pr-harvest.mjs +1 -1
  54. package/src/prompt-guard.mjs +33 -3
  55. package/src/provider.mjs +51 -7
  56. package/src/remediation.mjs +2 -2
  57. package/src/role-resolver.mjs +113 -3
  58. package/src/router.mjs +19 -11
  59. package/src/runtime-env.mjs +67 -0
  60. package/src/scaffold.mjs +3 -1
  61. package/src/scope-guard.mjs +249 -0
  62. package/src/secret-scanner.mjs +530 -0
  63. package/src/security.mjs +81 -2972
  64. package/src/self-audit.mjs +140 -0
  65. package/src/session-ops.mjs +29 -0
  66. package/src/stability.mjs +8 -1
  67. package/src/stack-detector.mjs +5 -2
  68. package/src/state.mjs +45 -0
  69. package/src/swarm.mjs +76 -0
  70. package/src/task-optimizer.mjs +34 -7
  71. package/src/telemetry.mjs +23 -0
  72. package/src/test-tamper-guard.mjs +2173 -0
  73. package/src/todo-scanner.mjs +129 -0
  74. package/src/web-templates.mjs +3 -3
  75. package/src/webhook.mjs +10 -3
  76. package/src/wizard-init.mjs +12 -20
  77. package/src/wizard-oracle.mjs +4 -3
  78. package/src/wizard-task.mjs +37 -9
  79. package/.agent/prompts/Sentinel.md +0 -18
  80. package/scripts/utils.mjs +0 -241
package/bin/init.js CHANGED
@@ -260,9 +260,12 @@ const setupMdContent = `# Google Jules Encoded Workspace Manifest
260
260
  > **Generated**: ${agentState.generatedAt}
261
261
  > **Workspace Manifest Code**: \`${payloadToken}\`
262
262
 
263
- ## 🔗 Official Setup & Documentation
264
- 1. Documentation & Guide: https://jules.google
265
- 2. Workspace Manifest Code:
263
+ ## Encoded workspace preset
264
+ This kit-generated JULES1. token is an encoded workspace preset generated for the Jules web UI, not an official Google setup code or credential. It contains a SHA-256 digest and base64url-encoded Brotli-compressed JSON; encoding is not encryption. Review the decoded settings before use.
265
+
266
+ Jules web UI: https://jules.google (the preset format is maintained by this kit).
267
+
268
+ Workspace Manifest Code:
266
269
  \`\`\`
267
270
  ${payloadToken}
268
271
  \`\`\`
package/index.mjs CHANGED
@@ -111,13 +111,16 @@ export { measureEventLoopDelay } from "./src/perf.mjs";
111
111
  export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
112
112
  export { journalIntent, journalDone, reapOrphanedIntents, reapStaleMutexDirs } from "./src/journal.mjs";
113
113
 
114
- // Legacy SDK shims for backward compatibility
114
+ // Command resolution entrypoint (package.json/CI required) and its boundary helpers.
115
115
  export { resolveProjectCommands, resolveWorkspaceExecutionBoundary } from "./scripts/command-resolver.mjs";
116
- export { runSelfAudit, runPreflightSandbox } from "./scripts/jules-self-audit.mjs";
117
- export { scanCodebaseForTodos, runScanner } from "./scripts/jules-scan-todos.mjs";
118
- export { getDynamicGuardrails, dispatchTask } from "./scripts/jules-dispatch.mjs";
119
- export { classifyQueueFailure } from "./scripts/jules-queue-runner.mjs";
120
- export { extractPrUrls, auditSessions, buildSyncManifest, pushReservationManifest } from "./scripts/utils.mjs";
116
+ // Self-audit, TODO scanner, dispatch helpers, and swarm sync manifest:
117
+ // implementation lives in src/ (P04 shim cleanup); the scripts/ files are
118
+ // minimal CLI entrypoints that re-export the same surface for compatibility.
119
+ export { runSelfAudit, runPreflightSandbox } from "./src/self-audit.mjs";
120
+ export { scanCodebaseForTodos, runScanner } from "./src/todo-scanner.mjs";
121
+ export { getDynamicGuardrails, dispatchTask } from "./src/dispatch.mjs";
122
+ export { classifyQueueFailure } from "./src/engine.mjs";
123
+ export { extractPrUrls, auditSessions, buildSyncManifest, pushReservationManifest } from "./src/swarm.mjs";
121
124
 
122
125
  // Native TUI Primitives
123
126
  export { isTTY, styleText, select, multiSelect, input, confirm, secretInput, spinner, ANSI } from "./src/tui.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.72.2",
3
+ "version": "0.73.0",
4
4
  "description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents — Google Jules, Claude Code, Codex and Gemini CLI.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@ const root = process.cwd();
8
8
  const dirsToScan = ["public", "src/assets", "assets", "static"].map((d) => join(root, d)).filter(existsSync);
9
9
 
10
10
  if (dirsToScan.length === 0) {
11
- console.log("[asset-integrity] No asset directories (public/, src/assets/) found. Passing.");
11
+ console.log("[asset-integrity] status: N/A — No asset directories (public/, src/assets/, assets/, static/) found. Nothing to check.");
12
12
  process.exit(0);
13
13
  }
14
14
 
@@ -20,6 +20,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { execSync } from "node:child_process";
22
22
  import { resolveRoot } from "../src/config.mjs";
23
+ import { formatRegistryMarkdown } from "../src/ops/command-registry.mjs";
23
24
 
24
25
  /**
25
26
  * Runs the unit suite and extracts the authoritative counts.
@@ -317,6 +318,52 @@ export function checkDocSync(root = process.cwd(), opts = {}) {
317
318
  }
318
319
  }
319
320
 
321
+ // 6. SECURITY.md — supported version table must name the current version series.
322
+ const security = readIfExists(join(root, "SECURITY.md"));
323
+ if (security === null) {
324
+ add("SECURITY present", false, "SECURITY.md not found");
325
+ } else {
326
+ const sem = version.split(".");
327
+ const expectedSeries = `v${sem[0]}.${sem[1]}.x`;
328
+ const secMatch = security.match(/\|\s*`v(\d+\.\d+)\.x`\s*\(Latest\)\s*\|/);
329
+ add(
330
+ "SECURITY supported version",
331
+ secMatch ? `v${secMatch[1]}.x` === expectedSeries : false,
332
+ secMatch ? `found v${secMatch[1]}.x, expected ${expectedSeries}` : "no `vX.Y.x (Latest)` marker found"
333
+ );
334
+ }
335
+
336
+ // 7. docs/COMMAND_REFERENCE.md — regenerated from the command registry on
337
+ // every run and diffed against the file on disk. The registry feeds
338
+ // `agentctl --help` and `help <command>`, so a stale reference means the
339
+ // docs disagree with the CLI itself. Regenerate with:
340
+ // node scripts/generate-command-reference.mjs
341
+ const referencePath = join(root, "docs", "COMMAND_REFERENCE.md");
342
+ const referenceOnDisk = readIfExists(referencePath);
343
+ if (referenceOnDisk === null) {
344
+ add("COMMAND_REFERENCE present", false, "docs/COMMAND_REFERENCE.md not found");
345
+ } else {
346
+ let rendered = null;
347
+ try {
348
+ rendered = `${formatRegistryMarkdown()}`.replace(/\s+$/, "") + "\n";
349
+ } catch {
350
+ rendered = null;
351
+ }
352
+ if (rendered === null) {
353
+ add("COMMAND_REFERENCE in sync", false, "could not render registry (import failed)");
354
+ } else {
355
+ const diskNorm = referenceOnDisk.replace(/\r\n/g, "\n");
356
+ const renderedNorm = rendered.replace(/\r\n/g, "\n");
357
+ add(
358
+ "COMMAND_REFERENCE in sync",
359
+ diskNorm === renderedNorm,
360
+ diskNorm === renderedNorm
361
+ ? "matches src/ops/command-registry.mjs"
362
+ : "stale — run: node scripts/generate-command-reference.mjs"
363
+ );
364
+ }
365
+ }
366
+
320
367
  return { ok: checks.every((c) => c.ok), version, checks };
321
368
  }
322
369
 
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Regenerate docs/COMMAND_REFERENCE.md from the command registry.
5
+ *
6
+ * The registry (src/ops/command-registry.mjs) is the single source of truth
7
+ * for command flags, usage and examples; this script renders it to Markdown
8
+ * so the checked-in reference cannot drift from `agentctl --help` output.
9
+ * The doc-sync gate (scripts/doc-sync-check.mjs) diffs the rendered output
10
+ * against the file on disk and fails on any difference.
11
+ *
12
+ * Usage:
13
+ * node scripts/generate-command-reference.mjs # write the file
14
+ * node scripts/generate-command-reference.mjs --check # exit 1 if stale
15
+ */
16
+
17
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
18
+ import { join, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { formatRegistryMarkdown } from "../src/ops/command-registry.mjs";
21
+
22
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
23
+ const TARGET = join(ROOT, "docs", "COMMAND_REFERENCE.md");
24
+
25
+ const rendered = formatRegistryMarkdown().replace(/\s+$/, "") + "\n";
26
+
27
+ if (process.argv.includes("--check")) {
28
+ const onDisk = existsSync(TARGET) ? readFileSync(TARGET, "utf-8").replace(/\r\n/g, "\n") : null;
29
+ const expected = rendered.replace(/\r\n/g, "\n");
30
+ if (onDisk === expected) {
31
+ console.log("✅ docs/COMMAND_REFERENCE.md is in sync with the command registry.");
32
+ process.exit(0);
33
+ }
34
+ console.error("❌ docs/COMMAND_REFERENCE.md is stale — run: node scripts/generate-command-reference.mjs");
35
+ process.exit(1);
36
+ }
37
+
38
+ writeFileSync(TARGET, rendered);
39
+ console.log(`✅ Wrote ${TARGET}`);
@@ -1,128 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Task dispatch entrypoint: delegates execution to engine dispatch pipeline.
4
+ * Task dispatch entrypoint: deprecated CLI shim that delegates to the
5
+ * dispatch pipeline. All logic (guardrails, attachments, argv parsing,
6
+ * dispatchTask) lives in src/dispatch.mjs (P04 shim cleanup).
7
+ *
8
+ * The re-export below preserves the historical
9
+ * `scripts/jules-dispatch.mjs` import surface for external consumers,
10
+ * and — deliberately — keeps argv parsing OUT of module scope: importing
11
+ * this file must never consume the importer's process.argv (D10).
5
12
  */
6
13
 
7
- import { dispatch } from "../src/engine.mjs";
8
- import { loadConfig } from "../src/config.mjs";
9
- import { existsSync, statSync } from "node:fs";
10
- import { join } from "node:path";
14
+ export * from "../src/dispatch.mjs";
15
+
16
+ import { dispatchTask, parseDispatchArgs } from "../src/dispatch.mjs";
11
17
 
12
18
  if (process.argv[1] && (process.argv[1].endsWith("jules-dispatch.mjs") || process.argv[1].endsWith("jules-dispatch"))) {
13
19
  process.emitWarning(
14
20
  "scripts/jules-dispatch.mjs is deprecated and will be removed in v1.0.0. Use 'agentctl dispatch' or the programmatic SDK.",
15
21
  "DeprecationWarning"
16
22
  );
17
- }
18
-
19
- const args = process.argv.slice(2);
20
- let title = "Task Dispatch";
21
- let prompt = "";
22
- let dryRun = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1" || args.includes("--dry-run");
23
- let repoless = args.includes("--repoless");
24
-
25
- for (let i = 0; i < args.length; i++) {
26
- if (args[i] === "--title" && args[i + 1]) title = args[i + 1];
27
- else if (args[i] === "--prompt" && args[i + 1]) prompt = args[i + 1];
28
- }
29
-
30
- if (!prompt && args[0] && !args[0].startsWith("-")) {
31
- title = args[0];
32
- prompt = args[1] || args[0];
33
- }
34
-
35
- if (!prompt) {
36
- prompt = "Execute task";
37
- }
38
-
39
- export function getDynamicGuardrails(promptText = "") {
40
- const p = (promptText || "").toLowerCase();
41
- const rules = [];
42
- if (p.includes("auth") || p.includes("sec") || p.includes("key") || p.includes("token")) {
43
- rules.push("Sentinel: SECRET REDACTION GUARDRAILS");
44
- }
45
- if (p.includes("perf") || p.includes("optimiz") || p.includes("fast")) {
46
- rules.push("Performance Guidance (Bolt)");
47
- }
48
- if (p.includes("clean") || p.includes("refactor") || p.includes("lint")) {
49
- rules.push("Clean Code Guidance (Janitor)");
50
- }
51
- if (p.includes("db") || p.includes("sql") || p.includes("database") || p.includes("schema")) {
52
- rules.push("Alchemist: DATABASE GUARDRAILS");
53
- }
54
- if (p.includes("css") || p.includes("tailwind") || p.includes("theme")) {
55
- rules.push("CSS & DESIGN GUARDRAILS");
56
- }
57
- return rules.join("\n");
58
- }
59
-
60
- export function extractImageAttachments(promptText = "", projectRoot = process.cwd()) {
61
- if (!promptText || typeof promptText !== "string") return [];
62
- const regex = /!\[([^\]]*)\]\(([^)]+)\)/g;
63
- const matches = [];
64
- let match;
65
- while ((match = regex.exec(promptText)) !== null) {
66
- const relPath = match[2].trim();
67
- if (relPath.includes("..")) continue;
68
- const absPath = join(projectRoot, relPath);
69
- if (existsSync(absPath)) {
70
- let mime = "image/png";
71
- if (relPath.endsWith(".jpg") || relPath.endsWith(".jpeg")) mime = "image/jpeg";
72
- if (relPath.endsWith(".gif")) mime = "image/gif";
73
- if (relPath.endsWith(".webp")) mime = "image/webp";
74
- const size = statSync(absPath).size;
75
- matches.push({ relPath, absPath, mime, size });
76
- }
77
- }
78
- return matches;
79
- }
80
-
81
- export function getMultimodalAttachmentDirective(attachments = []) {
82
- if (!attachments || attachments.length === 0) return "";
83
- return `### Multimodal Task Attachments (${attachments.length})\n` + attachments.map((a) => `- ${a.relPath}`).join("\n");
84
- }
85
-
86
- export function runPreflightStaticCheck(_projectRoot = process.cwd()) {
87
- return "PASSED";
88
- }
89
-
90
- export function getAlphaRange(slotIndex = 0, totalSlots = 1) {
91
- const total = Number(totalSlots);
92
- const idx = Number(slotIndex);
93
- if (total <= 1) return "A-Z";
94
- if (total === 2) {
95
- return idx === 0 ? "A-M" : "N-Z";
96
- }
97
- return `Slot ${idx + 1}`;
98
- }
99
-
100
- export function getSlotPartitionDirective(slotIndex = 0, totalSlots = 1) {
101
- const total = Number(totalSlots);
102
- const idx = Number(slotIndex);
103
- if (total <= 1) return "";
104
- return `[PARALLEL SWARM SLOT ${idx} of ${total}] Range: ${getAlphaRange(idx, total)} (Partition Focus)`;
105
- }
106
-
107
- export async function dispatchTask(opts = {}) {
108
- const root = process.env.JULES_PROJECT_ROOT || process.cwd();
109
- const config = loadConfig(root);
110
- const isDry = opts.dryRun || dryRun;
111
- const taskTitle = opts.title || title;
112
- const taskPrompt = opts.prompt || prompt;
113
-
114
- if (isDry) {
115
- console.log(`[DRY RUN] Dispatching task: ${taskTitle}`);
116
- console.log(`[DRY RUN] Dispatch payload prepared successfully`);
117
- if (repoless || opts.repoless) {
118
- console.log(`[DRY RUN] Target: (repoless / serverless)`);
119
- }
120
- }
121
- return dispatch({ title: taskTitle, prompt: taskPrompt }, { config, dryRun: isDry });
122
- }
123
23
 
124
- if (process.argv[1] && process.argv[1].endsWith("jules-dispatch.mjs")) {
125
- dispatchTask()
24
+ dispatchTask(parseDispatchArgs(process.argv.slice(2)))
126
25
  .then((session) => {
127
26
  console.log(`[Shim] Task dispatched session: ${session.id}`);
128
27
  process.exit(0);
@@ -1,204 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Swarm branch merger with 3-way structural JSON/object merge, git merge-file execution
5
- * isolated in os.tmpdir(), and safety gate checking.
4
+ * Swarm merge CLI entrypoint.
5
+ *
6
+ * The merge engine (3-way structural merge, git merge-file isolation,
7
+ * safety gate) lives in src/merge-swarm.mjs (P04 shim cleanup). The
8
+ * re-export below preserves the historical `scripts/jules-merge-swarm.mjs`
9
+ * import surface for external consumers.
6
10
  */
7
11
 
8
- import { writeFileSync, readFileSync, existsSync, readdirSync, rmSync, mkdtempSync } from "node:fs";
9
- import { join } from "node:path";
10
- import { tmpdir } from "node:os";
11
- import { spawnSync } from "node:child_process";
12
- import { classifyRiskTier, RISK_TIERS } from "../src/risk.mjs";
13
- import { git, resolveBase } from "../src/git.mjs";
14
- import { normalizePath, loadConfig } from "../src/config.mjs";
12
+ export * from "../src/merge-swarm.mjs";
15
13
 
16
- export const EXIT = Object.freeze({
17
- SUCCESS: 0,
18
- R3_RESTRICTED: 3,
19
- MERGE_CONFLICT: 4,
20
- });
21
-
22
- const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
23
-
24
- function isPlainObject(v) {
25
- return (
26
- v !== null &&
27
- typeof v === "object" &&
28
- !Array.isArray(v) &&
29
- (Object.getPrototypeOf(v) === null || Object.getPrototypeOf(v) === Object.prototype)
30
- );
31
- }
32
-
33
- function deepClone(v) {
34
- if (v === undefined) return undefined;
35
- return JSON.parse(JSON.stringify(v));
36
- }
37
-
38
- function stableStringify(v) {
39
- if (v === null || typeof v !== "object") return JSON.stringify(v);
40
- if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
41
- const keys = Object.keys(v).filter((k) => !BLOCKED_KEYS.has(k)).sort();
42
- return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(",")}}`;
43
- }
44
-
45
- function deepEqual(a, b) {
46
- return stableStringify(a) === stableStringify(b);
47
- }
48
-
49
- /**
50
- * Recursive 3-way structural object & array merge.
51
- */
52
- export function deepMerge3Way(base, ours, theirs, path = "$") {
53
- const conflicts = [];
54
- if (deepEqual(ours, theirs)) return { merged: deepClone(ours), conflicts };
55
- if (deepEqual(ours, base)) return { merged: deepClone(theirs), conflicts };
56
- if (deepEqual(theirs, base)) return { merged: deepClone(ours), conflicts };
57
-
58
- if (isPlainObject(base) && isPlainObject(ours) && isPlainObject(theirs)) {
59
- const merged = {};
60
- const safeBase = base || {};
61
- const safeOurs = ours || {};
62
- const safeTheirs = theirs || {};
63
- const keys = new Set([
64
- ...Object.keys(safeBase),
65
- ...Object.keys(safeOurs),
66
- ...Object.keys(safeTheirs),
67
- ]);
68
- for (const k of keys) {
69
- if (BLOCKED_KEYS.has(k)) continue;
70
- const childPath = `${path}.${k}`;
71
- const b = safeBase[k], o = safeOurs[k], t = safeTheirs[k];
72
- if (o === undefined && t === undefined) continue;
73
- if (o === undefined) { merged[k] = deepClone(t); continue; }
74
- if (t === undefined) { merged[k] = deepClone(o); continue; }
75
- const sub = deepMerge3Way(b, o, t, childPath);
76
- merged[k] = sub.merged;
77
- conflicts.push(...sub.conflicts);
78
- }
79
- return { merged, conflicts };
80
- }
81
-
82
- if (Array.isArray(ours) && Array.isArray(theirs)) {
83
- const baseArr = Array.isArray(base) ? base : [];
84
- const minLen = Math.min(ours.length, theirs.length);
85
- const merged = [];
86
- for (let i = 0; i < minLen; i++) {
87
- const sub = deepMerge3Way(baseArr[i], ours[i], theirs[i], `${path}[${i}]`);
88
- merged.push(sub.merged);
89
- conflicts.push(...sub.conflicts);
90
- }
91
- if (ours.length > minLen && theirs.length <= minLen) {
92
- merged.push(...ours.slice(minLen).map(deepClone));
93
- } else if (theirs.length > minLen && ours.length <= minLen) {
94
- merged.push(...theirs.slice(minLen).map(deepClone));
95
- } else if (ours.length > minLen && theirs.length > minLen) {
96
- conflicts.push({
97
- path: `${path}[tail]`,
98
- base: baseArr.slice(minLen),
99
- ours: ours.slice(minLen),
100
- theirs: theirs.slice(minLen),
101
- });
102
- merged.push(...ours.slice(minLen).map(deepClone));
103
- }
104
- return { merged, conflicts };
105
- }
106
-
107
- conflicts.push({ path, base: deepClone(base), ours: deepClone(ours), theirs: deepClone(theirs) });
108
- return { merged: deepClone(ours), conflicts };
109
- }
110
-
111
- /**
112
- * Executes a function inside a temporary directory in os.tmpdir() with guaranteed cleanup.
113
- */
114
- export function withTempDir(prefix, fn) {
115
- const dir = mkdtempSync(join(tmpdir(), prefix || "jules-merge-"));
116
- try {
117
- return fn(dir);
118
- } finally {
119
- try {
120
- rmSync(dir, { recursive: true, force: true });
121
- } catch (_) {}
122
- }
123
- }
124
-
125
- /**
126
- * Attempts a 3-way text file merge using git merge-file inside os.tmpdir().
127
- */
128
- export function attemptCodeMergeFile(repoRoot, relPath, oursContent, baseContent, theirsContent) {
129
- return withTempDir("jules-merge-", (tmp) => {
130
- const baseFile = join(tmp, "base");
131
- const oursFile = join(tmp, "ours");
132
- const theirsFile = join(tmp, "theirs");
133
- writeFileSync(baseFile, baseContent ?? "", "utf-8");
134
- writeFileSync(oursFile, oursContent ?? "", "utf-8");
135
- writeFileSync(theirsFile, theirsContent ?? "", "utf-8");
136
-
137
- const res = spawnSync("git", ["merge-file", "-p", "--diff3", oursFile, baseFile, theirsFile], {
138
- encoding: "utf-8",
139
- maxBuffer: 64 * 1024 * 1024,
140
- });
141
-
142
- if (res.error) {
143
- return { ok: false, conflicts: 0, error: res.error.message };
144
- }
145
- const conflictCount = res.status && res.status > 0 ? res.status : 0;
146
- const merged = res.stdout ?? "";
147
- return {
148
- ok: conflictCount === 0,
149
- merged,
150
- conflicts: conflictCount,
151
- };
152
- });
153
- }
154
-
155
- /**
156
- * Checks safety gate against active worker locks and risk tiers.
157
- */
158
- export function checkSafetyGate(branchName = "", projectRoot = process.cwd(), opts = {}) {
159
- const locksDir = join(projectRoot, ".agent/state/locks");
160
- if (existsSync(locksDir)) {
161
- try {
162
- const files = readdirSync(locksDir);
163
- for (const f of files) {
164
- if (!f.endsWith(".json")) continue;
165
- const content = readFileSync(join(locksDir, f), "utf-8");
166
- const parsed = JSON.parse(content);
167
- if (parsed.branch === branchName) {
168
- return { safe: false, reason: `Active lock held by worker ${parsed.agent || "unknown"}` };
169
- }
170
- }
171
- } catch (_) {}
172
- }
173
-
174
- // Check R3 risk tier if files modified on target branch
175
- if (branchName) {
176
- try {
177
- const baseBranch = process.env.BASE_BRANCH || "main";
178
- const resolvedBase = resolveBase(projectRoot, baseBranch);
179
- const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedBase}...${branchName}`], { cwd: projectRoot, raw: true, ignoreError: true }) || "";
180
- const files = raw.split("\0").map(normalizePath).filter(Boolean);
181
- if (files.length > 0) {
182
- // The repository's own risk paths must apply here too, or the gate and
183
- // the harvester classify the same branch differently.
184
- let config = opts.config;
185
- if (!config) {
186
- try {
187
- config = loadConfig(projectRoot);
188
- } catch (_) {
189
- config = {};
190
- }
191
- }
192
- const tier = classifyRiskTier(files, { config });
193
- if (tier.tier === RISK_TIERS.R3) {
194
- return { safe: false, reason: `R3 Restricted Path violation: ${tier.reason}` };
195
- }
196
- }
197
- } catch (_) {}
198
- }
199
-
200
- return { safe: true };
201
- }
14
+ import { EXIT, checkSafetyGate } from "../src/merge-swarm.mjs";
202
15
 
203
16
  if (process.argv[1] && process.argv[1].endsWith("jules-merge-swarm.mjs")) {
204
17
  const branchName = process.argv[2] || "";
@@ -211,4 +24,3 @@ if (process.argv[1] && process.argv[1].endsWith("jules-merge-swarm.mjs")) {
211
24
  console.log("No open Jules PRs found.");
212
25
  process.exit(EXIT.SUCCESS);
213
26
  }
214
-
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Session patch fetching utility.
4
+ * Session patch fetching CLI entrypoint.
5
+ *
6
+ * fetchSessionPatch (argument validation around src/session-ops.mjs's
7
+ * extractSessionPatch) moved to session-ops itself (P04 shim cleanup); it is
8
+ * re-exported here for historical importers.
5
9
  */
6
10
 
7
- import { extractSessionPatch } from "../src/session-ops.mjs";
11
+ export { fetchSessionPatch } from "../src/session-ops.mjs";
8
12
 
9
- export async function fetchSessionPatch(sessionId, options = {}) {
10
- if (!sessionId || typeof sessionId !== "string") {
11
- throw new Error("Session ID is required.");
12
- }
13
- return await extractSessionPatch(sessionId, options);
14
- }
13
+ import { fetchSessionPatch } from "../src/session-ops.mjs";
15
14
 
16
15
  if (process.argv[1] && process.argv[1].endsWith("jules-patch.mjs")) {
17
16
  const sessionId = process.argv[2];
@@ -2,17 +2,15 @@
2
2
 
3
3
  /**
4
4
  * Queue processing runner: processes envelopes in .agent/jules-queue/.
5
+ *
6
+ * Spawned by the webhook receiver as `node scripts/jules-queue-runner.mjs`.
7
+ * Failure classification lives in src/engine.mjs (P04 shim cleanup); the
8
+ * re-export keeps the historical import surface for external consumers.
5
9
  */
6
10
 
7
- import { run } from "../src/engine.mjs";
11
+ export { classifyQueueFailure } from "../src/engine.mjs";
8
12
 
9
- export function classifyQueueFailure(err) {
10
- const msg = String(err?.message || err || "");
11
- if (msg.includes("FAILED_PRECONDITION") || msg.includes("Active Session Limit") || msg.includes("concurrency")) {
12
- return "concurrency_limit";
13
- }
14
- return "retriable";
15
- }
13
+ import { run } from "../src/engine.mjs";
16
14
 
17
15
  if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
18
16
  const root = process.env.JULES_PROJECT_ROOT || process.cwd();
@@ -1,48 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Codebase scanner for TODO and FIXME comments.
4
+ * TODO/FIXME scanner CLI entrypoint.
5
+ *
6
+ * The scanner itself (extension allowlist, size/binary limits, .gitignore
7
+ * awareness) lives in src/todo-scanner.mjs (P04 shim cleanup). The re-export
8
+ * below preserves the historical `scripts/jules-scan-todos.mjs` import
9
+ * surface for external consumers.
5
10
  */
6
11
 
7
- import { readdirSync, readFileSync, statSync } from "node:fs";
8
- import { join } from "node:path";
12
+ export * from "../src/todo-scanner.mjs";
9
13
 
10
- export function scanCodebaseForTodos(dir = process.cwd()) {
11
- const todos = [];
12
- function walk(current) {
13
- let entries = [];
14
- try { entries = readdirSync(current); } catch (_) { return; }
15
- for (const entry of entries) {
16
- if (entry === "node_modules" || entry === ".git") continue;
17
- const full = join(current, entry);
18
- try {
19
- const stat = statSync(full);
20
- if (stat.isDirectory()) {
21
- walk(full);
22
- } else if (stat.isFile()) {
23
- const content = readFileSync(full, "utf-8");
24
- const lines = content.split("\n");
25
- lines.forEach((line, idx) => {
26
- if (line.includes("TODO:")) {
27
- todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", type: "TODO", priority: "MEDIUM" });
28
- } else if (line.includes("FIXME:")) {
29
- todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", type: "FIXME", priority: "HIGH" });
30
- }
31
- });
32
- }
33
- } catch (_) {}
34
- }
35
- }
36
- walk(dir);
37
- return todos;
38
- }
39
-
40
- export function runScanner(dir = process.cwd()) {
41
- const todos = scanCodebaseForTodos(dir);
42
- return { todos, count: todos.length };
43
- }
14
+ import { runScanner } from "../src/todo-scanner.mjs";
44
15
 
45
16
  if (process.argv[1] && process.argv[1].endsWith("jules-scan-todos.mjs")) {
46
- console.log("[Shim] TODO Scanner complete.");
17
+ const { count } = runScanner(process.env.JULES_PROJECT_ROOT || process.cwd());
18
+ console.log(`[Shim] TODO Scanner complete. Found ${count} annotation(s).`);
47
19
  process.exit(0);
48
20
  }