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
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Codebase scanner for TODO and FIXME comments.
3
+ *
4
+ * Safety limits: only source-ish extensions are read, files larger than
5
+ * SCAN_MAX_FILE_BYTES are skipped, buffers containing null bytes are treated
6
+ * as binary and skipped, and paths matched by the scan root's .gitignore are
7
+ * never descended into or read.
8
+ *
9
+ * This is the home of the scanner that used to live in
10
+ * `scripts/jules-scan-todos.mjs`; that file is now a minimal CLI entry point.
11
+ */
12
+
13
+ import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
14
+ import { join, extname, relative } from "node:path";
15
+
16
+ export const SCAN_ALLOWED_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".py", ".rs", ".go", ".md"]);
17
+ export const SCAN_MAX_FILE_BYTES = 1024 * 1024;
18
+
19
+ function globToRegExp(glob) {
20
+ let out = "";
21
+ for (let i = 0; i < glob.length; i++) {
22
+ const ch = glob[i];
23
+ if (ch === "*") {
24
+ if (glob[i + 1] === "*") {
25
+ out += ".*";
26
+ i++;
27
+ if (glob[i + 1] === "/") i++;
28
+ } else {
29
+ out += "[^/]*";
30
+ }
31
+ } else if (ch === "?") {
32
+ out += "[^/]";
33
+ } else {
34
+ out += ch.replace(/[.*+?^${}()|[\]\\]/, "\\$&");
35
+ }
36
+ }
37
+ return new RegExp(`^${out}$`);
38
+ }
39
+
40
+ function loadGitignorePatterns(dir) {
41
+ const ignoreFile = join(dir, ".gitignore");
42
+ if (!existsSync(ignoreFile)) return [];
43
+ let raw;
44
+ try {
45
+ raw = readFileSync(ignoreFile, "utf-8");
46
+ } catch (_) {
47
+ return [];
48
+ }
49
+ const patterns = [];
50
+ for (let line of raw.split("\n")) {
51
+ line = line.trim();
52
+ if (!line || line.startsWith("#")) continue;
53
+ let negated = false;
54
+ if (line.startsWith("!")) {
55
+ negated = true;
56
+ line = line.slice(1).trim();
57
+ if (!line) continue;
58
+ }
59
+ if (line.startsWith("/")) line = line.slice(1);
60
+ const dirOnly = line.endsWith("/");
61
+ if (dirOnly) line = line.slice(0, -1);
62
+ const hasSlash = line.includes("/");
63
+ patterns.push({ negated, dirOnly, hasSlash, regex: globToRegExp(line), literal: line });
64
+ }
65
+ return patterns;
66
+ }
67
+
68
+ function isIgnored(relPath, isDir, patterns) {
69
+ if (patterns.length === 0) return false;
70
+ const base = relPath.split("/").pop();
71
+ let ignored = false;
72
+ for (const p of patterns) {
73
+ // A file pattern also ignores everything beneath a matched directory.
74
+ const candidate = p.hasSlash ? relPath : base;
75
+ let hit = p.regex.test(candidate);
76
+ if (!hit && p.dirOnly && (relPath === p.literal || relPath.startsWith(`${p.literal}/`))) hit = true;
77
+ if (!hit && p.hasSlash && relPath.startsWith(`${p.literal}/`)) hit = true;
78
+ if (hit && !(p.dirOnly && !isDir && !relPath.startsWith(`${p.literal}/`))) {
79
+ ignored = !p.negated;
80
+ }
81
+ }
82
+ return ignored;
83
+ }
84
+
85
+ export function scanCodebaseForTodos(dir = process.cwd()) {
86
+ const todos = [];
87
+ const patterns = loadGitignorePatterns(dir);
88
+ function walk(current) {
89
+ let entries = [];
90
+ try { entries = readdirSync(current); } catch (_) { return; }
91
+ for (const entry of entries) {
92
+ if (entry === "node_modules" || entry === ".git") continue;
93
+ const full = join(current, entry);
94
+ try {
95
+ const stat = statSync(full);
96
+ const rel = relative(dir, full).split("\\").join("/");
97
+ if (rel && isIgnored(rel, stat.isDirectory(), patterns)) continue;
98
+ if (stat.isDirectory()) {
99
+ walk(full);
100
+ } else if (stat.isFile()) {
101
+ if (!SCAN_ALLOWED_EXTENSIONS.has(extname(entry).toLowerCase())) continue;
102
+ if (stat.size > SCAN_MAX_FILE_BYTES) continue;
103
+ let content;
104
+ try {
105
+ content = readFileSync(full);
106
+ } catch (_) {
107
+ continue;
108
+ }
109
+ if (content.includes(0)) continue;
110
+ const lines = content.toString("utf-8").split("\n");
111
+ lines.forEach((line, idx) => {
112
+ if (line.includes("TODO:")) {
113
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", type: "TODO", priority: "MEDIUM" });
114
+ } else if (line.includes("FIXME:")) {
115
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", type: "FIXME", priority: "HIGH" });
116
+ }
117
+ });
118
+ }
119
+ } catch (_) {}
120
+ }
121
+ }
122
+ walk(dir);
123
+ return todos;
124
+ }
125
+
126
+ export function runScanner(dir = process.cwd()) {
127
+ const todos = scanCodebaseForTodos(dir);
128
+ return { todos, count: todos.length };
129
+ }
@@ -388,13 +388,13 @@ The test must fail on a hand-broken fixture before you consider it done.`;
388
388
  1. **Prove Each Test Can Fail (Mutation Falsification)**:
389
389
  - For every test in scope, deliberately introduce a defect into the code under test (${strategy}), run only that test, and confirm it turns red.
390
390
  - Immediately revert every deliberate code mutation before proceeding to the next test.
391
- - A test nobody has seen fail is an unverified assumption; delete or rewrite tests that remain green under deliberate mutation.
391
+ - A test nobody has seen fail is an unverified assumption; add targeted assertions or classify equivalent mutants for tests that remain green under deliberate mutation. Never delete surviving tests.
392
392
  2. **Eliminate Vacuous & Tautological Checks**:
393
393
  - Strip tests that mock the subject under test (measuring the mock rather than the implementation).
394
394
  - Replace shallow shape assertions (\`toBeDefined()\`, \`assertTrue(result)\`, \`is not None\`) with precise value assertions derived from requirements.
395
395
  3. **Reconcile Collected vs Passed Counts**:
396
396
  - Compare test runner collected test count against actual test function count to detect uncollected or silently ignored test files.
397
- - Document every deleted tautological test with its specific failure mode.`;
397
+ - Document every purged tautological test with its specific failure mode.`;
398
398
  }
399
399
  },
400
400
 
@@ -920,7 +920,7 @@ export function synthesizeWebEnvelope(templateId, userParams = {}, options = {})
920
920
 
921
921
  if (explorationBudget) {
922
922
  lines.push("## Google Labs Exploration Budget Protocol (3-Phase Discovery)");
923
- lines.push("To maximize diagnostic accuracy (Hit@5 57%), execute this task in 3 distinct phases:");
923
+ lines.push("Use deep planning mode. execute this task in 3 distinct phases (Discovery, Test Formulation, Surgical Implementation):");
924
924
  lines.push("1. **PHASE 1: DISCOVERY & SYMBOL TRACING (Stay Silent, Write NO Code)**");
925
925
  lines.push(" - Read target component files, CSS definitions, imports, and existing test specs.");
926
926
  lines.push(" - Formulate diagnostic hypothesis and verify symbol signatures before planning edits.");
package/src/webhook.mjs CHANGED
@@ -465,7 +465,7 @@ export async function dispatchEscalation(incident = {}, config = {}) {
465
465
 
466
466
  /**
467
467
  * Verify GitHub webhook HMAC SHA-256 signature against request payload.
468
- * @param {Buffer|string} payload - Raw request body
468
+ * @param {Uint8Array|string} payload - Raw request body
469
469
  * @param {string} signatureHeader - Value of X-Hub-Signature-256 header
470
470
  * @param {string} secret - Configured webhook secret
471
471
  * @returns {boolean} True if signature matches
@@ -493,7 +493,7 @@ export function verifySignature(payload, signatureHeader, secret) {
493
493
 
494
494
  /**
495
495
  * Parse incoming webhook body buffer into a JS object.
496
- * @param {Buffer|Uint8Array|string} bodyBuf
496
+ * @param {Uint8Array|string} bodyBuf
497
497
  * @param {string} contentType
498
498
  * @returns {object} Parsed JSON payload
499
499
  */
@@ -620,7 +620,14 @@ export function createWebhookServer({ port = 8787, secret = process.env.JULES_WE
620
620
 
621
621
  req.on("end", () => {
622
622
  if (totalSize > MAX_BODY) return;
623
- const bodyBuf = Buffer.concat(chunks);
623
+
624
+ const bodyBuf = new Uint8Array(totalSize);
625
+ let offset = 0;
626
+ for (const chunk of chunks) {
627
+ bodyBuf.set(chunk, offset);
628
+ offset += chunk.length;
629
+ }
630
+
624
631
  const sigHeader = req.headers["x-hub-signature-256"];
625
632
  const eventType = req.headers["x-github-event"];
626
633
  const contentType = req.headers["content-type"] || "application/json";
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync, mkdirSync, readdirSync } from "node:fs";
1
+ import { existsSync, readFileSync, mkdirSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseYaml, yamlScalar, TIER_PRESETS, VENDOR_TIERS, FALLBACK_TIER } from "./config.mjs";
4
+ import { safeAtomicWrite } from "./security.mjs";
4
5
  import { suggestProvider, detectAvailableProviders } from "./provider-readiness.mjs";
5
6
  import { detectDefaultBranch } from "./git.mjs";
6
7
  import { resolveWorkspaceBoundary, oracleCandidates } from "./stack-detector.mjs";
@@ -10,23 +11,6 @@ import { parseCollectedTests, producedNoOutput, looksLikeTestSuiteCommand } from
10
11
  import { select, multiSelect, input, confirm, spinner, isTTY } from "./tui.mjs";
11
12
  import { KIT_VERSION } from "./version.mjs";
12
13
 
13
- /**
14
- * Write a file atomically using a temporary file and atomic rename.
15
- * @param {string} filePath
16
- * @param {string} content
17
- */
18
- function writeAtomic(filePath, content) {
19
- const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
20
- const fd = openSync(tmpPath, "w");
21
- try {
22
- writeFileSync(fd, content, "utf-8");
23
- fsyncSync(fd);
24
- } finally {
25
- closeSync(fd);
26
- }
27
- renameSync(tmpPath, filePath);
28
- }
29
-
30
14
  /**
31
15
  * Snake_case projection of {@link TIER_PRESETS} for the YAML the wizard writes.
32
16
  * Derived rather than declared: as a second literal table it drifted out of sync
@@ -125,6 +109,14 @@ export function planInit(root = process.cwd(), options = {}) {
125
109
  // a 100-task budget and 8 concurrent workers it does not have. The same
126
110
  // reasoning makes FALLBACK_TIER conservative in loadConfig(); the wizard has
127
111
  // to agree with it or the manifest guards a ceiling the runtime does not.
112
+ const validTiers = Object.keys(TIER_PRESETS);
113
+ if (options.tier && !validTiers.includes(String(options.tier).toLowerCase())) {
114
+ const rawTier = String(options.tier).toLowerCase();
115
+ if (["minimal", "standard", "max"].includes(rawTier)) {
116
+ throw new Error(`Invalid tier "${options.tier}". Valid tiers: ${validTiers.join(", ")}. Did you mean '--profile ${options.tier}'?`);
117
+ }
118
+ throw new Error(`Invalid tier "${options.tier}". Valid tiers: ${validTiers.join(", ")}.`);
119
+ }
128
120
  const tierName = options.tier || FALLBACK_TIER;
129
121
  // An unrecognised name resolves the same way loadConfig() resolves it, so the
130
122
  // scaffolded limits always match what the runtime will later enforce.
@@ -572,8 +564,8 @@ export async function runInitWizard(root = process.cwd(), options = {}) {
572
564
  const configPath = join(agentDir, "config.yml");
573
565
  const julesPath = join(agentDir, "jules.yml");
574
566
 
575
- writeAtomic(configPath, plan.configYaml);
576
- writeAtomic(julesPath, plan.julesYaml);
567
+ safeAtomicWrite(configPath, plan.configYaml);
568
+ safeAtomicWrite(julesPath, plan.julesYaml);
577
569
 
578
570
  return {
579
571
  ok: true,
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { detectStack, parseYaml } from "./config.mjs";
5
5
  import { runCmd } from "./git.mjs";
6
+ import { pytestCmd } from "./stack-detector.mjs";
6
7
 
7
8
  /**
8
9
  * Check if a command-line binary is available on the system PATH.
@@ -92,7 +93,7 @@ export function detectStackOracles(root = process.cwd()) {
92
93
  }
93
94
 
94
95
  // Check Go module
95
- if (stack.includes("go") || existsSync(join(root, "go.mod"))) {
96
+ if (stack === "go" || existsSync(join(root, "go.mod"))) {
96
97
  candidates.testCmd = candidates.testCmd || "go test ./...";
97
98
  candidates.buildCmd = candidates.buildCmd || "go build ./...";
98
99
  if (hasBinary("golangci-lint")) {
@@ -102,8 +103,8 @@ export function detectStackOracles(root = process.cwd()) {
102
103
  }
103
104
 
104
105
  // Check Python
105
- if (stack.includes("pytest") || existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "requirements.txt"))) {
106
- candidates.testCmd = candidates.testCmd || "pytest";
106
+ if (stack === "python" || stack === "django" || existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "requirements.txt"))) {
107
+ candidates.testCmd = candidates.testCmd || pytestCmd(process.env, root);
107
108
  candidates.buildCmd = candidates.buildCmd || "";
108
109
  if (hasBinary("flake8")) {
109
110
  candidates.lintCmd = candidates.lintCmd || "flake8 .";
@@ -1,10 +1,11 @@
1
1
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { untrackedOnboardingArtifacts } from "./git.mjs";
3
4
  import { loadConfig } from "./config.mjs";
4
5
  import { gate } from "./engine.mjs";
5
6
  import { scanDiff, shannonEntropy } from "./security.mjs";
6
7
  import { getQueueDir } from "./state.mjs";
7
- import { scanCodebaseForTodos } from "../scripts/jules-scan-todos.mjs";
8
+ import { scanCodebaseForTodos } from "./todo-scanner.mjs";
8
9
  import { select, input, confirm, spinner, isTTY } from "./tui.mjs";
9
10
  import { scorePromptFalsifiability } from "./task-optimizer.mjs";
10
11
  import { getWebTemplate, synthesizeWebEnvelope } from "./web-templates.mjs";
@@ -110,16 +111,21 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
110
111
  const config = loadConfig(root);
111
112
 
112
113
  let title = inputObj.title;
113
- let rawPrompt = inputObj.prompt || "";
114
+ let rawPrompt = (inputObj.prompt || "").trim();
114
115
  let verifyCmd = (inputObj.verifyCmd || config.verify.test || config.verify.build || "").trim();
115
116
 
117
+ // Validate that a task objective or template was provided before injecting role instructions
118
+ if (!rawPrompt && !inputObj.template) {
119
+ throw new Error("Task prompt cannot be empty.");
120
+ }
121
+
116
122
  // 0a. Process Specialist Role if specified
117
123
  let resolvedRole = null;
118
124
  if (inputObj.role) {
119
- resolvedRole = resolveRolePrompt(root, inputObj.role);
125
+ resolvedRole = resolveRolePrompt(root, inputObj.role, { config });
120
126
  if (!resolvedRole) {
121
127
  throw new Error(
122
- `Unknown agent role '${inputObj.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Overseer, Bolt, Sentinel, Janitor).`
128
+ `Unknown agent role '${inputObj.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Auditor, Performance, Security, Hygiene, Testing).`
123
129
  );
124
130
  }
125
131
  rawPrompt = `${resolvedRole.content}\n\n${rawPrompt}`.trim();
@@ -164,8 +170,19 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
164
170
  if (!secretScan.ok) {
165
171
  secretScan.findings.forEach((f) => secretFindings.push({ id: f.type || f.id || "SECRET_LEAK", ...f }));
166
172
  }
167
- const hasHighEntropyToken = rawPrompt.split(/\s+/).some((token) => token.length >= 20 && shannonEntropy(token) > 4.3);
168
- const isShortHighEntropy = rawPrompt.length <= 120 && shannonEntropy(rawPrompt) > 4.5;
173
+ const isPathOrUrlToken = (str) => {
174
+ const clean = str.replace(/^[(\[{<`"']+|[)\]}>`"',;:]+$/g, "");
175
+ if (/^(https?|file|ftp|vscode|git|ssh|node):/i.test(clean)) return true;
176
+ if (/^[a-zA-Z]:[/\\]/.test(clean)) return true;
177
+ if (/^[./~\\]/.test(clean)) return true;
178
+ if (/[/\\][a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(:\d+(:\d+)?)?$/.test(clean)) return true;
179
+ return false;
180
+ };
181
+ const hasHighEntropyToken = rawPrompt
182
+ .split(/\s+/)
183
+ .some((token) => !isPathOrUrlToken(token) && token.length >= 20 && shannonEntropy(token) > 4.3);
184
+ const isShortHighEntropy =
185
+ rawPrompt.length <= 120 && !isPathOrUrlToken(rawPrompt.trim()) && shannonEntropy(rawPrompt) > 4.5;
169
186
  if ((hasHighEntropyToken || isShortHighEntropy) && !inputObj.allowHighEntropy) {
170
187
  secretFindings.push({ id: "HIGH_ENTROPY_PROMPT", line: 1 });
171
188
  }
@@ -344,9 +361,20 @@ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
344
361
  // Perform Gate Preflight
345
362
  const gateRes = await gate({ root, mode: "working-tree" });
346
363
  if (!gateRes.ok && (gateRes.code === 3 || gateRes.code === 6) && !options.allowGateFailure) {
347
- // Filter out uncommitted .agent/ config scope warnings if user initialized locally
348
- const realViolations = gateRes.phases[0]?.violations?.filter((v) => !v.file.startsWith(".agent/")) || [];
349
- if (realViolations.length > 0 || gateRes.code === 6) {
364
+ const scopeViolations = gateRes.phases[0]?.violations || [];
365
+ // A cold-start repository has nothing committed yet: every path init just
366
+ // wrote — the .agent/ manifest, and the package.json / lockfile a fresh
367
+ // `npm install` produced — reads as a scope violation because nothing is
368
+ // on the base branch. Failing task creation on scaffolding the user cannot
369
+ // have committed *before* installing the tool is a dead end. Untracked
370
+ // root package-manager artifacts are just-run-install output (not a
371
+ // dependency change the maintainer shipped), so they are filtered out the
372
+ // same way the .agent/ scaffolding already is. A modified *tracked*
373
+ // lockfile stays protected and still rejects the task.
374
+ const realViolations = scopeViolations.filter((v) => !v.file.startsWith(".agent/")) || [];
375
+ const installArtifacts = new Set(untrackedOnboardingArtifacts(root, scopeViolations.map((v) => v.file)));
376
+ const blockers = realViolations.filter((v) => !installArtifacts.has(v.file));
377
+ if (blockers.length > 0 || gateRes.code === 6) {
350
378
  throw new Error(`Gate Preflight Rejected Task: Repository contains scope or secret violations (Exit ${gateRes.code}).`);
351
379
  }
352
380
  }
@@ -1,18 +0,0 @@
1
- # Sentinel - Security Audit & Hardening Specialist 🛡️
2
-
3
- > **Role:** Codebase Security Auditor & AST Vulnerability Scanner.
4
- > **Scope:** Input sanitization, secret scanning, RBAC verification, and prompt injection defense.
5
-
6
- ## Core Directives
7
-
8
- 1. **Vulnerability Mitigation:**
9
- - Scan for unescaped SQL queries, `eval()`, dynamic `exec()`, or unvalidated shell arguments.
10
- - Enforce explicit input validation and type coercion on all external API entry points.
11
-
12
- 2. **Secret Leak Prevention:**
13
- - Ensure credentials, private keys, API tokens, and JWT secrets are loaded strictly from `process.env`.
14
- - Never log sensitive tokens or unmasked PII into console logs or file artifacts.
15
-
16
- 3. **Untrusted Fencing:**
17
- - Label user-controllable input data with `<UNTRUSTED>` fencing tags.
18
- - Instruct parsing logic to fail-closed on malformed or malicious payload structures.
package/scripts/utils.mjs DELETED
@@ -1,241 +0,0 @@
1
- /**
2
- * Common scripting utilities and helper functions.
3
- */
4
-
5
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
6
- import { join, resolve } from "node:path";
7
- import { randomUUID } from "node:crypto";
8
- import os from "node:os";
9
- import { resolveRoot } from "../src/config.mjs";
10
- import { appendLedger, checkDailyBudget as baseCheckDailyBudget, verifyLedgerIntegrity as baseVerifyLedgerIntegrity } from "../src/state.mjs";
11
-
12
- export { loadConfig, resolveRoot, normalizePath } from "../src/config.mjs";
13
- export {
14
- shannonEntropy,
15
- shannonEntropy as calculateShannonEntropy,
16
- redactSecrets,
17
- anonymizePii,
18
- matchesGlob,
19
- isForbiddenPath,
20
- hasHighConfidenceSecret,
21
- hasLowConfidenceSecret,
22
- HIGH_CONFIDENCE_PATTERNS,
23
- LOW_CONFIDENCE_PATTERNS,
24
- } from "../src/security.mjs";
25
- export { git, runCmd } from "../src/git.mjs";
26
- export { appendLedger, getDailyLedgerPath, ensureDir } from "../src/state.mjs";
27
-
28
- export const log = {
29
- info: (msg) => console.log(`ℹ️ ${msg}`),
30
- success: (msg) => console.log(`✅ ${msg}`),
31
- warn: (msg) => console.warn(`⚠️ ${msg}`),
32
- error: (msg) => console.error(`❌ ${msg}`),
33
- step: (stepStr, msg) => console.log(`${stepStr} ${msg}`),
34
- dim: (msg) => console.log(msg),
35
- header: (msg) => console.log(`\n=== ${msg} ===\n`),
36
- groupEnd: () => {},
37
- };
38
-
39
- export function timestamp() {
40
- return new Date().toISOString();
41
- }
42
-
43
- export function loadEnv(targetDir = process.cwd()) {
44
- const envPath = join(targetDir, ".env");
45
- if (!existsSync(envPath)) return;
46
- try {
47
- const raw = readFileSync(envPath, "utf-8");
48
- for (const line of raw.split(/\r?\n/)) {
49
- const trimmed = line.trim();
50
- if (!trimmed || trimmed.startsWith("#")) continue;
51
- const clean = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
52
- const eqIdx = clean.indexOf("=");
53
- if (eqIdx > 0) {
54
- const k = clean.slice(0, eqIdx).trim();
55
- let v = clean.slice(eqIdx + 1).trim();
56
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
57
- v = v.slice(1, -1);
58
- }
59
- process.env[k] = v;
60
- }
61
- }
62
- } catch (_) {}
63
- }
64
-
65
- export function getIsolatedCacheDir() {
66
- return process.env.JULES_CACHE_DIR ? resolve(process.env.JULES_CACHE_DIR) : join(os.homedir(), ".cache", "jules-orchestrator-kit");
67
- }
68
-
69
- export function ensureSdkCacheIsolation() {
70
- const dir = getIsolatedCacheDir();
71
- if (!existsSync(dir)) {
72
- try { mkdirSync(dir, { recursive: true }); } catch (_) {}
73
- }
74
- process.env.JULES_CACHE_DIR = dir;
75
- return dir;
76
- }
77
-
78
- export function logToHistory(filename, content) {
79
- const dateStr = new Date().toISOString().split("T")[0];
80
- const historyDir = resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/history");
81
- if (!existsSync(historyDir)) {
82
- try { mkdirSync(historyDir, { recursive: true }); } catch (_) {}
83
- }
84
- const filePath = join(historyDir, `${dateStr}-${filename}`);
85
- try { writeFileSync(filePath, content, "utf-8"); } catch (_) {}
86
- return filePath;
87
- }
88
-
89
- export function resolveMarkdownConflict(content) {
90
- if (!content || typeof content !== "string") return "";
91
- const lines = content.split("\n");
92
- const result = [];
93
- for (const line of lines) {
94
- if (line.startsWith("<<<<<<<") || line.startsWith("=======") || line.startsWith(">>>>>>>")) {
95
- continue;
96
- }
97
- result.push(line);
98
- }
99
- return result.join("\n");
100
- }
101
-
102
- export function pruneOldLedgers(stateDir, retentionDays = 30) {
103
- if (!existsSync(stateDir)) return;
104
- const cutoff = Date.now() - retentionDays * 86400 * 1000;
105
- try {
106
- const files = readdirSync(stateDir);
107
- for (const f of files) {
108
- const full = join(stateDir, f);
109
- try {
110
- const stat = statSync(full);
111
- if (stat.isFile() && stat.mtimeMs < cutoff) {
112
- unlinkSync(full);
113
- }
114
- } catch (_) {}
115
- }
116
- } catch (_) {}
117
- }
118
-
119
- export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
120
- let root = typeof arg1 === "string" ? arg1 : resolveRoot();
121
- let limit = typeof arg1 === "number" ? arg1 : (typeof arg2 === "number" ? arg2 : 300);
122
-
123
- const res = baseCheckDailyBudget(root, limit);
124
- // Filter for budget_reserved events if present
125
- const ledgerPath = join(root, `.agent/state/ledger-${new Date().toISOString().split("T")[0]}.jsonl`);
126
- if (existsSync(ledgerPath)) {
127
- try {
128
- const lines = readFileSync(ledgerPath, "utf-8").split("\n").filter(Boolean);
129
- const reservedCount = lines.filter((l) => l.includes('"event":"budget_reserved"')).length;
130
- if (reservedCount > 0) {
131
- return { ok: reservedCount < limit, used: reservedCount, budget: limit, remaining: Math.max(0, limit - reservedCount) };
132
- }
133
- } catch (_) {}
134
- }
135
- return res;
136
- }
137
-
138
- /**
139
- * @param {number} [maxSessions=300]
140
- * @param {string} [taskKey=""]
141
- * @param {string} [root] - Ledger root. Defaults to the git toplevel. Pass an
142
- * explicit root to keep callers (notably tests) off the operator's real ledger.
143
- */
144
- export function reserveDailyBudget(maxSessions = 300, taskKey = "", root = resolveRoot()) {
145
- // The id is what makes the reservation releasable. Written without one, a
146
- // reservation counted against the day and no rollback, commit or reconcile
147
- // could ever name it again — it stayed charged until the ledger rotated.
148
- const reservationId = `res-${Date.now()}-${randomUUID().slice(0, 8)}`;
149
- appendLedger({ event: "budget_reserved", reservationId, key: taskKey }, root);
150
- const check = checkDailyBudget(root, maxSessions);
151
- return { ok: check.ok, used: check.used, budget: maxSessions, reservationId };
152
- }
153
-
154
- export function verifyLedgerIntegrity(filePath) {
155
- if (!existsSync(filePath)) return { ok: false, count: 0 };
156
- try {
157
- const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
158
- let hasHashes = true;
159
- for (const line of lines) {
160
- const obj = JSON.parse(line);
161
- if (!obj.hash) hasHashes = false;
162
- }
163
- if (hasHashes && lines.length > 0) {
164
- return baseVerifyLedgerIntegrity(filePath);
165
- }
166
- return { ok: true, count: lines.length, lastHash: "sha256-verified" };
167
- } catch (err) {
168
- return { ok: false, count: 0, error: err.message || "Invalid JSON in ledger" };
169
- }
170
- }
171
-
172
- export function acquireBudgetLock(_root) {
173
- return true;
174
- }
175
-
176
- export function releaseBudgetLock(_root) {}
177
-
178
- export function extractPrUrls(outputs = []) {
179
- const prs = [];
180
- const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
181
- for (const item of outputs) {
182
- const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
183
- const matches = text.match(regex);
184
- if (matches) {
185
- for (const m of matches) {
186
- if (!prs.includes(m)) prs.push(m);
187
- }
188
- }
189
- }
190
- return prs;
191
- }
192
-
193
- export function auditSessions(sessions = [], opts = {}) {
194
- const staleHoursThreshold = opts.staleHoursThreshold || 24;
195
- const now = Date.now();
196
- const merged = [];
197
- const active = [];
198
- const stale = [];
199
-
200
- for (const s of sessions) {
201
- if (s.state === "MERGED") {
202
- merged.push(s);
203
- } else {
204
- const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
205
- const ageHours = (now - updatedMs) / (1000 * 60 * 60);
206
- if (ageHours > staleHoursThreshold) {
207
- stale.push(s);
208
- } else {
209
- active.push(s);
210
- }
211
- }
212
- }
213
-
214
- return { merged, active, stale };
215
- }
216
-
217
- export function buildSyncManifest(tasks = []) {
218
- const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
219
- return {
220
- version: 1,
221
- totalTasks: tasks.length,
222
- reservations,
223
- };
224
- }
225
-
226
- export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
227
- const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
228
- const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
229
-
230
- const agentDir = join(projectRoot, ".agent");
231
- if (!existsSync(agentDir)) {
232
- try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
233
- }
234
- const manifestPath = join(agentDir, "sync-manifest.json");
235
- try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
236
-
237
- if (isDry) return { status: "DRY_RUN", path: manifestPath };
238
- if (isRemote) return { status: "PUSHED", path: manifestPath };
239
- return { status: "SAVED_LOCAL", path: manifestPath };
240
- }
241
-