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/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.9.4",
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
- * Backward compatibility shim for jules-merge-swarm.mjs in v0.9.0.
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, readFileSync } from "node:fs";
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 (!existsSync(locksDir)) return { safe: true };
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 = readdirSync(locksDir);
15
- for (const f of files) {
16
- if (!f.endsWith(".json")) continue;
17
- const content = readFileSync(join(locksDir, f), "utf-8");
18
- const parsed = JSON.parse(content);
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(0);
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
- return `Auto-Repair Attempt #${attempt}
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
- 1. Ground your fix strictly in the error log below. Do NOT guess file contents or function signatures.
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.