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.
- package/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
- package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
- package/.agent/prompts/Debugger.md +25 -0
- package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
- package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
- package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
- package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
- package/.agent/prompts/Resilience.md +20 -0
- package/.agent/prompts/Security.md +21 -0
- package/.agent/prompts/Testing.md +30 -0
- package/.agent/prompts/Types.md +19 -0
- package/.agent/rules/jules-protocol.md +4 -3
- package/AGENTS.md +78 -96
- package/CHANGELOG.md +193 -0
- package/JULES_RULES_TEMPLATE.md +83 -96
- package/LICENSE +1 -1
- package/README.md +77 -439
- package/ROADMAP_V1.md +22 -132
- package/bin/agentctl.mjs +443 -144
- package/bin/init.js +6 -3
- package/index.mjs +9 -6
- package/package.json +1 -1
- package/scripts/asset-integrity-check.mjs +1 -1
- package/scripts/doc-sync-check.mjs +47 -0
- package/scripts/generate-command-reference.mjs +39 -0
- package/scripts/jules-dispatch.mjs +12 -113
- package/scripts/jules-merge-swarm.mjs +8 -196
- package/scripts/jules-patch.mjs +7 -8
- package/scripts/jules-queue-runner.mjs +6 -8
- package/scripts/jules-scan-todos.mjs +10 -38
- package/scripts/jules-self-audit.mjs +8 -139
- package/scripts/jules-status.mjs +32 -38
- package/scripts/jules-webhook-receiver.mjs +1 -1
- package/src/assertions.mjs +5 -50
- package/src/bidi-guard.mjs +36 -0
- package/src/budget.mjs +3 -14
- package/src/config.mjs +2 -6
- package/src/dashboard.mjs +7 -9
- package/src/dispatch.mjs +212 -0
- package/src/engine.mjs +40 -16
- package/src/evidence.mjs +10 -41
- package/src/execution-envelope.mjs +13 -1
- package/src/flaky-ledger.mjs +1 -1
- package/src/fs-atomic.mjs +72 -0
- package/src/git.mjs +298 -27
- package/src/mcp.mjs +296 -7
- package/src/memory.mjs +0 -0
- package/src/merge-swarm.mjs +202 -0
- package/src/ops/cli-intent.mjs +1 -0
- package/src/ops/command-registry.mjs +796 -70
- package/src/ops/doctor-registry.mjs +134 -47
- package/src/ops/handover.mjs +3 -27
- package/src/ops/pr-harvest.mjs +1 -1
- package/src/prompt-guard.mjs +33 -3
- package/src/provider.mjs +51 -7
- package/src/remediation.mjs +2 -2
- package/src/role-resolver.mjs +113 -3
- package/src/router.mjs +19 -11
- package/src/runtime-env.mjs +67 -0
- package/src/scaffold.mjs +3 -1
- package/src/scope-guard.mjs +249 -0
- package/src/secret-scanner.mjs +530 -0
- package/src/security.mjs +81 -2972
- package/src/self-audit.mjs +140 -0
- package/src/session-ops.mjs +29 -0
- package/src/stability.mjs +8 -1
- package/src/stack-detector.mjs +5 -2
- package/src/state.mjs +45 -0
- package/src/swarm.mjs +76 -0
- package/src/task-optimizer.mjs +34 -7
- package/src/telemetry.mjs +23 -0
- package/src/test-tamper-guard.mjs +2173 -0
- package/src/todo-scanner.mjs +129 -0
- package/src/web-templates.mjs +3 -3
- package/src/webhook.mjs +10 -3
- package/src/wizard-init.mjs +12 -20
- package/src/wizard-oracle.mjs +4 -3
- package/src/wizard-task.mjs +37 -9
- package/.agent/prompts/Sentinel.md +0 -18
- 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 (
|
|
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
|
|
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
|
+
}
|
package/src/ops/cli-intent.mjs
CHANGED
|
@@ -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
|
}
|