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/git.mjs CHANGED
@@ -392,8 +392,40 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
392
392
  );
393
393
  }
394
394
 
395
+ export function getHeadCommit(root = process.cwd()) {
396
+ try {
397
+ const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], {
398
+ cwd: root,
399
+ encoding: "utf-8",
400
+ shell: false,
401
+ stdio: ["ignore", "pipe", "ignore"],
402
+ });
403
+ return res && res.trim() ? res.trim() : null;
404
+ } catch (_) {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ export function getHeadParent(root = process.cwd()) {
410
+ try {
411
+ const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", "HEAD~1^{commit}"], {
412
+ cwd: root,
413
+ encoding: "utf-8",
414
+ shell: false,
415
+ stdio: ["ignore", "pipe", "ignore"],
416
+ });
417
+ return res && res.trim() ? res.trim() : null;
418
+ } catch (_) {
419
+ return null;
420
+ }
421
+ }
422
+
395
423
  export function changedFiles(root = process.cwd(), base = "main", mode = "committed") {
396
424
  const resolvedRef = resolveBase(root, base);
425
+ const headCommit = getHeadCommit(root);
426
+ const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
427
+ const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
428
+
397
429
  if (mode === "working-tree" || mode === "working") {
398
430
  const committedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
399
431
  const uncommittedRaw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
@@ -409,6 +441,16 @@ export function changedFiles(root = process.cwd(), base = "main", mode = "commit
409
441
  const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", "--cached", resolvedRef], { cwd: root, raw: true });
410
442
  return raw.split("\0").map(normalizePath).filter(Boolean);
411
443
  } else {
444
+ if (isBaseAtHead) {
445
+ const headParent = getHeadParent(root);
446
+ if (headParent) {
447
+ const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${headParent}...HEAD`], { cwd: root, raw: true });
448
+ return raw.split("\0").map(normalizePath).filter(Boolean);
449
+ } else {
450
+ const raw = git(["-c", "core.quotePath=false", "diff-tree", "--root", "--no-commit-id", "-r", "-z", "--name-only", headCommit], { cwd: root, raw: true });
451
+ return raw.split("\0").map(normalizePath).filter(Boolean);
452
+ }
453
+ }
412
454
  const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
413
455
  return raw.split("\0").map(normalizePath).filter(Boolean);
414
456
  }
@@ -416,6 +458,10 @@ export function changedFiles(root = process.cwd(), base = "main", mode = "commit
416
458
 
417
459
  export function diffText(root = process.cwd(), base = "main", mode = "committed") {
418
460
  const resolvedRef = resolveBase(root, base);
461
+ const headCommit = getHeadCommit(root);
462
+ const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
463
+ const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
464
+
419
465
  if (mode === "working-tree" || mode === "working") {
420
466
  const committed = git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
421
467
  const uncommitted = git(["diff", "HEAD"], { cwd: root, raw: true, ignoreError: true }) || "";
@@ -465,6 +511,15 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
465
511
  } else if (mode === "staged" || mode === "index") {
466
512
  return git(["diff", "--cached", resolvedRef], { cwd: root, raw: true });
467
513
  }
514
+
515
+ if (isBaseAtHead) {
516
+ const headParent = getHeadParent(root);
517
+ if (headParent) {
518
+ return git(["diff", `${headParent}...HEAD`], { cwd: root, raw: true });
519
+ } else {
520
+ return git(["diff-tree", "-p", "--no-commit-id", "--root", headCommit], { cwd: root, raw: true });
521
+ }
522
+ }
468
523
  return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
469
524
  }
470
525
 
@@ -486,38 +541,72 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
486
541
  * @param {string} mode
487
542
  * @returns {Array<{ file: string, bytes: number }>}
488
543
  */
544
+ function parseRawDiffOutput(raw, out) {
545
+ // `--raw -z` emits ":<srcmode> <dstmode> <srcsha> <dstsha> <status>\0<path>\0".
546
+ const fields = raw.split("\0").filter(Boolean);
547
+ for (let i = 0; i < fields.length; i++) {
548
+ const meta = fields[i];
549
+ if (!meta.startsWith(":")) continue;
550
+ const parts = meta.slice(1).split(/\s+/);
551
+ const file = fields[i + 1];
552
+ i += 1;
553
+ if (!file) continue;
554
+ out.push({
555
+ file,
556
+ srcMode: parts[0] || "",
557
+ dstMode: parts[1] || "",
558
+ srcSha: parts[2] || "",
559
+ dstSha: parts[3] || "",
560
+ status: (parts[4] || "").charAt(0),
561
+ });
562
+ }
563
+ }
564
+
489
565
  export function parseRawDiff(root = process.cwd(), base = "main", mode = "committed") {
490
566
  const resolvedRef = resolveBase(root, base);
491
- const ranges =
492
- mode === "working-tree" || mode === "working"
493
- ? [[`${resolvedRef}...HEAD`], ["HEAD"]]
494
- : [[`${resolvedRef}...HEAD`]];
567
+ const headCommit = getHeadCommit(root);
568
+ const isHeadRef = typeof base === "string" && (base === "HEAD" || base.startsWith("HEAD~") || base.startsWith("HEAD^") || base.startsWith("HEAD@"));
569
+ const isBaseAtHead = Boolean(!isHeadRef && headCommit && resolvedRef === headCommit);
495
570
 
496
571
  const out = [];
497
- for (const range of ranges) {
572
+ if (mode === "working-tree" || mode === "working") {
573
+ for (const range of [[`${resolvedRef}...HEAD`], ["HEAD"]]) {
574
+ let raw = "";
575
+ try {
576
+ raw = git(["diff", "--raw", "--no-renames", "-z", ...range], { cwd: root, raw: true, ignoreError: true }) || "";
577
+ } catch (_) {
578
+ continue;
579
+ }
580
+ parseRawDiffOutput(raw, out);
581
+ }
582
+ } else if (mode === "staged" || mode === "index") {
498
583
  let raw = "";
499
584
  try {
500
- raw = git(["diff", "--raw", "--no-renames", "-z", ...range], { cwd: root, raw: true, ignoreError: true }) || "";
501
- } catch (_) {
502
- continue;
503
- }
504
- // `--raw -z` emits ":<srcmode> <dstmode> <srcsha> <dstsha> <status>\0<path>\0".
505
- const fields = raw.split("\0").filter(Boolean);
506
- for (let i = 0; i < fields.length; i++) {
507
- const meta = fields[i];
508
- if (!meta.startsWith(":")) continue;
509
- const parts = meta.slice(1).split(/\s+/);
510
- const file = fields[i + 1];
511
- i += 1;
512
- if (!file) continue;
513
- out.push({
514
- file,
515
- srcMode: parts[0] || "",
516
- dstMode: parts[1] || "",
517
- srcSha: parts[2] || "",
518
- dstSha: parts[3] || "",
519
- status: (parts[4] || "").charAt(0),
520
- });
585
+ raw = git(["diff", "--raw", "--no-renames", "-z", "--cached", resolvedRef], { cwd: root, raw: true, ignoreError: true }) || "";
586
+ } catch (_) {}
587
+ parseRawDiffOutput(raw, out);
588
+ } else {
589
+ if (isBaseAtHead) {
590
+ const headParent = getHeadParent(root);
591
+ if (headParent) {
592
+ let raw = "";
593
+ try {
594
+ raw = git(["diff", "--raw", "--no-renames", "-z", `${headParent}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
595
+ } catch (_) {}
596
+ parseRawDiffOutput(raw, out);
597
+ } else {
598
+ let raw = "";
599
+ try {
600
+ raw = git(["diff-tree", "--root", "--no-commit-id", "-r", "--raw", "--no-renames", "-z", headCommit], { cwd: root, raw: true, ignoreError: true }) || "";
601
+ } catch (_) {}
602
+ parseRawDiffOutput(raw, out);
603
+ }
604
+ } else {
605
+ let raw = "";
606
+ try {
607
+ raw = git(["diff", "--raw", "--no-renames", "-z", `${resolvedRef}...HEAD`], { cwd: root, raw: true, ignoreError: true }) || "";
608
+ } catch (_) {}
609
+ parseRawDiffOutput(raw, out);
521
610
  }
522
611
  }
523
612
  return out;
@@ -878,6 +967,79 @@ export function partitionTracked(root = process.cwd(), files = []) {
878
967
  };
879
968
  }
880
969
 
970
+ // Root-level package-manager manifests a just-run `npm install` (or equivalent)
971
+ // writes while onboarding a cold-start repository. `npm init -y` creates a
972
+ // brand-new package.json; `npm install` adds/updates the lockfile. The scope
973
+ // rules legitimately `protect` these — but a tool's *own* install step must not
974
+ // brick the first interaction a newcomer has with it. The two helpers below
975
+ // give callers a way to tell "fresh onboarding artifact" (untracked) apart from
976
+ // "a dependency change the maintainer actually shipped" (tracked+modified), so
977
+ // the gate keeps its teeth while task creation and the init commit hint stop
978
+ // tripping over scaffolding the installer just laid down.
979
+ const ONBOARDING_ROOT_ARTIFACTS = [
980
+ "package.json",
981
+ "package-lock.json",
982
+ "npm-shrinkwrap.json",
983
+ "yarn.lock",
984
+ "pnpm-lock.yaml",
985
+ "bun.lock",
986
+ "bun.lockb",
987
+ ];
988
+
989
+ /**
990
+ * Among the given repo-relative paths, which are *untracked* root-level
991
+ * package-manager artifacts — i.e. manifests a fresh `npm init -y`/install just
992
+ * created rather than files the maintainer committed and an agent then rewrote?
993
+ *
994
+ * The scope rules flag a brand-new package.json as `protect`, which is the
995
+ * right call for the gate. But a task-create preflight that rejects a brand-new
996
+ * repository because its own onboarding just produced package.json is a cold
997
+ * start deadlock: the user has not committed anything yet, so they cannot make
998
+ * the preflight pass without guessing the secret incantation. Modified
999
+ * *tracked* lockfiles are deliberately NOT returned here — those stay protected
1000
+ * as supply-chain tampering.
1001
+ *
1002
+ * @param {string} root
1003
+ * @param {string[]} files repo-relative paths (as reported by a scope phase)
1004
+ * @returns {string[]}
1005
+ */
1006
+ export function untrackedOnboardingArtifacts(root = process.cwd(), files = []) {
1007
+ const { untracked } = partitionTracked(root, files);
1008
+ if (untracked.length === 0) return [];
1009
+ return untracked.filter((f) => ONBOARDING_ROOT_ARTIFACTS.includes(f));
1010
+ }
1011
+
1012
+ /**
1013
+ * Root-level package-manager artifacts a `git add` in this repo would actually
1014
+ * stage *right now* — present on disk and not gitignored — for a commit hint.
1015
+ *
1016
+ * A cold-start repo has these as brand-new untracked files; an existing repo
1017
+ * that just ran `npm install -D` may have a modified (tracked) package.json
1018
+ * plus a new lockfile. Gitignored lockfiles (a repo that chooses not to track
1019
+ * them) are excluded, so the suggested `git add …` never errors on a path git
1020
+ * refuses to add.
1021
+ *
1022
+ * @param {string} root
1023
+ * @returns {string[]}
1024
+ */
1025
+ export function addableOnboardingArtifacts(root = process.cwd()) {
1026
+ let porcelain = "";
1027
+ try {
1028
+ // `--untracked-files=all` so a lone new file appears verbatim, not as a
1029
+ // collapsed `dir/` entry that would not match a root manifest name.
1030
+ porcelain = git(["status", "--porcelain", "--untracked-files=all"], { cwd: root, ignoreError: true }) || "";
1031
+ } catch (_) {
1032
+ return [];
1033
+ }
1034
+ const changed = new Set();
1035
+ for (const line of porcelain.split("\n")) {
1036
+ // Porcelain v1: two status chars, one space, then the path.
1037
+ const path = line.length > 3 ? line.slice(3) : "";
1038
+ if (path && !path.startsWith('"')) changed.add(path);
1039
+ }
1040
+ return ONBOARDING_ROOT_ARTIFACTS.filter((a) => changed.has(a) && existsSync(join(root, a)));
1041
+ }
1042
+
881
1043
  export function detectDefaultBranch(root = process.cwd()) {
882
1044
  // `git()` returns trimmed stdout, and "" for a non-zero exit under
883
1045
  // ignoreError — there is no status field to read.
@@ -896,7 +1058,12 @@ export function detectDefaultBranch(root = process.cwd()) {
896
1058
  }
897
1059
 
898
1060
  for (const candidate of ["main", "master"]) {
899
- if (ask(["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`])) return candidate;
1061
+ if (
1062
+ ask(["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]) ||
1063
+ ask(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${candidate}`])
1064
+ ) {
1065
+ return candidate;
1066
+ }
900
1067
  }
901
1068
 
902
1069
  const current = ask(["branch", "--show-current"]);
@@ -914,3 +1081,107 @@ export function resolveGitRemoteOrigin(root = process.cwd()) {
914
1081
  }
915
1082
  }
916
1083
 
1084
+ /**
1085
+ * Default globs for generated, bundled, and lockfile paths that exhaust the diff budget.
1086
+ */
1087
+ export const DEFAULT_GENERATED_GLOBS = [
1088
+ "dist/**",
1089
+ "**/dist/**",
1090
+ "**/*.map",
1091
+ "**/*-lock.yaml",
1092
+ "**/*-lock.json",
1093
+ "**/package-lock.json",
1094
+ "**/pnpm-lock.yaml",
1095
+ "**/yarn.lock",
1096
+ "**/Cargo.lock",
1097
+ "**/generated/**",
1098
+ ];
1099
+
1100
+ const REGEXP_SPECIAL = ".+?^${}()|[]\\";
1101
+
1102
+ /**
1103
+ * Convert a glob with * and ** into an anchored RegExp.
1104
+ * @param {string} glob
1105
+ * @returns {RegExp}
1106
+ */
1107
+ export function globToRegExp(glob = "") {
1108
+ let re = "";
1109
+ for (let i = 0; i < glob.length; i++) {
1110
+ const c = glob[i];
1111
+ if (c === "*") {
1112
+ if (glob[i + 1] === "*") {
1113
+ if (glob[i + 2] === "/") {
1114
+ re += "(?:.*/)?";
1115
+ i += 2;
1116
+ } else {
1117
+ re += ".*";
1118
+ i += 1;
1119
+ }
1120
+ } else {
1121
+ re += "[^/]*";
1122
+ }
1123
+ } else if (REGEXP_SPECIAL.includes(c)) {
1124
+ re += "\\" + c;
1125
+ } else {
1126
+ re += c;
1127
+ }
1128
+ }
1129
+ return new RegExp("^" + re + "$");
1130
+ }
1131
+
1132
+ /**
1133
+ * Filter out per-file sections whose target path matches any ignore glob from a unified git diff.
1134
+ * Returns the filtered diff text and the array of excluded file paths.
1135
+ * If filtering would remove all file sections, the original diff is preserved.
1136
+ *
1137
+ * @param {string} diff - Unified git diff
1138
+ * @param {string[]} [ignoreGlobs=DEFAULT_GENERATED_GLOBS] - List of glob patterns
1139
+ * @returns {{ diff: string, excludedPaths: string[] }}
1140
+ */
1141
+ export function filterDiffByPaths(diff = "", ignoreGlobs = DEFAULT_GENERATED_GLOBS) {
1142
+ if (!diff || typeof diff !== "string" || !ignoreGlobs || ignoreGlobs.length === 0) {
1143
+ return { diff: diff || "", excludedPaths: [] };
1144
+ }
1145
+
1146
+ const matchers = ignoreGlobs.map(globToRegExp);
1147
+ const sections = diff.split(/(?=^diff --git )/m);
1148
+ const kept = [];
1149
+ const excludedPaths = [];
1150
+ let fileSectionsCount = 0;
1151
+
1152
+ for (const section of sections) {
1153
+ if (!section.startsWith("diff --git ")) {
1154
+ if (section.length > 0) kept.push(section);
1155
+ continue;
1156
+ }
1157
+
1158
+ fileSectionsCount++;
1159
+ const match = section.match(/^diff --git a\/.*? b\/(.+)$/m);
1160
+ let path = match ? match[1].trim() : undefined;
1161
+ if (path) {
1162
+ path = path.replace(/^["']|["']$/g, "");
1163
+ }
1164
+
1165
+ if (path && matchers.some((re) => re.test(path))) {
1166
+ excludedPaths.push(path);
1167
+ continue;
1168
+ }
1169
+ kept.push(section);
1170
+ }
1171
+
1172
+ if (fileSectionsCount > 0 && excludedPaths.length === fileSectionsCount) {
1173
+ return { diff, excludedPaths: [] };
1174
+ }
1175
+
1176
+ return { diff: kept.join(""), excludedPaths };
1177
+ }
1178
+
1179
+ /**
1180
+ * Compute byte length of diff after pruning generated files and lockfiles.
1181
+ */
1182
+ export function prunedDiffBytes(root = process.cwd(), base = "main", mode = "committed", ignoreGlobs = DEFAULT_GENERATED_GLOBS) {
1183
+ const text = diffText(root, base, mode);
1184
+ const filtered = filterDiffByPaths(text, ignoreGlobs);
1185
+ return Buffer.byteLength(filtered.diff, "utf-8");
1186
+ }
1187
+