jules-orchestrator-kit 0.9.3 → 0.20.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.
@@ -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
+
package/src/config.mjs CHANGED
@@ -278,6 +278,30 @@ 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
  */
@@ -301,9 +325,16 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
301
325
  const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
302
326
  const autoVerify = resolveVerify(root);
303
327
 
328
+ const activeTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
329
+ const tierLimits = TIER_PRESETS[activeTier] || TIER_PRESETS.ultra;
330
+
331
+ const envDailyTasks = process.env.JULES_DAILY_BUDGET ? Number(process.env.JULES_DAILY_BUDGET) : null;
332
+ const envDiffKb = process.env.JULES_MAX_DIFF_KB ? Number(process.env.JULES_MAX_DIFF_KB) : null;
333
+
304
334
  const config = {
305
335
  version: parsed.version || DEFAULTS.version,
306
336
  provider: parsed.provider || DEFAULTS.provider,
337
+ tier: activeTier,
307
338
  verify: {
308
339
  test: testCmd || autoVerify.test,
309
340
  build: buildCmd || autoVerify.build,
@@ -311,7 +342,10 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
311
342
  scope: normalizeScope(parsed),
312
343
  limits: {
313
344
  ...DEFAULTS.limits,
345
+ ...tierLimits,
314
346
  ...(parsed.limits || {}),
347
+ ...(envDailyTasks && !isNaN(envDailyTasks) ? { dailyTasks: envDailyTasks } : {}),
348
+ ...(envDiffKb && !isNaN(envDiffKb) ? { diffKb: envDiffKb } : {}),
315
349
  },
316
350
  isolation: parsed.isolation || DEFAULTS.isolation,
317
351
  runner: parsed.runner || DEFAULTS.runner,
package/src/engine.mjs CHANGED
@@ -5,6 +5,33 @@ 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(/:\d+:\d+/g, ":?:?")
17
+ .replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z?\b/g, "<timestamp>")
18
+ .replace(/\b[0-9a-f]{7,40}\b/g, "<sha>")
19
+ .replace(/\/[\w\/-]+\//g, "/?/")
20
+ .replace(/\s+/g, " ")
21
+ .trim();
22
+
23
+ let diffStat = "0:0";
24
+ try {
25
+ const rawDiff = diffText(root) || "";
26
+ const lines = rawDiff.split("\n");
27
+ const added = lines.filter((l) => l.startsWith("+")).length;
28
+ const removed = lines.filter((l) => l.startsWith("-")).length;
29
+ diffStat = `${added}:${removed}`;
30
+ } catch (_) {}
31
+
32
+ const combined = `${normalizedStderr}::${diffStat}`;
33
+ return createHash("sha256").update(combined).digest("hex").substring(0, 16);
34
+ }
8
35
 
9
36
  /**
10
37
  * Gatekeeper verification engine (Phase 1: Scope, Phase 2: Payload, Phase 3: Secrets, Phase 4: Verify Commands).
@@ -106,14 +133,77 @@ export async function gate(opts = {}) {
106
133
  return { ok: verifyOk, code: verifyOk ? 0 : 4, phases };
107
134
  }
108
135
 
136
+ /**
137
+ * Sliding-window ring-buffer circuit breaker for non-convergent OODA loops.
138
+ */
139
+ export class OODACircuitBreaker {
140
+ constructor({ windowSize = 6, threshold = 2, cooldownMs = 60000 } = {}) {
141
+ this.windowSize = windowSize;
142
+ this.threshold = threshold;
143
+ this.cooldownMs = cooldownMs;
144
+ this.ring = [];
145
+ this.openedAt = 0;
146
+ this.openedFingerprint = null;
147
+ }
148
+
149
+ observe(fingerprint) {
150
+ if (!fingerprint || typeof fingerprint !== "string") {
151
+ return { tripped: false };
152
+ }
153
+ if (this.isOpen()) {
154
+ return { tripped: true, reason: "OODA_CIRCUIT_OPEN", fingerprint: this.openedFingerprint };
155
+ }
156
+ const occurrences = this.ring.filter((f) => f === fingerprint).length + 1;
157
+ this.ring.push(fingerprint);
158
+ if (this.ring.length > this.windowSize) this.ring.shift();
159
+
160
+ if (occurrences >= this.threshold) {
161
+ this.openedAt = Date.now();
162
+ this.openedFingerprint = fingerprint;
163
+ return {
164
+ tripped: true,
165
+ reason: "OODA_THRASH_DETECTED",
166
+ fingerprint,
167
+ occurrences,
168
+ };
169
+ }
170
+ return { tripped: false, occurrences };
171
+ }
172
+
173
+ isOpen() {
174
+ if (this.openedAt === 0) return false;
175
+ if (Date.now() - this.openedAt >= this.cooldownMs) {
176
+ this.openedAt = 0;
177
+ this.openedFingerprint = null;
178
+ this.ring = [];
179
+ return false;
180
+ }
181
+ return true;
182
+ }
183
+
184
+ reset() {
185
+ this.ring = [];
186
+ this.openedAt = 0;
187
+ this.openedFingerprint = null;
188
+ }
189
+ }
190
+
109
191
  export async function repair(failure, opts = {}) {
110
192
  const root = opts.root || process.cwd();
111
193
  const config = opts.config || loadConfig(root);
112
194
  const maxRetries = config.limits.repairAttempts || 3;
113
- const provider = createProvider(config.provider, config);
195
+ const provider = opts.provider || createProvider(config.provider, config);
114
196
  const attempts = [];
197
+ const breaker = opts.circuitBreaker || new OODACircuitBreaker({
198
+ windowSize: Math.max(maxRetries + 1, 6),
199
+ threshold: 2,
200
+ cooldownMs: 60000,
201
+ });
115
202
 
116
203
  let currentFailure = failure;
204
+ const initialFingerprint = fingerprintFailureState(currentFailure, root);
205
+ breaker.observe(initialFingerprint);
206
+
117
207
  for (let n = 1; n <= maxRetries; n++) {
118
208
  const repairPrompt = buildRepairPrompt(currentFailure, n, config);
119
209
  let session;
@@ -137,9 +227,23 @@ export async function repair(failure, opts = {}) {
137
227
  // Re-verify after repair attempt
138
228
  const gateRes = await gate({ root, config, fix: false });
139
229
  if (gateRes.ok) {
230
+ breaker.reset();
140
231
  return { ok: true, attempts, finalStatus: "PASSED" };
141
232
  }
233
+
142
234
  currentFailure = gateRes.phases.find((p) => p.phase === "verify")?.testResult || failure;
235
+ const currentFingerprint = fingerprintFailureState(currentFailure, root);
236
+
237
+ const check = breaker.observe(currentFingerprint);
238
+ if (check.tripped) {
239
+ return {
240
+ ok: false,
241
+ attempts,
242
+ finalStatus: "DETERMINISTIC_REGRESSION",
243
+ reason: `Identical failure state fingerprint (${currentFingerprint}) observed during attempt #${n}`,
244
+ fingerprint: currentFingerprint,
245
+ };
246
+ }
143
247
  }
144
248
 
145
249
  return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
@@ -147,11 +251,23 @@ export async function repair(failure, opts = {}) {
147
251
 
148
252
  function buildRepairPrompt(failure, attempt, _config) {
149
253
  const cleanStderr = redactSecrets(failure.stderr || failure.stdout || "Unknown Error");
150
- return `Auto-Repair Attempt #${attempt}
254
+
255
+ let escalationStrategy = "DIRECT_REPAIR";
256
+ let escalationDirective = "1. Ground your fix strictly in the error log below. Do NOT guess file contents or function signatures.";
257
+
258
+ if (attempt === 2) {
259
+ escalationStrategy = "DIAGNOSTIC_ANALYSIS";
260
+ 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.";
261
+ } else if (attempt >= 3) {
262
+ escalationStrategy = "MINIMAL_SIMPLIFICATION";
263
+ escalationDirective = "1. SIMPLIFY: Multiple repair attempts have failed. Revert complex refactors and implement the minimal patch that satisfies the failing assertion.";
264
+ }
265
+
266
+ return `Auto-Repair Attempt #${attempt} [Strategy: ${escalationStrategy}]
151
267
  Command Failed: ${failure.command || "verify"}
152
268
 
153
269
  REPAIR DIRECTIVE & STRICT INVARIANTS:
154
- 1. Ground your fix strictly in the error log below. Do NOT guess file contents or function signatures.
270
+ ${escalationDirective}
155
271
  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
272
  3. CARRY EVIDENCE: Address the root cause directly and verify the exact fix.
157
273
  4. RE-VERIFY: Execute full verification after patching to ensure zero new regressions.
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolveRoot } from "./config.mjs";
3
+ import { resolveBase } from "./git.mjs";
4
+ import { classifyRiskTier } from "./risk.mjs";
5
+
6
+ /**
7
+ * Generates a SHA-256 hash of the execution envelope payload.
8
+ */
9
+ export function hashExecutionEnvelope(envelope) {
10
+ if (!envelope || typeof envelope !== "object") return "";
11
+ const rawPayload = {
12
+ id: envelope.id,
13
+ taskId: envelope.taskId,
14
+ baseRef: envelope.baseRef,
15
+ baseSha: envelope.baseSha,
16
+ configSha: envelope.configSha,
17
+ scope: envelope.scope,
18
+ verify: envelope.verify,
19
+ riskTier: envelope.riskTier,
20
+ budgetReservationId: envelope.budgetReservationId,
21
+ capabilities: envelope.capabilities,
22
+ createdAt: envelope.createdAt,
23
+ };
24
+ const sortedKeys = Object.keys(rawPayload).sort();
25
+ const canonicalPayload = {};
26
+ for (const k of sortedKeys) {
27
+ if (rawPayload[k] !== undefined) {
28
+ canonicalPayload[k] = rawPayload[k];
29
+ }
30
+ }
31
+ return createHash("sha256").update(JSON.stringify(canonicalPayload)).digest("hex");
32
+ }
33
+
34
+ /**
35
+ * Deep freezes an execution envelope to prevent modifications during OODA repair loops.
36
+ */
37
+ export function freezeExecutionEnvelope(envelope) {
38
+ if (!envelope || typeof envelope !== "object") return envelope;
39
+ return Object.freeze({
40
+ ...envelope,
41
+ scope: Object.freeze({
42
+ deny: Object.freeze([...(envelope.scope?.deny || [])]),
43
+ allow: Object.freeze([...(envelope.scope?.allow || [])]),
44
+ protect: Object.freeze([...(envelope.scope?.protect || [])]),
45
+ }),
46
+ verify: Object.freeze({ ...(envelope.verify || {}) }),
47
+ capabilities: Object.freeze([...(envelope.capabilities || [])]),
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Creates an immutable Capability-Bounded Execution Envelope (CBEE).
53
+ */
54
+ export function createExecutionEnvelope(task = {}, opts = {}) {
55
+ const root = opts.root || resolveRoot();
56
+ const baseRef = opts.base || task.base || "main";
57
+ const baseSha = resolveBase(root, baseRef);
58
+
59
+ const taskId = task.id || opts.taskId || `task-${Date.now()}`;
60
+ const envelopeId = `env-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
61
+
62
+ const denyPaths = opts.forbiddenPaths || task.forbiddenPaths || [".github/**", "package-lock.json", "pnpm-lock.yaml"];
63
+ const allowPaths = opts.allowPaths || task.allowPaths || [];
64
+ const protectPaths = opts.protectPaths || task.protectPaths || ["package.json", "tsconfig.json", "AGENTS.md"];
65
+
66
+ const files = task.files || opts.files || [];
67
+ const riskMeta = classifyRiskTier(files, { diffLines: opts.diffLines || 0 });
68
+
69
+ const rawEnvelope = {
70
+ id: envelopeId,
71
+ taskId,
72
+ baseRef,
73
+ baseSha,
74
+ configSha: createHash("sha256").update(JSON.stringify({ denyPaths, allowPaths, protectPaths })).digest("hex"),
75
+ scope: {
76
+ deny: denyPaths,
77
+ allow: allowPaths,
78
+ protect: protectPaths,
79
+ },
80
+ verify: {
81
+ testCmd: opts.testCmd || task.testCmd || "npm test",
82
+ buildCmd: opts.buildCmd || task.buildCmd || "npm run build",
83
+ },
84
+ riskTier: riskMeta.tier,
85
+ budgetReservationId: opts.reservationId || null,
86
+ capabilities: opts.capabilities || ["git:read", "git:diff", "test:run"],
87
+ createdAt: new Date().toISOString(),
88
+ };
89
+
90
+ const envelopeHash = hashExecutionEnvelope(rawEnvelope);
91
+ const fullEnvelope = { ...rawEnvelope, hash: envelopeHash };
92
+
93
+ return freezeExecutionEnvelope(fullEnvelope);
94
+ }
95
+
96
+ /**
97
+ * Validates that an envelope has not been tampered with.
98
+ */
99
+ export function verifyExecutionEnvelope(envelope) {
100
+ if (!envelope || !envelope.hash) return false;
101
+ const expectedHash = hashExecutionEnvelope(envelope);
102
+ return envelope.hash === expectedHash;
103
+ }
package/src/git.mjs CHANGED
@@ -16,10 +16,28 @@ function normalizePath(p) {
16
16
 
17
17
  export function runCmd(command, opts = {}) {
18
18
  const cwd = opts.cwd || process.cwd();
19
+ let binary = "";
20
+ let args = [];
21
+
22
+ if (Array.isArray(command)) {
23
+ binary = command[0];
24
+ args = command.slice(1);
25
+ } else if (typeof command === "string") {
26
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
27
+ binary = tokens[0] || "";
28
+ args = tokens.slice(1);
29
+ }
30
+
31
+ if (!binary) {
32
+ if (opts.ignoreError) return { status: 0, stdout: "", stderr: "" };
33
+ throw new GateError("Empty command provided");
34
+ }
35
+
19
36
  try {
20
- const stdout = execFileSync("sh", ["-c", command], {
37
+ const stdout = execFileSync(binary, args, {
21
38
  cwd,
22
39
  encoding: "utf-8",
40
+ shell: false,
23
41
  stdio: ["ignore", "pipe", "pipe"],
24
42
  });
25
43
  return { status: 0, stdout: stdout.trim(), stderr: "" };
@@ -59,7 +77,7 @@ export function git(args = [], opts = {}) {
59
77
  }
60
78
 
61
79
  export function resolveBase(root = process.cwd(), baseRef = "main") {
62
- const candidates = [`origin/${baseRef}`, baseRef, `refs/remotes/origin/${baseRef}`, "HEAD"];
80
+ const candidates = [`origin/${baseRef}`, `refs/remotes/origin/${baseRef}`, baseRef];
63
81
  for (const ref of candidates) {
64
82
  try {
65
83
  const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {