jules-orchestrator-kit 0.8.1 → 0.9.3
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/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/.agent/rules/dynamic-guardrails.json +0 -4
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +7 -2
- package/README.md +113 -21
- package/bin/agentctl.mjs +274 -0
- package/bin/init.js +1 -1
- package/index.mjs +34 -6
- package/package.json +22 -6
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -174
- package/scripts/jules-create.mjs +12 -51
- package/scripts/jules-dispatch.mjs +103 -349
- package/scripts/jules-merge-swarm.mjs +30 -0
- package/scripts/jules-nightly.mjs +7 -124
- package/scripts/jules-patch.mjs +23 -0
- package/scripts/jules-queue-runner.mjs +28 -203
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +76 -578
- package/scripts/jules-status.mjs +42 -65
- package/scripts/jules-swarm.mjs +27 -232
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -170
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +118 -252
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +209 -0
- package/src/webhook.mjs +138 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
package/src/git.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export class GateError extends Error {
|
|
5
|
+
constructor(message, opts = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "GateError";
|
|
8
|
+
this.code = opts.code || 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizePath(p) {
|
|
13
|
+
if (!p || typeof p !== "string") return "";
|
|
14
|
+
return p.split(sep).join("/").replace(/\\/g, "/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runCmd(command, opts = {}) {
|
|
18
|
+
const cwd = opts.cwd || process.cwd();
|
|
19
|
+
try {
|
|
20
|
+
const stdout = execFileSync("sh", ["-c", command], {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
return { status: 0, stdout: stdout.trim(), stderr: "" };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (opts.ignoreError) {
|
|
28
|
+
return {
|
|
29
|
+
status: err.status || 1,
|
|
30
|
+
stdout: (err.stdout || "").toString().trim(),
|
|
31
|
+
stderr: (err.stderr || "").toString().trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shell-safe git invoker using direct argument arrays (shell: false).
|
|
40
|
+
*/
|
|
41
|
+
export function git(args = [], opts = {}) {
|
|
42
|
+
const cwd = opts.cwd || process.cwd();
|
|
43
|
+
const argArray = Array.isArray(args) ? args : String(args).split(" ").filter(Boolean);
|
|
44
|
+
try {
|
|
45
|
+
const stdout = execFileSync("git", argArray, {
|
|
46
|
+
cwd,
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
shell: false,
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
});
|
|
51
|
+
return opts.raw ? stdout : stdout.trim();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (opts.ignoreError) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
const stderr = (err.stderr || err.message || "").toString().trim();
|
|
57
|
+
throw new GateError(`Git command failed [git ${argArray.join(" ")}]: ${stderr}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
62
|
+
const candidates = [`origin/${baseRef}`, baseRef, `refs/remotes/origin/${baseRef}`, "HEAD"];
|
|
63
|
+
for (const ref of candidates) {
|
|
64
|
+
try {
|
|
65
|
+
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
66
|
+
cwd: root,
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
shell: false,
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
});
|
|
71
|
+
if (res && res.trim()) {
|
|
72
|
+
return ref;
|
|
73
|
+
}
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
throw new GateError(
|
|
77
|
+
`Cannot resolve base reference "${baseRef}". Ensure base branch is fetched (e.g., fetch-depth: 0 in CI).`,
|
|
78
|
+
{ code: 1 }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function changedFiles(root = process.cwd(), base = "main") {
|
|
83
|
+
const resolvedRef = resolveBase(root, base);
|
|
84
|
+
const raw = git(["-c", "core.quotePath=false", "diff", "-z", "--name-only", `${resolvedRef}...HEAD`], {
|
|
85
|
+
cwd: root,
|
|
86
|
+
raw: true,
|
|
87
|
+
});
|
|
88
|
+
return raw
|
|
89
|
+
.split("\0")
|
|
90
|
+
.map(normalizePath)
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function diffBytes(root = process.cwd(), base = "main") {
|
|
95
|
+
const resolvedRef = resolveBase(root, base);
|
|
96
|
+
const raw = git(["diff", "--shortstat", `${resolvedRef}...HEAD`], { cwd: root });
|
|
97
|
+
if (!raw) return 0;
|
|
98
|
+
const diffStr = git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
|
|
99
|
+
return Buffer.byteLength(diffStr, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function diffText(root = process.cwd(), base = "main") {
|
|
103
|
+
const resolvedRef = resolveBase(root, base);
|
|
104
|
+
return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function showFromOrigin(root = process.cwd(), base = "main", filePath = "") {
|
|
108
|
+
const resolvedRef = resolveBase(root, base);
|
|
109
|
+
const normPath = normalizePath(filePath);
|
|
110
|
+
try {
|
|
111
|
+
return git(["show", `${resolvedRef}:${normPath}`], { cwd: root, raw: true });
|
|
112
|
+
} catch (_) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function worktreeAdd(root = process.cwd(), branch = "agent/task", targetDir = "") {
|
|
118
|
+
return git(["worktree", "add", targetDir, "-b", branch], { cwd: root });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function worktreeRemove(root = process.cwd(), targetDir = "") {
|
|
122
|
+
return git(["worktree", "remove", targetDir, "--force"], { cwd: root });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function worktreePrune(root = process.cwd()) {
|
|
126
|
+
return git(["worktree", "prune"], { cwd: root });
|
|
127
|
+
}
|
package/src/provider.mjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const JULES_PRESET = {
|
|
4
|
+
name: "jules",
|
|
5
|
+
type: "http",
|
|
6
|
+
url: "https://jules.googleapis.com/v1/projects/{project}/sessions",
|
|
7
|
+
headers: {
|
|
8
|
+
"Authorization": "Bearer {token}",
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
bodyTemplate: {
|
|
12
|
+
prompt: "{prompt}",
|
|
13
|
+
title: "{title}",
|
|
14
|
+
branch: "{branch}",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const CLAUDE_PRESET = {
|
|
19
|
+
name: "claude-code",
|
|
20
|
+
type: "exec",
|
|
21
|
+
command: "claude",
|
|
22
|
+
args: ["--print", "-p", "{prompt}"],
|
|
23
|
+
promptViaStdin: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const CODEX_PRESET = {
|
|
27
|
+
name: "codex",
|
|
28
|
+
type: "exec",
|
|
29
|
+
command: "codex",
|
|
30
|
+
args: ["exec", "{prompt}"],
|
|
31
|
+
promptViaStdin: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function interpolateString(template, data) {
|
|
35
|
+
if (typeof template !== "string") return template;
|
|
36
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function interpolateDeep(node, data) {
|
|
40
|
+
if (typeof node === "string") return interpolateString(node, data);
|
|
41
|
+
if (Array.isArray(node)) return node.map((item) => interpolateDeep(item, data));
|
|
42
|
+
if (node && typeof node === "object") {
|
|
43
|
+
const res = {};
|
|
44
|
+
for (const [k, v] of Object.entries(node)) {
|
|
45
|
+
res[k] = interpolateDeep(v, data);
|
|
46
|
+
}
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates an HTTP or Exec provider adapter based on spec/config.
|
|
54
|
+
*/
|
|
55
|
+
export function createProvider(spec = "jules", config = {}) {
|
|
56
|
+
const providerSpec = typeof spec === "string"
|
|
57
|
+
? (spec === "claude-code" ? CLAUDE_PRESET : spec === "codex" ? CODEX_PRESET : JULES_PRESET)
|
|
58
|
+
: spec;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name: providerSpec.name || "custom-provider",
|
|
62
|
+
|
|
63
|
+
async dispatch(task, ctx = {}) {
|
|
64
|
+
const rawToken = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
|
|
65
|
+
const data = {
|
|
66
|
+
prompt: task.prompt || "",
|
|
67
|
+
title: task.title || "Agent Task",
|
|
68
|
+
branch: task.branch || (config.branchPrefix ? config.branchPrefix + "task" : "agent/task"),
|
|
69
|
+
project: process.env.JULES_PROJECT_ID || "default",
|
|
70
|
+
...ctx,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (ctx.dryRun) {
|
|
74
|
+
return {
|
|
75
|
+
id: "dry-run-session-id",
|
|
76
|
+
status: "pending",
|
|
77
|
+
title: data.title,
|
|
78
|
+
provider: providerSpec.name,
|
|
79
|
+
data: {
|
|
80
|
+
...data,
|
|
81
|
+
token: rawToken ? "[REDACTED]" : undefined,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (providerSpec.type === "http") {
|
|
87
|
+
const urlData = { ...data, token: rawToken };
|
|
88
|
+
const url = interpolateString(providerSpec.url, urlData);
|
|
89
|
+
const headers = {};
|
|
90
|
+
for (const [k, v] of Object.entries(providerSpec.headers || {})) {
|
|
91
|
+
const val = interpolateString(v, urlData);
|
|
92
|
+
if (val.includes("\r") || val.includes("\n")) {
|
|
93
|
+
throw new Error(`CRITICAL: Header injection attempt detected in header "${k}"`);
|
|
94
|
+
}
|
|
95
|
+
headers[k] = val;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let body;
|
|
99
|
+
if (typeof providerSpec.bodyTemplate === "string") {
|
|
100
|
+
try {
|
|
101
|
+
const parsedObj = JSON.parse(providerSpec.bodyTemplate);
|
|
102
|
+
const interpolatedObj = interpolateDeep(parsedObj, data);
|
|
103
|
+
body = JSON.stringify(interpolatedObj);
|
|
104
|
+
} catch (_) {
|
|
105
|
+
body = interpolateString(providerSpec.bodyTemplate, data);
|
|
106
|
+
}
|
|
107
|
+
} else if (providerSpec.bodyTemplate) {
|
|
108
|
+
const interpolatedObj = interpolateDeep(providerSpec.bodyTemplate, data);
|
|
109
|
+
body = JSON.stringify(interpolatedObj);
|
|
110
|
+
} else {
|
|
111
|
+
body = JSON.stringify(data);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const res = await fetch(url, {
|
|
115
|
+
method: providerSpec.method || "POST",
|
|
116
|
+
headers,
|
|
117
|
+
body,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
const cleanText = text.slice(0, 500);
|
|
123
|
+
const sanitizedText = rawToken ? cleanText.split(rawToken).join("[REDACTED]") : cleanText;
|
|
124
|
+
throw new Error(`Provider HTTP Error (${res.status}): ${sanitizedText}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const json = await res.json();
|
|
128
|
+
return {
|
|
129
|
+
id: json.id || json.name || "http-session",
|
|
130
|
+
status: json.state || "active",
|
|
131
|
+
raw: json,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (providerSpec.type === "exec") {
|
|
136
|
+
const rawArgs = providerSpec.args || (providerSpec.command ? providerSpec.command.split(" ").slice(1) : []);
|
|
137
|
+
const command = providerSpec.command ? providerSpec.command.split(" ")[0] : "claude";
|
|
138
|
+
|
|
139
|
+
// CRITICAL H-a FIX: Filter out '{prompt}' argument if prompt is passed via stdin
|
|
140
|
+
const filteredArgs = providerSpec.promptViaStdin
|
|
141
|
+
? rawArgs.filter((arg) => !arg.includes("{prompt}"))
|
|
142
|
+
: rawArgs;
|
|
143
|
+
|
|
144
|
+
const processedArgs = filteredArgs.map((arg) => interpolateString(arg, data));
|
|
145
|
+
|
|
146
|
+
const res = spawnSync(command, processedArgs, {
|
|
147
|
+
cwd: config._root || process.cwd(),
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
shell: false, // CRITICAL: Ban shell execution to eliminate CWE-77
|
|
150
|
+
input: providerSpec.promptViaStdin ? data.prompt : undefined,
|
|
151
|
+
timeout: providerSpec.timeoutMs || 900000,
|
|
152
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (res.error) {
|
|
156
|
+
throw new Error(`Provider Exec Failed: ${res.error.message}`);
|
|
157
|
+
}
|
|
158
|
+
if (res.status !== 0) {
|
|
159
|
+
const stderr = (res.stderr || "").slice(0, 500);
|
|
160
|
+
throw new Error(`Provider Exec Exit ${res.status}: ${stderr}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: "exec-session-" + Date.now(),
|
|
165
|
+
status: "completed",
|
|
166
|
+
output: res.stdout || "",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(`Unsupported provider type: ${providerSpec.type}`);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
validate() {
|
|
174
|
+
if (providerSpec.type === "http" && !providerSpec.url) return false;
|
|
175
|
+
if (providerSpec.type === "exec" && !providerSpec.command) return false;
|
|
176
|
+
return true;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
package/src/risk.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { normalizePath } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
export const RISK_TIERS = {
|
|
4
|
+
R0: "R0_COSMETIC", // Docs, markdown, comments, safe devDep patches
|
|
5
|
+
R1: "R1_ROUTINE", // Pure utility logic, unit tests, single package layer
|
|
6
|
+
R2: "R2_CONSEQUENTIAL",// UI components, DB helpers, diff > 400 lines, bundle size impact
|
|
7
|
+
R3: "R3_RESTRICTED", // Migrations, Auth, Pricing/VAT, Protected paths (.github, configs)
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const RESTRICTED_PATH_PATTERNS = [
|
|
11
|
+
"**/drizzle/migrations/**",
|
|
12
|
+
"**/migrations/**",
|
|
13
|
+
"**/auth/**",
|
|
14
|
+
"**/pricing/**",
|
|
15
|
+
"**/vat/**",
|
|
16
|
+
"**/contracts/**",
|
|
17
|
+
"**/ledger/**",
|
|
18
|
+
".github/**",
|
|
19
|
+
".githooks/**",
|
|
20
|
+
"wrangler.jsonc",
|
|
21
|
+
"pnpm-lock.yaml",
|
|
22
|
+
"package-lock.json",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const CONSEQUENTIAL_PATH_PATTERNS = [
|
|
26
|
+
"apps/web/src/components/**",
|
|
27
|
+
"packages/db/**",
|
|
28
|
+
"src/engine.mjs",
|
|
29
|
+
"src/security.mjs",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const COSMETIC_EXTENSIONS = new Set([".md", ".txt", ".jsonl", ".svg"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Classifies a set of changed files and diff metadata into a Risk Tier (R0, R1, R2, R3).
|
|
36
|
+
*
|
|
37
|
+
* @param {string[]} files - Array of changed file paths
|
|
38
|
+
* @param {Object} [opts] - Options (diffBytes, diffLines, author)
|
|
39
|
+
* @returns {{ tier: string, reason: string, isAutoMergeAllowed: boolean, requiresHumanReview: boolean }}
|
|
40
|
+
*/
|
|
41
|
+
export function classifyRiskTier(files = [], opts = {}) {
|
|
42
|
+
const diffLines = opts.diffLines ?? 0;
|
|
43
|
+
|
|
44
|
+
if (!files || files.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
tier: RISK_TIERS.R0,
|
|
47
|
+
reason: "No files changed",
|
|
48
|
+
isAutoMergeAllowed: true,
|
|
49
|
+
requiresHumanReview: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 1. Check R3 (Restricted Paths)
|
|
54
|
+
for (const rawFile of files) {
|
|
55
|
+
const file = normalizePath(rawFile);
|
|
56
|
+
for (const pat of RESTRICTED_PATH_PATTERNS) {
|
|
57
|
+
if (file.includes(pat.replace(/\*/g, "")) || file.endsWith(pat)) {
|
|
58
|
+
return {
|
|
59
|
+
tier: RISK_TIERS.R3,
|
|
60
|
+
reason: `Matches restricted security/financial path pattern '${pat}'`,
|
|
61
|
+
isAutoMergeAllowed: false,
|
|
62
|
+
requiresHumanReview: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Check R2 (Consequential Paths or Diff Size > 400 lines)
|
|
69
|
+
if (diffLines >= 400) {
|
|
70
|
+
return {
|
|
71
|
+
tier: RISK_TIERS.R2,
|
|
72
|
+
reason: `Diff size (${diffLines} lines) exceeds R1 limit of 400 lines`,
|
|
73
|
+
isAutoMergeAllowed: false,
|
|
74
|
+
requiresHumanReview: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const rawFile of files) {
|
|
79
|
+
const file = normalizePath(rawFile);
|
|
80
|
+
for (const pat of CONSEQUENTIAL_PATH_PATTERNS) {
|
|
81
|
+
if (file.includes(pat.replace(/\*/g, ""))) {
|
|
82
|
+
return {
|
|
83
|
+
tier: RISK_TIERS.R2,
|
|
84
|
+
reason: `Matches consequential component/DB path pattern '${pat}'`,
|
|
85
|
+
isAutoMergeAllowed: false,
|
|
86
|
+
requiresHumanReview: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Check R0 (Purely Cosmetic files)
|
|
93
|
+
const isAllCosmetic = files.every((rawFile) => {
|
|
94
|
+
const file = normalizePath(rawFile);
|
|
95
|
+
if (file.startsWith(".agent/history/") || file.startsWith("docs/")) return true;
|
|
96
|
+
const dotIdx = file.lastIndexOf(".");
|
|
97
|
+
if (dotIdx !== -1) {
|
|
98
|
+
const ext = file.slice(dotIdx).toLowerCase();
|
|
99
|
+
return COSMETIC_EXTENSIONS.has(ext);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (isAllCosmetic) {
|
|
105
|
+
return {
|
|
106
|
+
tier: RISK_TIERS.R0,
|
|
107
|
+
reason: "All changed files are markdown, documentation, or cosmetic assets",
|
|
108
|
+
isAutoMergeAllowed: true,
|
|
109
|
+
requiresHumanReview: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Default R1 (Routine Logic)
|
|
114
|
+
return {
|
|
115
|
+
tier: RISK_TIERS.R1,
|
|
116
|
+
reason: "Routine isolated package logic / unit test update within limits",
|
|
117
|
+
isAutoMergeAllowed: true,
|
|
118
|
+
requiresHumanReview: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_CHARS = 10000; // Safe threshold below Antigravity 12k & Claude 25k limits
|
|
5
|
+
const DEFAULT_MAX_LINES = 250;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Audit character and line counts of compiled instruction files to prevent truncation.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} root - Project root directory
|
|
11
|
+
* @param {Object} [opts] - Options (maxChars, maxLines)
|
|
12
|
+
* @returns {{ ok: boolean, violations: Array<{ path: string, charCount: number, lineCount: number, reason: string }> }}
|
|
13
|
+
*/
|
|
14
|
+
export function checkRulesBudget(root = process.cwd(), opts = {}) {
|
|
15
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
16
|
+
const maxLines = opts.maxLines ?? DEFAULT_MAX_LINES;
|
|
17
|
+
const violations = [];
|
|
18
|
+
|
|
19
|
+
const candidateFiles = [
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
"CLAUDE.md",
|
|
22
|
+
"GEMINI.md",
|
|
23
|
+
".cursorrules",
|
|
24
|
+
"JULES_RULES_TEMPLATE.md",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Scan .agent/rules/ directory if it exists
|
|
28
|
+
const rulesDir = join(root, ".agent", "rules");
|
|
29
|
+
try {
|
|
30
|
+
const files = readdirSync(rulesDir);
|
|
31
|
+
for (const f of files) {
|
|
32
|
+
if (f.endsWith(".md")) {
|
|
33
|
+
candidateFiles.push(join(".agent", "rules", f));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (_) {}
|
|
37
|
+
|
|
38
|
+
for (const relPath of candidateFiles) {
|
|
39
|
+
const fullPath = join(root, relPath);
|
|
40
|
+
if (!existsSync(fullPath)) continue;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
44
|
+
const charCount = content.length;
|
|
45
|
+
const lineCount = content.split("\n").length;
|
|
46
|
+
|
|
47
|
+
if (charCount > maxChars) {
|
|
48
|
+
violations.push({
|
|
49
|
+
path: relPath,
|
|
50
|
+
charCount,
|
|
51
|
+
lineCount,
|
|
52
|
+
reason: `Exceeds max character budget of ${maxChars} chars (${charCount} chars). Antigravity silently truncates rules > 12,000 chars.`,
|
|
53
|
+
});
|
|
54
|
+
} else if (lineCount > maxLines) {
|
|
55
|
+
violations.push({
|
|
56
|
+
path: relPath,
|
|
57
|
+
charCount,
|
|
58
|
+
lineCount,
|
|
59
|
+
reason: `Exceeds max line budget of ${maxLines} lines (${lineCount} lines). Claude Code auto-memory truncates > 200 lines.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
ok: violations.length === 0,
|
|
67
|
+
violations,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function existsSync(p) {
|
|
72
|
+
try {
|
|
73
|
+
return statSync(p).isFile();
|
|
74
|
+
} catch (_) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|