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
|
@@ -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
|
+
}
|
package/src/web-templates.mjs
CHANGED
|
@@ -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;
|
|
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
|
|
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("
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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";
|
package/src/wizard-init.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync,
|
|
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
|
-
|
|
576
|
-
|
|
567
|
+
safeAtomicWrite(configPath, plan.configYaml);
|
|
568
|
+
safeAtomicWrite(julesPath, plan.julesYaml);
|
|
577
569
|
|
|
578
570
|
return {
|
|
579
571
|
ok: true,
|
package/src/wizard-oracle.mjs
CHANGED
|
@@ -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
|
|
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
|
|
106
|
-
candidates.testCmd = candidates.testCmd ||
|
|
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 .";
|
package/src/wizard-task.mjs
CHANGED
|
@@ -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 "
|
|
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.
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|