jules-orchestrator-kit 0.9.4 → 0.21.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/Janitor.md +11 -0
- package/README.md +261 -259
- package/bin/agentctl.mjs +3 -3
- package/bin/init.js +16 -1
- package/index.mjs +11 -2
- package/package.json +2 -1
- package/scripts/jules-merge-swarm.mjs +179 -11
- package/scripts/release.mjs +103 -0
- package/src/config.mjs +37 -0
- package/src/engine.mjs +124 -3
- package/src/execution_envelope.mjs +103 -0
- package/src/git.mjs +20 -2
- package/src/mcp.mjs +120 -13
- package/src/process-group.mjs +80 -0
- package/src/risk.mjs +4 -2
- package/src/rules_budget.mjs +102 -0
- package/src/security.mjs +45 -6
- package/src/state.mjs +160 -23
- package/src/webhook.mjs +67 -18
package/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* security auditing, repo gating, and state operations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export { loadConfig, detectStack, resolveVerify, resolveRoot, normalizePath } from "./src/config.mjs";
|
|
8
|
+
export { loadConfig, detectStack, resolveVerify, resolveRoot, normalizePath, TIER_PRESETS } from "./src/config.mjs";
|
|
9
9
|
export {
|
|
10
10
|
shannonEntropy,
|
|
11
11
|
redactSecrets,
|
|
@@ -19,14 +19,23 @@ export { createProvider, JULES_PRESET, CLAUDE_PRESET, CODEX_PRESET } from "./src
|
|
|
19
19
|
export {
|
|
20
20
|
appendLedger,
|
|
21
21
|
readLedger,
|
|
22
|
+
verifyLedgerIntegrity,
|
|
23
|
+
reserveBudget,
|
|
24
|
+
commitBudgetReservation,
|
|
22
25
|
withBudget,
|
|
23
26
|
checkDailyBudget,
|
|
24
27
|
acquireLock,
|
|
25
28
|
releaseLock,
|
|
26
29
|
lockStatus,
|
|
27
30
|
} from "./src/state.mjs";
|
|
28
|
-
export { gate, dispatch, repair, run } from "./src/engine.mjs";
|
|
31
|
+
export { gate, dispatch, repair, run, fingerprintFailureState } from "./src/engine.mjs";
|
|
29
32
|
export { validateEnvelope } from "./src/envelope.mjs";
|
|
33
|
+
export {
|
|
34
|
+
createExecutionEnvelope,
|
|
35
|
+
verifyExecutionEnvelope,
|
|
36
|
+
freezeExecutionEnvelope,
|
|
37
|
+
hashExecutionEnvelope,
|
|
38
|
+
} from "./src/execution_envelope.mjs";
|
|
30
39
|
export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
|
|
31
40
|
export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
|
|
32
41
|
export { checkRulesBudget } from "./src/rules_budget.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Orchestration kit for running Google Jules autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"jules:check-asset-integrity": "node scripts/asset-integrity-check.mjs",
|
|
59
59
|
"jules:risk-tier": "node scripts/risk-tier.mjs",
|
|
60
60
|
"jules:rules-lint": "node scripts/rules-lint.mjs",
|
|
61
|
+
"release": "node scripts/release.mjs",
|
|
61
62
|
"lint": "eslint ."
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|
|
@@ -1,30 +1,198 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Swarm branch merger with 3-way structural JSON/object merge, git merge-file execution
|
|
5
|
+
* isolated in os.tmpdir(), and safety gate checking.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { existsSync, readdirSync,
|
|
8
|
+
import { writeFileSync, readFileSync, existsSync, readdirSync, rmSync, mkdtempSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { classifyRiskTier, RISK_TIERS } from "../src/risk.mjs";
|
|
13
|
+
import { changedFiles } from "../src/git.mjs";
|
|
9
14
|
|
|
15
|
+
export const EXIT = Object.freeze({
|
|
16
|
+
SUCCESS: 0,
|
|
17
|
+
R3_RESTRICTED: 3,
|
|
18
|
+
MERGE_CONFLICT: 4,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
22
|
+
|
|
23
|
+
function isPlainObject(v) {
|
|
24
|
+
return (
|
|
25
|
+
v !== null &&
|
|
26
|
+
typeof v === "object" &&
|
|
27
|
+
!Array.isArray(v) &&
|
|
28
|
+
(Object.getPrototypeOf(v) === null || Object.getPrototypeOf(v) === Object.prototype)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deepClone(v) {
|
|
33
|
+
if (v === undefined) return undefined;
|
|
34
|
+
return JSON.parse(JSON.stringify(v));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stableStringify(v) {
|
|
38
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
39
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
40
|
+
const keys = Object.keys(v).filter((k) => !BLOCKED_KEYS.has(k)).sort();
|
|
41
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(",")}}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deepEqual(a, b) {
|
|
45
|
+
return stableStringify(a) === stableStringify(b);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recursive 3-way structural object & array merge.
|
|
50
|
+
*/
|
|
51
|
+
export function deepMerge3Way(base, ours, theirs, path = "$") {
|
|
52
|
+
const conflicts = [];
|
|
53
|
+
if (deepEqual(ours, theirs)) return { merged: deepClone(ours), conflicts };
|
|
54
|
+
if (deepEqual(ours, base)) return { merged: deepClone(theirs), conflicts };
|
|
55
|
+
if (deepEqual(theirs, base)) return { merged: deepClone(ours), conflicts };
|
|
56
|
+
|
|
57
|
+
if (isPlainObject(base) && isPlainObject(ours) && isPlainObject(theirs)) {
|
|
58
|
+
const merged = {};
|
|
59
|
+
const safeBase = base || {};
|
|
60
|
+
const safeOurs = ours || {};
|
|
61
|
+
const safeTheirs = theirs || {};
|
|
62
|
+
const keys = new Set([
|
|
63
|
+
...Object.keys(safeBase),
|
|
64
|
+
...Object.keys(safeOurs),
|
|
65
|
+
...Object.keys(safeTheirs),
|
|
66
|
+
]);
|
|
67
|
+
for (const k of keys) {
|
|
68
|
+
if (BLOCKED_KEYS.has(k)) continue;
|
|
69
|
+
const childPath = `${path}.${k}`;
|
|
70
|
+
const b = safeBase[k], o = safeOurs[k], t = safeTheirs[k];
|
|
71
|
+
if (o === undefined && t === undefined) continue;
|
|
72
|
+
if (o === undefined) { merged[k] = deepClone(t); continue; }
|
|
73
|
+
if (t === undefined) { merged[k] = deepClone(o); continue; }
|
|
74
|
+
const sub = deepMerge3Way(b, o, t, childPath);
|
|
75
|
+
merged[k] = sub.merged;
|
|
76
|
+
conflicts.push(...sub.conflicts);
|
|
77
|
+
}
|
|
78
|
+
return { merged, conflicts };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(ours) && Array.isArray(theirs)) {
|
|
82
|
+
const baseArr = Array.isArray(base) ? base : [];
|
|
83
|
+
const minLen = Math.min(ours.length, theirs.length);
|
|
84
|
+
const merged = [];
|
|
85
|
+
for (let i = 0; i < minLen; i++) {
|
|
86
|
+
const sub = deepMerge3Way(baseArr[i], ours[i], theirs[i], `${path}[${i}]`);
|
|
87
|
+
merged.push(sub.merged);
|
|
88
|
+
conflicts.push(...sub.conflicts);
|
|
89
|
+
}
|
|
90
|
+
if (ours.length > minLen && theirs.length <= minLen) {
|
|
91
|
+
merged.push(...ours.slice(minLen).map(deepClone));
|
|
92
|
+
} else if (theirs.length > minLen && ours.length <= minLen) {
|
|
93
|
+
merged.push(...theirs.slice(minLen).map(deepClone));
|
|
94
|
+
} else if (ours.length > minLen && theirs.length > minLen) {
|
|
95
|
+
conflicts.push({
|
|
96
|
+
path: `${path}[tail]`,
|
|
97
|
+
base: baseArr.slice(minLen),
|
|
98
|
+
ours: ours.slice(minLen),
|
|
99
|
+
theirs: theirs.slice(minLen),
|
|
100
|
+
});
|
|
101
|
+
merged.push(...ours.slice(minLen).map(deepClone));
|
|
102
|
+
}
|
|
103
|
+
return { merged, conflicts };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
conflicts.push({ path, base: deepClone(base), ours: deepClone(ours), theirs: deepClone(theirs) });
|
|
107
|
+
return { merged: deepClone(ours), conflicts };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Executes a function inside a temporary directory in os.tmpdir() with guaranteed cleanup.
|
|
112
|
+
*/
|
|
113
|
+
export function withTempDir(prefix, fn) {
|
|
114
|
+
const dir = mkdtempSync(join(tmpdir(), prefix || "jules-merge-"));
|
|
115
|
+
try {
|
|
116
|
+
return fn(dir);
|
|
117
|
+
} finally {
|
|
118
|
+
try {
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Attempts a 3-way text file merge using git merge-file inside os.tmpdir().
|
|
126
|
+
*/
|
|
127
|
+
export function attemptCodeMergeFile(repoRoot, relPath, oursContent, baseContent, theirsContent) {
|
|
128
|
+
return withTempDir("jules-merge-", (tmp) => {
|
|
129
|
+
const baseFile = join(tmp, "base");
|
|
130
|
+
const oursFile = join(tmp, "ours");
|
|
131
|
+
const theirsFile = join(tmp, "theirs");
|
|
132
|
+
writeFileSync(baseFile, baseContent ?? "", "utf-8");
|
|
133
|
+
writeFileSync(oursFile, oursContent ?? "", "utf-8");
|
|
134
|
+
writeFileSync(theirsFile, theirsContent ?? "", "utf-8");
|
|
135
|
+
|
|
136
|
+
const res = spawnSync("git", ["merge-file", "-p", "--diff3", oursFile, baseFile, theirsFile], {
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (res.error) {
|
|
142
|
+
return { ok: false, conflicts: 0, error: res.error.message };
|
|
143
|
+
}
|
|
144
|
+
const conflictCount = res.status && res.status > 0 ? res.status : 0;
|
|
145
|
+
const merged = res.stdout ?? "";
|
|
146
|
+
return {
|
|
147
|
+
ok: conflictCount === 0,
|
|
148
|
+
merged,
|
|
149
|
+
conflicts: conflictCount,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Checks safety gate against active worker locks and risk tiers.
|
|
156
|
+
*/
|
|
10
157
|
export function checkSafetyGate(branchName = "", projectRoot = process.cwd()) {
|
|
11
158
|
const locksDir = join(projectRoot, ".agent/state/locks");
|
|
12
|
-
if (
|
|
159
|
+
if (existsSync(locksDir)) {
|
|
160
|
+
try {
|
|
161
|
+
const files = readdirSync(locksDir);
|
|
162
|
+
for (const f of files) {
|
|
163
|
+
if (!f.endsWith(".json")) continue;
|
|
164
|
+
const content = readFileSync(join(locksDir, f), "utf-8");
|
|
165
|
+
const parsed = JSON.parse(content);
|
|
166
|
+
if (parsed.branch === branchName) {
|
|
167
|
+
return { safe: false, reason: `Active lock held by worker ${parsed.agent || "unknown"}` };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check R3 risk tier if files modified
|
|
13
174
|
try {
|
|
14
|
-
const files =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (parsed.branch === branchName) {
|
|
20
|
-
return { safe: false, reason: `Active lock held by worker ${parsed.agent || "unknown"}` };
|
|
175
|
+
const files = changedFiles(projectRoot, process.env.BASE_BRANCH || "main");
|
|
176
|
+
if (files.length > 0) {
|
|
177
|
+
const tier = classifyRiskTier(files);
|
|
178
|
+
if (tier.tier === RISK_TIERS.R3) {
|
|
179
|
+
return { safe: false, reason: `R3 Restricted Path violation: ${tier.reason}` };
|
|
21
180
|
}
|
|
22
181
|
}
|
|
23
182
|
} catch (_) {}
|
|
183
|
+
|
|
24
184
|
return { safe: true };
|
|
25
185
|
}
|
|
26
186
|
|
|
27
187
|
if (process.argv[1] && process.argv[1].endsWith("jules-merge-swarm.mjs")) {
|
|
188
|
+
const branchName = process.argv[2] || "";
|
|
189
|
+
const gateResult = checkSafetyGate(branchName);
|
|
190
|
+
if (!gateResult.safe) {
|
|
191
|
+
console.error(`[merge-swarm] Safety gate check failed: ${gateResult.reason}`);
|
|
192
|
+
process.exit(EXIT.R3_RESTRICTED);
|
|
193
|
+
}
|
|
194
|
+
|
|
28
195
|
console.log("No open Jules PRs found.");
|
|
29
|
-
process.exit(
|
|
196
|
+
process.exit(EXIT.SUCCESS);
|
|
30
197
|
}
|
|
198
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automated Release Orchestrator script for jules-orchestrator-kit.
|
|
5
|
+
* Verifies test suite, updates git tags, and creates GitHub Release via gh CLI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { resolveRoot } from "../src/config.mjs";
|
|
12
|
+
|
|
13
|
+
const root = resolveRoot();
|
|
14
|
+
const pkgPath = join(root, "package.json");
|
|
15
|
+
const changelogPath = join(root, "CHANGELOG.md");
|
|
16
|
+
|
|
17
|
+
if (!existsSync(pkgPath)) {
|
|
18
|
+
console.error("❌ Error: package.json not found.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
23
|
+
const version = pkg.version;
|
|
24
|
+
const tagName = `v${version}`;
|
|
25
|
+
|
|
26
|
+
console.log(`🚀 Automated Release Pipeline for ${pkg.name} (${tagName})`);
|
|
27
|
+
console.log("-------------------------------------------------------");
|
|
28
|
+
|
|
29
|
+
// 1. Verify test suite
|
|
30
|
+
console.log("1. Running unit test verification suite...");
|
|
31
|
+
try {
|
|
32
|
+
execSync("npm test", { cwd: root, stdio: "inherit" });
|
|
33
|
+
console.log(" ✅ Test suite passed cleanly.\n");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("❌ Release Aborted: Test suite failed.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Extract release notes from CHANGELOG.md
|
|
40
|
+
console.log(`2. Extracting release notes for ${tagName} from CHANGELOG.md...`);
|
|
41
|
+
let notes = "";
|
|
42
|
+
if (existsSync(changelogPath)) {
|
|
43
|
+
const changelog = readFileSync(changelogPath, "utf-8");
|
|
44
|
+
const regex = new RegExp(`## \\[${version}\\][^\\n]*\\n([\\s\\S]*?)(?=\\n## \\[|$)`, "i");
|
|
45
|
+
const match = changelog.match(regex);
|
|
46
|
+
if (match && match[1]) {
|
|
47
|
+
notes = match[1].trim();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!notes) {
|
|
52
|
+
notes = `Release ${tagName}`;
|
|
53
|
+
console.log(" ⚠️ Warning: Release entry not found in CHANGELOG.md, using default title.");
|
|
54
|
+
} else {
|
|
55
|
+
console.log(" ✅ Extracted release notes from CHANGELOG.md.\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Create Git Tag if not exists
|
|
59
|
+
console.log(`3. Checking Git tag ${tagName}...`);
|
|
60
|
+
try {
|
|
61
|
+
const tags = execSync("git tag -l", { cwd: root, encoding: "utf-8" });
|
|
62
|
+
if (tags.split("\n").includes(tagName)) {
|
|
63
|
+
console.log(` ℹ️ Git tag ${tagName} already exists.`);
|
|
64
|
+
} else {
|
|
65
|
+
execSync(`git tag -a ${tagName} -m "${pkg.name} ${tagName}"`, { cwd: root, stdio: "inherit" });
|
|
66
|
+
console.log(` ✅ Created Git tag ${tagName}.`);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`❌ Tagging failed: ${err.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Push git commits and tag to origin
|
|
74
|
+
console.log(`4. Pushing main and tag ${tagName} to origin...`);
|
|
75
|
+
try {
|
|
76
|
+
execSync("git push origin main", { cwd: root, stdio: "inherit" });
|
|
77
|
+
execSync(`git push origin ${tagName}`, { cwd: root, stdio: "inherit" });
|
|
78
|
+
console.log(" ✅ Pushed commits and tag to origin.\n");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`❌ Push failed: ${err.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 5. Create GitHub Release via gh CLI
|
|
85
|
+
console.log(`5. Creating GitHub Release via gh CLI...`);
|
|
86
|
+
try {
|
|
87
|
+
const notesFile = join(root, ".agent", "temp_release_notes.txt");
|
|
88
|
+
const fs = await import("node:fs");
|
|
89
|
+
fs.writeFileSync(notesFile, notes, "utf-8");
|
|
90
|
+
|
|
91
|
+
execSync(`gh release create ${tagName} --title "${tagName}" --notes-file "${notesFile}"`, {
|
|
92
|
+
cwd: root,
|
|
93
|
+
stdio: "inherit",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (fs.existsSync(notesFile)) fs.unlinkSync(notesFile);
|
|
97
|
+
console.log(` 🎉 GitHub Release ${tagName} created successfully!`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.log(` ℹ️ Note: gh release creation finished or already exists.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("-------------------------------------------------------");
|
|
103
|
+
console.log(`✅ Release ${tagName} Completed Successfully!\n`);
|
package/src/config.mjs
CHANGED
|
@@ -278,10 +278,37 @@ export function resolveVerify(root = process.cwd()) {
|
|
|
278
278
|
return { test: s.testCmd || "", build: s.buildCmd || "" };
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
export const TIER_PRESETS = {
|
|
282
|
+
free: {
|
|
283
|
+
dailyTasks: 15,
|
|
284
|
+
repairAttempts: 1,
|
|
285
|
+
concurrency: 1,
|
|
286
|
+
staggerMs: 3000,
|
|
287
|
+
diffKb: 50,
|
|
288
|
+
},
|
|
289
|
+
pro: {
|
|
290
|
+
dailyTasks: 100,
|
|
291
|
+
repairAttempts: 2,
|
|
292
|
+
concurrency: 2,
|
|
293
|
+
staggerMs: 1500,
|
|
294
|
+
diffKb: 75,
|
|
295
|
+
},
|
|
296
|
+
ultra: {
|
|
297
|
+
dailyTasks: 300,
|
|
298
|
+
repairAttempts: 3,
|
|
299
|
+
concurrency: 3,
|
|
300
|
+
staggerMs: 1000,
|
|
301
|
+
diffKb: 75,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
281
305
|
/**
|
|
282
306
|
* Loads and validates configuration from .agent/config.yml or .agent/jules.yml.
|
|
283
307
|
*/
|
|
284
308
|
export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
309
|
+
if (root === null || root === undefined) {
|
|
310
|
+
root = resolveRoot();
|
|
311
|
+
}
|
|
285
312
|
const candidates = explicitPath
|
|
286
313
|
? [explicitPath]
|
|
287
314
|
: [join(root, ".agent/config.yml"), join(root, ".agent/jules.yml")];
|
|
@@ -301,9 +328,16 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
301
328
|
const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
|
|
302
329
|
const autoVerify = resolveVerify(root);
|
|
303
330
|
|
|
331
|
+
const activeTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
|
|
332
|
+
const tierLimits = TIER_PRESETS[activeTier] || TIER_PRESETS.ultra;
|
|
333
|
+
|
|
334
|
+
const envDailyTasks = process.env.JULES_DAILY_BUDGET !== undefined ? Number(process.env.JULES_DAILY_BUDGET) : null;
|
|
335
|
+
const envDiffKb = process.env.JULES_MAX_DIFF_KB !== undefined ? Number(process.env.JULES_MAX_DIFF_KB) : null;
|
|
336
|
+
|
|
304
337
|
const config = {
|
|
305
338
|
version: parsed.version || DEFAULTS.version,
|
|
306
339
|
provider: parsed.provider || DEFAULTS.provider,
|
|
340
|
+
tier: activeTier,
|
|
307
341
|
verify: {
|
|
308
342
|
test: testCmd || autoVerify.test,
|
|
309
343
|
build: buildCmd || autoVerify.build,
|
|
@@ -311,7 +345,10 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
311
345
|
scope: normalizeScope(parsed),
|
|
312
346
|
limits: {
|
|
313
347
|
...DEFAULTS.limits,
|
|
348
|
+
...tierLimits,
|
|
314
349
|
...(parsed.limits || {}),
|
|
350
|
+
...(envDailyTasks !== null && !isNaN(envDailyTasks) ? { dailyTasks: envDailyTasks } : {}),
|
|
351
|
+
...(envDiffKb !== null && !isNaN(envDiffKb) ? { diffKb: envDiffKb } : {}),
|
|
315
352
|
},
|
|
316
353
|
isolation: parsed.isolation || DEFAULTS.isolation,
|
|
317
354
|
runner: parsed.runner || DEFAULTS.runner,
|
package/src/engine.mjs
CHANGED
|
@@ -5,6 +5,38 @@ import { createProvider } from "./provider.mjs";
|
|
|
5
5
|
import { withBudget, appendLedger, getQueueDir, ensureDir } from "./state.mjs";
|
|
6
6
|
import { readdirSync, readFileSync, renameSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Computes a SHA-256 fingerprint for a failure state (normalized stderr + diff text).
|
|
12
|
+
*/
|
|
13
|
+
export function fingerprintFailureState(failure = {}, root = process.cwd()) {
|
|
14
|
+
const rawStderr = failure.stderr || failure.stdout || failure.message || "Unknown Error";
|
|
15
|
+
const normalizedStderr = String(rawStderr)
|
|
16
|
+
.replace(/[\u001b\x1b]\[[0-9;]*[a-zA-Z]/g, "")
|
|
17
|
+
.replace(/\?[^\s"'\)\\]+/g, "<?>")
|
|
18
|
+
.replace(/:\d+:\d+/g, ":?:?")
|
|
19
|
+
.replace(/:\d+/g, ":?")
|
|
20
|
+
.replace(/\bline \d+/gi, "line ?")
|
|
21
|
+
.replace(/\bcol(umn)? \d+/gi, "col ?")
|
|
22
|
+
.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z?\b/g, "<timestamp>")
|
|
23
|
+
.replace(/\b[0-9a-f]{7,40}\b/g, "<sha>")
|
|
24
|
+
.replace(/\/[\w\/-]+\//g, "/?/")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
|
|
28
|
+
let diffStat = "0:0";
|
|
29
|
+
try {
|
|
30
|
+
const rawDiff = diffText(root) || "";
|
|
31
|
+
const lines = rawDiff.split("\n");
|
|
32
|
+
const added = lines.filter((l) => l.startsWith("+")).length;
|
|
33
|
+
const removed = lines.filter((l) => l.startsWith("-")).length;
|
|
34
|
+
diffStat = `${added}:${removed}`;
|
|
35
|
+
} catch (_) {}
|
|
36
|
+
|
|
37
|
+
const combined = `${normalizedStderr}::${diffStat}`;
|
|
38
|
+
return createHash("sha256").update(combined).digest("hex").substring(0, 16);
|
|
39
|
+
}
|
|
8
40
|
|
|
9
41
|
/**
|
|
10
42
|
* Gatekeeper verification engine (Phase 1: Scope, Phase 2: Payload, Phase 3: Secrets, Phase 4: Verify Commands).
|
|
@@ -106,14 +138,77 @@ export async function gate(opts = {}) {
|
|
|
106
138
|
return { ok: verifyOk, code: verifyOk ? 0 : 4, phases };
|
|
107
139
|
}
|
|
108
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Sliding-window ring-buffer circuit breaker for non-convergent OODA loops.
|
|
143
|
+
*/
|
|
144
|
+
export class OODACircuitBreaker {
|
|
145
|
+
constructor({ windowSize = 6, threshold = 2, cooldownMs = 60000 } = {}) {
|
|
146
|
+
this.windowSize = windowSize;
|
|
147
|
+
this.threshold = threshold;
|
|
148
|
+
this.cooldownMs = cooldownMs;
|
|
149
|
+
this.ring = [];
|
|
150
|
+
this.openedAt = 0;
|
|
151
|
+
this.openedFingerprint = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
observe(fingerprint) {
|
|
155
|
+
if (!fingerprint || typeof fingerprint !== "string") {
|
|
156
|
+
return { tripped: false };
|
|
157
|
+
}
|
|
158
|
+
if (this.isOpen()) {
|
|
159
|
+
return { tripped: true, reason: "OODA_CIRCUIT_OPEN", fingerprint: this.openedFingerprint };
|
|
160
|
+
}
|
|
161
|
+
const occurrences = this.ring.filter((f) => f === fingerprint).length + 1;
|
|
162
|
+
this.ring.push(fingerprint);
|
|
163
|
+
if (this.ring.length > this.windowSize) this.ring.shift();
|
|
164
|
+
|
|
165
|
+
if (occurrences >= this.threshold) {
|
|
166
|
+
this.openedAt = Date.now();
|
|
167
|
+
this.openedFingerprint = fingerprint;
|
|
168
|
+
return {
|
|
169
|
+
tripped: true,
|
|
170
|
+
reason: "OODA_THRASH_DETECTED",
|
|
171
|
+
fingerprint,
|
|
172
|
+
occurrences,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { tripped: false, occurrences };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
isOpen() {
|
|
179
|
+
if (this.openedAt === 0) return false;
|
|
180
|
+
if (Date.now() - this.openedAt >= this.cooldownMs) {
|
|
181
|
+
this.openedAt = 0;
|
|
182
|
+
this.openedFingerprint = null;
|
|
183
|
+
this.ring = [];
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
reset() {
|
|
190
|
+
this.ring = [];
|
|
191
|
+
this.openedAt = 0;
|
|
192
|
+
this.openedFingerprint = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
109
196
|
export async function repair(failure, opts = {}) {
|
|
110
197
|
const root = opts.root || process.cwd();
|
|
111
198
|
const config = opts.config || loadConfig(root);
|
|
112
199
|
const maxRetries = config.limits.repairAttempts || 3;
|
|
113
|
-
const provider = createProvider(config.provider, config);
|
|
200
|
+
const provider = opts.provider || createProvider(config.provider, config);
|
|
114
201
|
const attempts = [];
|
|
202
|
+
const breaker = opts.circuitBreaker || new OODACircuitBreaker({
|
|
203
|
+
windowSize: Math.max(maxRetries + 1, 6),
|
|
204
|
+
threshold: 2,
|
|
205
|
+
cooldownMs: 60000,
|
|
206
|
+
});
|
|
115
207
|
|
|
116
208
|
let currentFailure = failure;
|
|
209
|
+
const initialFingerprint = fingerprintFailureState(currentFailure, root);
|
|
210
|
+
breaker.observe(initialFingerprint);
|
|
211
|
+
|
|
117
212
|
for (let n = 1; n <= maxRetries; n++) {
|
|
118
213
|
const repairPrompt = buildRepairPrompt(currentFailure, n, config);
|
|
119
214
|
let session;
|
|
@@ -137,9 +232,23 @@ export async function repair(failure, opts = {}) {
|
|
|
137
232
|
// Re-verify after repair attempt
|
|
138
233
|
const gateRes = await gate({ root, config, fix: false });
|
|
139
234
|
if (gateRes.ok) {
|
|
235
|
+
breaker.reset();
|
|
140
236
|
return { ok: true, attempts, finalStatus: "PASSED" };
|
|
141
237
|
}
|
|
238
|
+
|
|
142
239
|
currentFailure = gateRes.phases.find((p) => p.phase === "verify")?.testResult || failure;
|
|
240
|
+
const currentFingerprint = fingerprintFailureState(currentFailure, root);
|
|
241
|
+
|
|
242
|
+
const check = breaker.observe(currentFingerprint);
|
|
243
|
+
if (check.tripped) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
attempts,
|
|
247
|
+
finalStatus: "DETERMINISTIC_REGRESSION",
|
|
248
|
+
reason: `Identical failure state fingerprint (${currentFingerprint}) observed during attempt #${n}`,
|
|
249
|
+
fingerprint: currentFingerprint,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
143
252
|
}
|
|
144
253
|
|
|
145
254
|
return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
|
|
@@ -147,11 +256,23 @@ export async function repair(failure, opts = {}) {
|
|
|
147
256
|
|
|
148
257
|
function buildRepairPrompt(failure, attempt, _config) {
|
|
149
258
|
const cleanStderr = redactSecrets(failure.stderr || failure.stdout || "Unknown Error");
|
|
150
|
-
|
|
259
|
+
|
|
260
|
+
let escalationStrategy = "DIRECT_REPAIR";
|
|
261
|
+
let escalationDirective = "1. Ground your fix strictly in the error log below. Do NOT guess file contents or function signatures.";
|
|
262
|
+
|
|
263
|
+
if (attempt === 2) {
|
|
264
|
+
escalationStrategy = "DIAGNOSTIC_ANALYSIS";
|
|
265
|
+
escalationDirective = "1. DIAGNOSE FIRST: Two attempts have failed. Analyze if the root cause is in a different file or component than where the previous patch was applied.";
|
|
266
|
+
} else if (attempt >= 3) {
|
|
267
|
+
escalationStrategy = "MINIMAL_SIMPLIFICATION";
|
|
268
|
+
escalationDirective = "1. SIMPLIFY: Multiple repair attempts have failed. Revert complex refactors and implement the minimal patch that satisfies the failing assertion.";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `Auto-Repair Attempt #${attempt} [Strategy: ${escalationStrategy}]
|
|
151
272
|
Command Failed: ${failure.command || "verify"}
|
|
152
273
|
|
|
153
274
|
REPAIR DIRECTIVE & STRICT INVARIANTS:
|
|
154
|
-
|
|
275
|
+
${escalationDirective}
|
|
155
276
|
2. NO WEAKENING: You are STRICTLY FORBIDDEN from deleting tests, commenting out assertions, or weakening expectations to achieve a passing test. Leave unmet requirements RED if the requirement is valid and feature code is missing.
|
|
156
277
|
3. CARRY EVIDENCE: Address the root cause directly and verify the exact fix.
|
|
157
278
|
4. RE-VERIFY: Execute full verification after patching to ensure zero new regressions.
|