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/src/mcp.mjs CHANGED
@@ -107,7 +107,7 @@ export const MCP_TOOLS = [
107
107
  properties: {
108
108
  title: { type: "string", description: "Short descriptive title for the task" },
109
109
  prompt: { type: "string", description: "Detailed task instructions and prompt" },
110
- role: { type: "string", description: "Specialist agent role (overseer | bolt | sentinel | janitor)" },
110
+ role: { type: "string", description: "Specialist agent role (auditor | performance | security | hygiene | resilience | types | debugger | testing | e2e | database | docs | a11y)" },
111
111
  tier: { type: "string", description: "Force the Cost Router tier when router.enabled (fast | complex); omit to let the heuristic classifier decide" },
112
112
  },
113
113
  required: ["prompt"],
@@ -310,8 +310,65 @@ export const MCP_TOOLS = [
310
310
  },
311
311
  },
312
312
  },
313
+ {
314
+ name: "jules_approve_plan",
315
+ description: "Approve a proposed execution plan for a Jules session awaiting plan approval.",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ sessionId: { type: "string", description: "Jules session ID to approve plan for" },
320
+ },
321
+ required: ["sessionId"],
322
+ },
323
+ },
324
+ {
325
+ name: "jules_send_message",
326
+ description: "Send feedback, steering instructions, or reply message to an active Jules session.",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ sessionId: { type: "string", description: "Jules session ID" },
331
+ prompt: { type: "string", description: "Instruction or feedback message" },
332
+ },
333
+ required: ["sessionId", "prompt"],
334
+ },
335
+ },
336
+ {
337
+ name: "jules_wait_for_session",
338
+ description: "Poll a Jules session until it reaches a terminal or action-required state (COMPLETED, FAILED, AWAITING_PLAN_APPROVAL).",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ sessionId: { type: "string", description: "Jules session ID" },
343
+ timeoutSeconds: { type: "number", description: "Maximum wait time in seconds (default 300, max 1800)" },
344
+ pollIntervalSeconds: { type: "number", description: "Interval between polls in seconds (default 10, min 2)" },
345
+ targetStates: {
346
+ type: "array",
347
+ items: { type: "string" },
348
+ description: "States that stop polling (defaults to COMPLETED, FAILED, AWAITING_PLAN_APPROVAL)",
349
+ },
350
+ },
351
+ required: ["sessionId"],
352
+ },
353
+ },
313
354
  ];
314
355
 
356
+ // Provider-neutral names share the existing schemas and execution paths.
357
+ // Naming aliases do not add capabilities to the configured provider.
358
+ export const MCP_TOOL_ALIASES = Object.freeze({
359
+ agent_dispatch_task: "dispatch_jules_task",
360
+ agent_audit_gate: "audit_jules_gate",
361
+ agent_get_status: "get_jules_status",
362
+ agent_optimize_prompt: "optimize_jules_prompt",
363
+ ...Object.fromEntries(MCP_TOOLS.filter((tool) => tool.name.startsWith("jules_"))
364
+ .map((tool) => [tool.name.replace(/^jules_/, "agent_"), tool.name])),
365
+ });
366
+
367
+ for (const [alias, original] of Object.entries(MCP_TOOL_ALIASES)) {
368
+ const tool = MCP_TOOLS.find((entry) => entry.name === original);
369
+ MCP_TOOLS.push({ ...tool, name: alias });
370
+ }
371
+
315
372
  export async function handleMcpRequest(request, opts = {}) {
316
373
  const root = opts.root || resolveRoot();
317
374
  const config = opts.config || loadConfig(root);
@@ -327,7 +384,7 @@ export async function handleMcpRequest(request, opts = {}) {
327
384
  id,
328
385
  result: {
329
386
  protocolVersion: "2024-11-05",
330
- capabilities: { tools: {} },
387
+ capabilities: { tools: {}, resources: {} },
331
388
  serverInfo: MCP_SERVER_INFO,
332
389
  },
333
390
  };
@@ -337,6 +394,113 @@ export async function handleMcpRequest(request, opts = {}) {
337
394
  return null;
338
395
  }
339
396
 
397
+ if (method === "resources/list") {
398
+ return {
399
+ jsonrpc: "2.0",
400
+ id,
401
+ result: {
402
+ resources: [
403
+ {
404
+ uri: "jules://status",
405
+ name: "Orchestrator Status",
406
+ description: "Daily task budget, active locks, provider readiness, and verification profile",
407
+ mimeType: "application/json",
408
+ },
409
+ {
410
+ uri: "jules://sources",
411
+ name: "Connected Sources",
412
+ description: "Connected GitHub repository sources available to the Jules account",
413
+ mimeType: "application/json",
414
+ },
415
+ {
416
+ uri: "jules://sessions",
417
+ name: "Recent Sessions",
418
+ description: "Recent Jules coding sessions snapshot",
419
+ mimeType: "application/json",
420
+ },
421
+ ],
422
+ },
423
+ };
424
+ }
425
+
426
+ if (method === "resources/read") {
427
+ const uri = params?.uri;
428
+ const isDryRun = Boolean(params?.dryRun ?? opts?.dryRun ?? opts?.config?.dryRun);
429
+
430
+ if (uri === "jules://status") {
431
+ const stackInfo = detectStack(root);
432
+ const budget = budgetStatus(config, root);
433
+ const locks = lockStatus(root);
434
+ const { probeProvider } = await import("./provider-readiness.mjs");
435
+ const { buildDefaultStages } = await import("./profiles.mjs");
436
+ const providerProbe = probeProvider(config.provider || "jules");
437
+ const profileStages = config.verify.stages || buildDefaultStages(config.verify);
438
+ const status = {
439
+ version: MCP_SERVER_INFO.version,
440
+ root,
441
+ stack: stackInfo.stack,
442
+ budget: {
443
+ used: budget.used,
444
+ limit: budget.limit,
445
+ remaining: budget.remaining,
446
+ source: budget.source,
447
+ enforced: budget.enforced,
448
+ scope: "this-repository",
449
+ },
450
+ activeLocksCount: locks.length,
451
+ locks,
452
+ provider: {
453
+ name: providerProbe.name,
454
+ kind: providerProbe.kind,
455
+ ready: providerProbe.ready,
456
+ reason: providerProbe.reason,
457
+ },
458
+ verification: {
459
+ profile: config.verify.profile,
460
+ stages: profileStages.map((st) => st.id),
461
+ skipped: (config.verify.profileSkipped || []).map((sk) => sk.id),
462
+ },
463
+ };
464
+ return {
465
+ jsonrpc: "2.0",
466
+ id,
467
+ result: {
468
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(status, null, 2) }],
469
+ },
470
+ };
471
+ }
472
+
473
+ if (uri === "jules://sources") {
474
+ const provider = createProvider(config.provider || "jules", config);
475
+ const res = await provider.listSources({ root, dryRun: isDryRun });
476
+ return {
477
+ jsonrpc: "2.0",
478
+ id,
479
+ result: {
480
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(res, null, 2) }],
481
+ },
482
+ };
483
+ }
484
+
485
+ if (uri === "jules://sessions") {
486
+ const provider = createProvider(config.provider || "jules", config);
487
+ const res = await provider.listSessions({ root, pageSize: 20, dryRun: isDryRun });
488
+ return {
489
+ jsonrpc: "2.0",
490
+ id,
491
+ result: {
492
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(res, null, 2) }],
493
+ },
494
+ };
495
+ }
496
+
497
+ return {
498
+ jsonrpc: "2.0",
499
+ id,
500
+ error: { code: -32602, message: `Resource not found: ${uri}` },
501
+ };
502
+ }
503
+
340
504
  if (method === "tools/list") {
341
505
  return {
342
506
  jsonrpc: "2.0",
@@ -346,7 +510,9 @@ export async function handleMcpRequest(request, opts = {}) {
346
510
  }
347
511
 
348
512
  if (method === "tools/call") {
349
- const toolName = params?.name;
513
+ const requestedName = params?.name;
514
+ const toolName = Object.hasOwn(MCP_TOOL_ALIASES, requestedName)
515
+ ? MCP_TOOL_ALIASES[requestedName] : requestedName;
350
516
  const args = params?.arguments || {};
351
517
 
352
518
  try {
@@ -563,9 +729,11 @@ export async function handleMcpRequest(request, opts = {}) {
563
729
  };
564
730
  }
565
731
 
732
+ const isDryRun = Boolean(args.dryRun ?? opts?.dryRun ?? opts?.config?.dryRun);
733
+
566
734
  if (toolName === "jules_list_sessions") {
567
735
  const provider = createProvider(config.provider || "jules", config);
568
- const res = await provider.listSessions({ root, pageSize: args.pageSize, pageToken: args.pageToken });
736
+ const res = await provider.listSessions({ root, pageSize: args.pageSize, pageToken: args.pageToken, dryRun: isDryRun });
569
737
  return {
570
738
  jsonrpc: "2.0",
571
739
  id,
@@ -584,7 +752,7 @@ export async function handleMcpRequest(request, opts = {}) {
584
752
  };
585
753
  }
586
754
  const provider = createProvider(config.provider || "jules", config);
587
- const res = await provider.listActivities(args.sessionId, { root, pageSize: args.pageSize, pageToken: args.pageToken });
755
+ const res = await provider.listActivities(args.sessionId, { root, pageSize: args.pageSize, pageToken: args.pageToken, dryRun: isDryRun });
588
756
  return {
589
757
  jsonrpc: "2.0",
590
758
  id,
@@ -621,7 +789,7 @@ export async function handleMcpRequest(request, opts = {}) {
621
789
  };
622
790
  }
623
791
  const provider = createProvider(config.provider || "jules", config);
624
- const res = await provider.archiveSession(args.sessionId, { root });
792
+ const res = await provider.archiveSession(args.sessionId, { root, dryRun: isDryRun });
625
793
  return {
626
794
  jsonrpc: "2.0",
627
795
  id,
@@ -640,7 +808,7 @@ export async function handleMcpRequest(request, opts = {}) {
640
808
  };
641
809
  }
642
810
  const provider = createProvider(config.provider || "jules", config);
643
- const res = await provider.deleteSession(args.sessionId, { root });
811
+ const res = await provider.deleteSession(args.sessionId, { root, dryRun: isDryRun });
644
812
  return {
645
813
  jsonrpc: "2.0",
646
814
  id,
@@ -726,6 +894,127 @@ export async function handleMcpRequest(request, opts = {}) {
726
894
  };
727
895
  }
728
896
 
897
+ if (toolName === "jules_approve_plan") {
898
+ if (!args.sessionId) {
899
+ return {
900
+ jsonrpc: "2.0",
901
+ id,
902
+ error: { code: -32602, message: "Missing required parameter 'sessionId'" },
903
+ };
904
+ }
905
+ const provider = createProvider(config.provider || "jules", config);
906
+ const res = await provider.approvePlan(args.sessionId, { root, dryRun: isDryRun });
907
+ return {
908
+ jsonrpc: "2.0",
909
+ id,
910
+ result: {
911
+ content: [{ type: "text", text: JSON.stringify({ ok: true, session: res }, null, 2) }],
912
+ },
913
+ };
914
+ }
915
+
916
+ if (toolName === "jules_send_message") {
917
+ if (!args.sessionId) {
918
+ return {
919
+ jsonrpc: "2.0",
920
+ id,
921
+ error: { code: -32602, message: "Missing required parameter 'sessionId'" },
922
+ };
923
+ }
924
+ if (!args.prompt) {
925
+ return {
926
+ jsonrpc: "2.0",
927
+ id,
928
+ error: { code: -32602, message: "Missing required parameter 'prompt'" },
929
+ };
930
+ }
931
+ const provider = createProvider(config.provider || "jules", config);
932
+ const res = await provider.resume(args.sessionId, args.prompt, { root, dryRun: isDryRun });
933
+ return {
934
+ jsonrpc: "2.0",
935
+ id,
936
+ result: {
937
+ content: [{ type: "text", text: JSON.stringify({ ok: true, session: res }, null, 2) }],
938
+ },
939
+ };
940
+ }
941
+
942
+ if (toolName === "jules_wait_for_session") {
943
+ if (!args.sessionId) {
944
+ return {
945
+ jsonrpc: "2.0",
946
+ id,
947
+ error: { code: -32602, message: "Missing required parameter 'sessionId'" },
948
+ };
949
+ }
950
+ const timeoutSec = Math.min(Math.max(args.timeoutSeconds || 300, 2), 1800);
951
+ const intervalSec = Math.min(Math.max(args.pollIntervalSeconds || 10, 1), 60);
952
+ const targetStates = new Set(
953
+ (args.targetStates && Array.isArray(args.targetStates)
954
+ ? args.targetStates
955
+ : ["COMPLETED", "FAILED", "AWAITING_PLAN_APPROVAL"]
956
+ ).map((s) => String(s).toUpperCase())
957
+ );
958
+ const provider = createProvider(config.provider || "jules", config);
959
+ const startedAt = Date.now();
960
+ let lastSession = null;
961
+
962
+ if (isDryRun) {
963
+ return {
964
+ jsonrpc: "2.0",
965
+ id,
966
+ result: {
967
+ content: [{
968
+ type: "text",
969
+ text: JSON.stringify({ ok: true, dryRun: true, sessionId: args.sessionId, finalState: "COMPLETED", elapsedSeconds: 0 }, null, 2),
970
+ }],
971
+ },
972
+ };
973
+ }
974
+
975
+ while (Date.now() - startedAt < timeoutSec * 1000) {
976
+ lastSession = await provider.getSession(args.sessionId, { root });
977
+ const curState = String(lastSession?.state || lastSession?.status || "").toUpperCase();
978
+ if (targetStates.has(curState)) {
979
+ return {
980
+ jsonrpc: "2.0",
981
+ id,
982
+ result: {
983
+ content: [{
984
+ type: "text",
985
+ text: JSON.stringify({
986
+ ok: true,
987
+ sessionId: args.sessionId,
988
+ finalState: curState,
989
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
990
+ session: lastSession,
991
+ }, null, 2),
992
+ }],
993
+ },
994
+ };
995
+ }
996
+ await new Promise((r) => setTimeout(r, intervalSec * 1000));
997
+ }
998
+
999
+ return {
1000
+ jsonrpc: "2.0",
1001
+ id,
1002
+ result: {
1003
+ content: [{
1004
+ type: "text",
1005
+ text: JSON.stringify({
1006
+ ok: false,
1007
+ timeout: true,
1008
+ sessionId: args.sessionId,
1009
+ lastObservedState: lastSession?.state || "UNKNOWN",
1010
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
1011
+ }, null, 2),
1012
+ }],
1013
+ isError: true,
1014
+ },
1015
+ };
1016
+ }
1017
+
729
1018
  return {
730
1019
  jsonrpc: "2.0",
731
1020
  id,
package/src/memory.mjs CHANGED
Binary file
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Swarm branch merger with 3-way structural JSON/object merge, git merge-file
3
+ * execution isolated in os.tmpdir(), and safety gate checking.
4
+ *
5
+ * This is the home of the logic that used to live in
6
+ * `scripts/jules-merge-swarm.mjs`; that file is now a minimal CLI entry point.
7
+ */
8
+
9
+ import { writeFileSync, readFileSync, existsSync, readdirSync, rmSync, mkdtempSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { spawnSync } from "node:child_process";
13
+ import { classifyRiskTier, RISK_TIERS } from "./risk.mjs";
14
+ import { git, resolveBase } from "./git.mjs";
15
+ import { normalizePath, loadConfig } from "./config.mjs";
16
+
17
+ export const EXIT = Object.freeze({
18
+ SUCCESS: 0,
19
+ R3_RESTRICTED: 3,
20
+ MERGE_CONFLICT: 4,
21
+ });
22
+
23
+ const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
24
+
25
+ function isPlainObject(v) {
26
+ return (
27
+ v !== null &&
28
+ typeof v === "object" &&
29
+ !Array.isArray(v) &&
30
+ (Object.getPrototypeOf(v) === null || Object.getPrototypeOf(v) === Object.prototype)
31
+ );
32
+ }
33
+
34
+ function deepClone(v) {
35
+ if (v === undefined) return undefined;
36
+ return JSON.parse(JSON.stringify(v));
37
+ }
38
+
39
+ function stableStringify(v) {
40
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
41
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
42
+ const keys = Object.keys(v).filter((k) => !BLOCKED_KEYS.has(k)).sort();
43
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(",")}}`;
44
+ }
45
+
46
+ function deepEqual(a, b) {
47
+ return stableStringify(a) === stableStringify(b);
48
+ }
49
+
50
+ /**
51
+ * Recursive 3-way structural object & array merge.
52
+ */
53
+ export function deepMerge3Way(base, ours, theirs, path = "$") {
54
+ const conflicts = [];
55
+ if (deepEqual(ours, theirs)) return { merged: deepClone(ours), conflicts };
56
+ if (deepEqual(ours, base)) return { merged: deepClone(theirs), conflicts };
57
+ if (deepEqual(theirs, base)) return { merged: deepClone(ours), conflicts };
58
+
59
+ if (isPlainObject(base) && isPlainObject(ours) && isPlainObject(theirs)) {
60
+ const merged = {};
61
+ const safeBase = base || {};
62
+ const safeOurs = ours || {};
63
+ const safeTheirs = theirs || {};
64
+ const keys = new Set([
65
+ ...Object.keys(safeBase),
66
+ ...Object.keys(safeOurs),
67
+ ...Object.keys(safeTheirs),
68
+ ]);
69
+ for (const k of keys) {
70
+ if (BLOCKED_KEYS.has(k)) continue;
71
+ const childPath = `${path}.${k}`;
72
+ const b = safeBase[k], o = safeOurs[k], t = safeTheirs[k];
73
+ if (o === undefined && t === undefined) continue;
74
+ if (o === undefined) { merged[k] = deepClone(t); continue; }
75
+ if (t === undefined) { merged[k] = deepClone(o); continue; }
76
+ const sub = deepMerge3Way(b, o, t, childPath);
77
+ merged[k] = sub.merged;
78
+ conflicts.push(...sub.conflicts);
79
+ }
80
+ return { merged, conflicts };
81
+ }
82
+
83
+ if (Array.isArray(ours) && Array.isArray(theirs)) {
84
+ const baseArr = Array.isArray(base) ? base : [];
85
+ const minLen = Math.min(ours.length, theirs.length);
86
+ const merged = [];
87
+ for (let i = 0; i < minLen; i++) {
88
+ const sub = deepMerge3Way(baseArr[i], ours[i], theirs[i], `${path}[${i}]`);
89
+ merged.push(sub.merged);
90
+ conflicts.push(...sub.conflicts);
91
+ }
92
+ if (ours.length > minLen && theirs.length <= minLen) {
93
+ merged.push(...ours.slice(minLen).map(deepClone));
94
+ } else if (theirs.length > minLen && ours.length <= minLen) {
95
+ merged.push(...theirs.slice(minLen).map(deepClone));
96
+ } else if (ours.length > minLen && theirs.length > minLen) {
97
+ conflicts.push({
98
+ path: `${path}[tail]`,
99
+ base: baseArr.slice(minLen),
100
+ ours: ours.slice(minLen),
101
+ theirs: theirs.slice(minLen),
102
+ });
103
+ merged.push(...ours.slice(minLen).map(deepClone));
104
+ }
105
+ return { merged, conflicts };
106
+ }
107
+
108
+ conflicts.push({ path, base: deepClone(base), ours: deepClone(ours), theirs: deepClone(theirs) });
109
+ return { merged: deepClone(ours), conflicts };
110
+ }
111
+
112
+ /**
113
+ * Executes a function inside a temporary directory in os.tmpdir() with guaranteed cleanup.
114
+ */
115
+ export function withTempDir(prefix, fn) {
116
+ const dir = mkdtempSync(join(tmpdir(), prefix || "jules-merge-"));
117
+ try {
118
+ return fn(dir);
119
+ } finally {
120
+ try {
121
+ rmSync(dir, { recursive: true, force: true });
122
+ } catch (_) {}
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Attempts a 3-way text file merge using git merge-file inside os.tmpdir().
128
+ */
129
+ export function attemptCodeMergeFile(repoRoot, relPath, oursContent, baseContent, theirsContent) {
130
+ return withTempDir("jules-merge-", (tmp) => {
131
+ const baseFile = join(tmp, "base");
132
+ const oursFile = join(tmp, "ours");
133
+ const theirsFile = join(tmp, "theirs");
134
+ writeFileSync(baseFile, baseContent ?? "", "utf-8");
135
+ writeFileSync(oursFile, oursContent ?? "", "utf-8");
136
+ writeFileSync(theirsFile, theirsContent ?? "", "utf-8");
137
+
138
+ const res = spawnSync("git", ["merge-file", "-p", "--diff3", oursFile, baseFile, theirsFile], {
139
+ encoding: "utf-8",
140
+ maxBuffer: 64 * 1024 * 1024,
141
+ });
142
+
143
+ if (res.error) {
144
+ return { ok: false, conflicts: 0, error: res.error.message };
145
+ }
146
+ const conflictCount = res.status && res.status > 0 ? res.status : 0;
147
+ const merged = res.stdout ?? "";
148
+ return {
149
+ ok: conflictCount === 0,
150
+ merged,
151
+ conflicts: conflictCount,
152
+ };
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Checks safety gate against active worker locks and risk tiers.
158
+ */
159
+ export function checkSafetyGate(branchName = "", projectRoot = process.cwd(), opts = {}) {
160
+ const locksDir = join(projectRoot, ".agent/state/locks");
161
+ if (existsSync(locksDir)) {
162
+ try {
163
+ const files = readdirSync(locksDir);
164
+ for (const f of files) {
165
+ if (!f.endsWith(".json")) continue;
166
+ const content = readFileSync(join(locksDir, f), "utf-8");
167
+ const parsed = JSON.parse(content);
168
+ if (parsed.branch === branchName) {
169
+ return { safe: false, reason: `Active lock held by worker ${parsed.agent || "unknown"}` };
170
+ }
171
+ }
172
+ } catch (_) {}
173
+ }
174
+
175
+ // Check R3 risk tier if files modified on target branch
176
+ if (branchName) {
177
+ try {
178
+ const baseBranch = process.env.BASE_BRANCH || "main";
179
+ const resolvedBase = resolveBase(projectRoot, baseBranch);
180
+ const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedBase}...${branchName}`], { cwd: projectRoot, raw: true, ignoreError: true }) || "";
181
+ const files = raw.split("\0").map(normalizePath).filter(Boolean);
182
+ if (files.length > 0) {
183
+ // The repository's own risk paths must apply here too, or the gate and
184
+ // the harvester classify the same branch differently.
185
+ let config = opts.config;
186
+ if (!config) {
187
+ try {
188
+ config = loadConfig(projectRoot);
189
+ } catch (_) {
190
+ config = {};
191
+ }
192
+ }
193
+ const tier = classifyRiskTier(files, { config });
194
+ if (tier.tier === RISK_TIERS.R3) {
195
+ return { safe: false, reason: `R3 Restricted Path violation: ${tier.reason}` };
196
+ }
197
+ }
198
+ } catch (_) {}
199
+ }
200
+
201
+ return { safe: true };
202
+ }
@@ -25,5 +25,6 @@ export function resolveWizardInteractivity(input = {}) {
25
25
  // to edit rather than as a complete invocation.
26
26
  if (input.interactive === true) return true;
27
27
  if (input.nonInteractive || input.yes || input.fullySpecified) return false;
28
+ if (input.isTTY === false || input.ci) return false;
28
29
  return undefined;
29
30
  }