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/engine.mjs CHANGED
@@ -13,7 +13,7 @@ import { recordVerifyRun, readVerifyRuns, flakyVerdict } from "./flaky-ledger.mj
13
13
  import fs, { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
14
14
  import { join, basename } from "node:path";
15
15
  import { createHash } from "node:crypto";
16
- import { appendTelemetry as appendTelemetryUnsafe } from "./telemetry.mjs";
16
+ import { appendTelemetryBestEffort as appendTelemetry } from "./telemetry.mjs";
17
17
 
18
18
  import { spawn } from "node:child_process";
19
19
  import { resolveAffectedTests, executeQueueDag } from "./dag-engine.mjs";
@@ -59,14 +59,6 @@ export {
59
59
  };
60
60
 
61
61
 
62
- function appendTelemetry(root, kind, fields = {}) {
63
- try {
64
- return appendTelemetryUnsafe(root, kind, fields);
65
- } catch (_) {
66
- return null;
67
- }
68
- }
69
-
70
62
  function isSafeQueueFileName(fileName) {
71
63
  return typeof fileName === "string" && fileName.length > 0 && basename(fileName) === fileName && fileName !== "." && fileName !== "..";
72
64
  }
@@ -1138,8 +1130,8 @@ function buildRepairPrompt(failure, attempt, _config, extraPromptDirective = nul
1138
1130
  let escalationDirective = "1. Ground your fix strictly in the error log below. Do NOT guess file contents or function signatures.";
1139
1131
 
1140
1132
  if (extraPromptDirective) {
1141
- escalationStrategy = "WHACK_A_MOLE_PIVOT";
1142
- escalationDirective = `1. ARCHITECTURAL PIVOT: ${extraPromptDirective}`;
1133
+ escalationStrategy = "Test Oscillation Circuit Breaker Activated";
1134
+ escalationDirective = `1. Switching verification strategy: ${extraPromptDirective}`;
1143
1135
  } else if (attempt === 2) {
1144
1136
  escalationStrategy = "DIAGNOSTIC_ANALYSIS";
1145
1137
  escalationDirective = "1. DIAGNOSE FIRST: Two attempts have failed. Analyze if the root cause is in a different file or component than where the previous patch was applied.";
@@ -1205,10 +1197,15 @@ export async function dispatch(task = {}, opts = {}) {
1205
1197
  let cleanPrompt = redactSecrets(task.prompt || "");
1206
1198
 
1207
1199
  // Specialist Role resolution (if role is set and not already present in prompt)
1208
- if (task.role && !cleanPrompt.includes("Protocol - ")) {
1209
- const roleObj = resolveRolePrompt(root, task.role);
1200
+ if (task.role) {
1201
+ const roleObj = resolveRolePrompt(root, task.role, { config });
1210
1202
  if (roleObj) {
1211
- cleanPrompt = `${roleObj.content}\n\n${cleanPrompt}`.trim();
1203
+ const alreadyHasRole = cleanPrompt.includes(roleObj.content.slice(0, 60)) ||
1204
+ cleanPrompt.includes(`# ${roleObj.role}`) ||
1205
+ cleanPrompt.includes("Protocol - ");
1206
+ if (!alreadyHasRole) {
1207
+ cleanPrompt = `${roleObj.content}\n\n${cleanPrompt}`.trim();
1208
+ }
1212
1209
  } else {
1213
1210
  // A role reaching here comes from a task envelope or an internal
1214
1211
  // synthesis rather than a typed flag, so the dispatch still proceeds with
@@ -1227,7 +1224,13 @@ export async function dispatch(task = {}, opts = {}) {
1227
1224
  }
1228
1225
 
1229
1226
  // Wire Prompt Guard, Memory Hydration & Envelope to wrap raw task arguments
1230
- const taskInstructions = task.taskInstructions || cleanPrompt || task.title || "Autonomous Task Execution";
1227
+ let taskInstructions = cleanPrompt;
1228
+ if (task.taskInstructions && task.taskInstructions !== cleanPrompt) {
1229
+ taskInstructions = cleanPrompt ? `${cleanPrompt}\n\n${task.taskInstructions}` : task.taskInstructions;
1230
+ }
1231
+ if (!taskInstructions) {
1232
+ taskInstructions = task.title || "Autonomous Task Execution";
1233
+ }
1231
1234
  const untrustedData = Array.isArray(task.untrustedData) ? task.untrustedData : [];
1232
1235
  const systemPolicy = task.systemPolicy || config.systemPolicy || "";
1233
1236
 
@@ -1236,6 +1239,11 @@ export async function dispatch(task = {}, opts = {}) {
1236
1239
 
1237
1240
  const envelopedPrompt = buildAgentEnvelope(systemPolicy, taskInstructions, untrustedData, { learnedRemediations });
1238
1241
 
1242
+ // Enforce prompt size limit on the assembled final payload
1243
+ if (Buffer.byteLength(envelopedPrompt, "utf-8") > promptKb) {
1244
+ throw new Error(`Task prompt exceeds maximum payload limit of ${config.limits.promptKb} KB (${Math.ceil(Buffer.byteLength(envelopedPrompt, "utf-8") / 1024)} KB)`);
1245
+ }
1246
+
1239
1247
  const cleanTask = { ...task, prompt: envelopedPrompt };
1240
1248
 
1241
1249
  // Snapshot the tree before anything can change it.
@@ -1606,4 +1614,20 @@ export async function probeDevServer(serverConfig = {}, root = process.cwd()) {
1606
1614
  }
1607
1615
  }
1608
1616
 
1609
-
1617
+ /**
1618
+ * Classify a queue-run failure as "concurrency_limit" (retry later without
1619
+ * consuming a fresh dispatch) or "retriable".
1620
+ *
1621
+ * Moved out of scripts/jules-queue-runner.mjs, which keeps only the process
1622
+ * entry point; the queue loop itself already lives in this module.
1623
+ *
1624
+ * @param {Error|string} err
1625
+ * @returns {"concurrency_limit" | "retriable"}
1626
+ */
1627
+ export function classifyQueueFailure(err) {
1628
+ const msg = String(err?.message || err || "");
1629
+ if (msg.includes("FAILED_PRECONDITION") || msg.includes("Active Session Limit") || msg.includes("concurrency")) {
1630
+ return "concurrency_limit";
1631
+ }
1632
+ return "retriable";
1633
+ }
package/src/evidence.mjs CHANGED
@@ -4,26 +4,19 @@ import {
4
4
  existsSync,
5
5
  readdirSync,
6
6
  statSync,
7
- openSync,
8
- writeSync,
9
- fsyncSync,
10
- closeSync,
11
- renameSync,
12
7
  unlinkSync,
13
8
  } from "node:fs";
14
- import { join, resolve, relative, isAbsolute, sep, extname } from "node:path";
9
+ import { join, resolve, relative, isAbsolute, extname } from "node:path";
15
10
  import { createHash, randomUUID } from "node:crypto";
16
11
  import { execSync } from "node:child_process";
17
12
  import { isTestPath } from "./test-paths.mjs";
13
+ import { safeAtomicWrite } from "./security.mjs";
14
+ import { normalizePath } from "./config.mjs";
18
15
 
19
- /**
20
- * Normalizes file path to POSIX slashes.
21
- * @param {string} p
22
- * @returns {string}
23
- */
24
- export function normalizePath(p = "") {
25
- return p.split(sep).join("/").replace(/\\/g, "/");
26
- }
16
+ // normalizePath is the single shared implementation in src/config.mjs (the
17
+ // copy that used to live here differed only by not guarding non-strings).
18
+ // Re-exported so `./evidence.mjs` callers keep the same entry point.
19
+ export { normalizePath };
27
20
 
28
21
  /**
29
22
  * Computes SHA-256 hex digest of a string or buffer.
@@ -232,30 +225,6 @@ export function computeEvidenceHash(manifest) {
232
225
  return "sha256:" + sha256(canonicalPayload);
233
226
  }
234
227
 
235
- /**
236
- * Safely writes a file atomically.
237
- * @param {string} filePath
238
- * @param {string} content
239
- */
240
- function writeFileAtomically(filePath, content) {
241
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
242
- let fd;
243
- try {
244
- fd = openSync(tmpPath, "wx", 0o600);
245
- writeSync(fd, content, "utf-8");
246
- fsyncSync(fd);
247
- closeSync(fd);
248
- fd = undefined;
249
- renameSync(tmpPath, filePath);
250
- } catch (err) {
251
- if (fd !== undefined) {
252
- try { closeSync(fd); } catch (_) {}
253
- }
254
- try { unlinkSync(tmpPath); } catch (_) {}
255
- throw err;
256
- }
257
- }
258
-
259
228
  /**
260
229
  * Queries current git provenance data safely.
261
230
  * @param {string} root
@@ -407,12 +376,12 @@ export function writeEvidenceManifest(root, manifest, customPath = null) {
407
376
  ? isAbsolute(customPath) ? customPath : join(root, customPath)
408
377
  : join(evidenceDir, `${manifest.manifestId}.json`);
409
378
 
410
- writeFileAtomically(targetPath, JSON.stringify(manifest, null, 2));
379
+ safeAtomicWrite(targetPath, JSON.stringify(manifest, null, 2), { mode: 0o600 });
411
380
 
412
381
  // Also maintain symlink/latest pointer
413
382
  const latestPath = join(evidenceDir, "manifest.v1.json");
414
383
  try {
415
- writeFileAtomically(latestPath, JSON.stringify(manifest, null, 2));
384
+ safeAtomicWrite(latestPath, JSON.stringify(manifest, null, 2), { mode: 0o600 });
416
385
  } catch (_) {}
417
386
 
418
387
  // Written at a custom path means the caller owns the location and its
@@ -612,7 +581,7 @@ export function exportJsonReport(manifest, customPath = null) {
612
581
  };
613
582
 
614
583
  if (customPath) {
615
- writeFileAtomically(customPath, JSON.stringify(report, null, 2));
584
+ safeAtomicWrite(customPath, JSON.stringify(report, null, 2), { mode: 0o600 });
616
585
  }
617
586
 
618
587
  return report;
@@ -33,6 +33,18 @@ export function hashExecutionEnvelope(envelope) {
33
33
  return createHash("sha256").update(JSON.stringify(canonicalPayload)).digest("hex");
34
34
  }
35
35
 
36
+ /**
37
+ * Hashes the envelope's scope triple. The configSha contract: SHA-256 over
38
+ * exactly `{ denyPaths, allowPaths, protectPaths }`, defined once here so the
39
+ * implementation cannot drift from the specification.
40
+ *
41
+ * @param {{ denyPaths?: string[], allowPaths?: string[], protectPaths?: string[] }} scope
42
+ * @returns {string} Hex SHA-256 digest.
43
+ */
44
+ export function hashConfigScope({ denyPaths = [], allowPaths = [], protectPaths = [] } = {}) {
45
+ return createHash("sha256").update(JSON.stringify({ denyPaths, allowPaths, protectPaths })).digest("hex");
46
+ }
47
+
36
48
  /**
37
49
  * Deep freezes an execution envelope to prevent modifications during OODA repair loops.
38
50
  */
@@ -93,7 +105,7 @@ export function createExecutionEnvelope(task = {}, opts = {}) {
93
105
  taskId,
94
106
  baseRef,
95
107
  baseSha,
96
- configSha: createHash("sha256").update(JSON.stringify({ denyPaths, allowPaths, protectPaths })).digest("hex"),
108
+ configSha: hashConfigScope({ denyPaths, allowPaths, protectPaths }),
97
109
  scope: {
98
110
  deny: denyPaths,
99
111
  allow: allowPaths,
@@ -250,7 +250,7 @@ export function synthesizeFlakyHealingTask(quarantinedItem, options = {}) {
250
250
  // in a non-Node repo it produced a healing task that could not run.
251
251
  const testCmd = item.testCmd || options.testCmd || options.config?.verify?.test || "";
252
252
  const oscillationPct = Math.round((item.oscillation || 0.4) * 100);
253
- const role = options.role || "janitor";
253
+ const role = options.role || "hygiene";
254
254
  const slug = String(testCmd).replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").slice(0, 20).toLowerCase();
255
255
  const taskId = options.taskId || `flaky-heal-${slug}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
256
256
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Atomic filesystem writes for the kit's own state.
3
+ *
4
+ * Split out of src/security.mjs (P05): these are the write primitives every
5
+ * durable artefact in the kit rests on — a state file, an evidence bundle, a
6
+ * handover — and they have nothing to do with scanning a diff. They sit at the
7
+ * bottom of the dependency graph on purpose: nothing under src/ needs to import
8
+ * security scanning in order to write a file safely.
9
+ *
10
+ * Relocation only; the logic, the failure modes and the comment reasoning are
11
+ * the originals.
12
+ */
13
+
14
+ import { openSync, writeSync, fsyncSync, closeSync, renameSync, realpathSync, existsSync, lstatSync, unlinkSync } from "node:fs";
15
+ import { dirname, join, basename } from "node:path";
16
+ import { randomBytes } from "node:crypto";
17
+
18
+ export function safeRenameSync(src, dst) {
19
+ try {
20
+ renameSync(src, dst);
21
+ } catch (err) {
22
+ if (process.platform === "win32" && (err.code === "EEXIST" || err.code === "EPERM")) {
23
+ try { unlinkSync(dst); } catch (_) {}
24
+ renameSync(src, dst);
25
+ } else {
26
+ throw err;
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * TOCTOU-safe atomic file write using O_CREAT|O_EXCL|O_WRONLY temp file in target dir.
33
+ */
34
+ export function safeAtomicWrite(filePath, content, options = {}) {
35
+ const mode = options.mode || 0o644;
36
+ const encoding = options.encoding || "utf-8";
37
+ const rejectSymlinks = options.rejectSymlinks !== false;
38
+
39
+ if (existsSync(filePath)) {
40
+ const stat = lstatSync(filePath);
41
+ if (stat.isSymbolicLink()) {
42
+ const realPath = realpathSync(filePath);
43
+ if (rejectSymlinks) {
44
+ throw new Error(`TOCTOU Guard: Refusing to write to symlink ${filePath} -> ${realPath}`);
45
+ }
46
+ }
47
+ }
48
+
49
+ const dir = dirname(filePath);
50
+ const tempFile = join(dir, `.tmp-${basename(filePath)}-${randomBytes(6).toString("hex")}`);
51
+
52
+ let fd;
53
+ try {
54
+ fd = openSync(tempFile, "wx", mode);
55
+ writeSync(fd, content, null, encoding);
56
+ if (options.sync !== false) {
57
+ fsyncSync(fd);
58
+ }
59
+ closeSync(fd);
60
+ fd = undefined;
61
+ safeRenameSync(tempFile, filePath);
62
+ return true;
63
+ } catch (err) {
64
+ if (fd !== undefined) {
65
+ try { closeSync(fd); } catch (_) {}
66
+ }
67
+ try {
68
+ if (existsSync(tempFile)) unlinkSync(tempFile);
69
+ } catch (_) {}
70
+ throw err;
71
+ }
72
+ }