jules-orchestrator-kit 0.23.0 → 1.0.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/scripts/utils.mjs CHANGED
@@ -159,3 +159,68 @@ export function acquireBudgetLock(_root) {
159
159
  }
160
160
 
161
161
  export function releaseBudgetLock(_root) {}
162
+
163
+ export function extractPrUrls(outputs = []) {
164
+ const prs = [];
165
+ const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
166
+ for (const item of outputs) {
167
+ const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
168
+ const matches = text.match(regex);
169
+ if (matches) {
170
+ for (const m of matches) {
171
+ if (!prs.includes(m)) prs.push(m);
172
+ }
173
+ }
174
+ }
175
+ return prs;
176
+ }
177
+
178
+ export function auditSessions(sessions = [], opts = {}) {
179
+ const staleHoursThreshold = opts.staleHoursThreshold || 24;
180
+ const now = Date.now();
181
+ const merged = [];
182
+ const active = [];
183
+ const stale = [];
184
+
185
+ for (const s of sessions) {
186
+ if (s.state === "MERGED") {
187
+ merged.push(s);
188
+ } else {
189
+ const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
190
+ const ageHours = (now - updatedMs) / (1000 * 60 * 60);
191
+ if (ageHours > staleHoursThreshold) {
192
+ stale.push(s);
193
+ } else {
194
+ active.push(s);
195
+ }
196
+ }
197
+ }
198
+
199
+ return { merged, active, stale };
200
+ }
201
+
202
+ export function buildSyncManifest(tasks = []) {
203
+ const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
204
+ return {
205
+ version: 1,
206
+ totalTasks: tasks.length,
207
+ reservations,
208
+ };
209
+ }
210
+
211
+ export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
212
+ const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
213
+ const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
214
+
215
+ const agentDir = join(projectRoot, ".agent");
216
+ if (!existsSync(agentDir)) {
217
+ try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
218
+ }
219
+ const manifestPath = join(agentDir, "sync-manifest.json");
220
+ try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
221
+
222
+ if (isDry) return { status: "DRY_RUN", path: manifestPath };
223
+ if (isRemote) return { status: "PUSHED", path: manifestPath };
224
+ return { status: "SAVED_LOCAL", path: manifestPath };
225
+ }
226
+
package/src/engine.mjs CHANGED
@@ -354,6 +354,16 @@ export async function repair(failure, opts = {}) {
354
354
  attempts.push({ n, session, ok: true });
355
355
  appendTelemetry(root, "ooda_repair_attempt", { attempt: n, ok: true });
356
356
 
357
+ // Poll async provider for terminal session state before executing re-verification gates
358
+ if (provider && session) {
359
+ await pollSessionState(provider, session, {
360
+ root,
361
+ dryRun: opts.dryRun,
362
+ pollIntervalMs: opts.pollIntervalMs,
363
+ maxPollAttempts: opts.maxPollAttempts,
364
+ });
365
+ }
366
+
357
367
  // Re-verify after repair attempt
358
368
  const gateRes = await gate({ root, config, fix: false, progressBus, progressToken });
359
369
  if (gateRes.ok) {
@@ -379,6 +389,50 @@ export async function repair(failure, opts = {}) {
379
389
  return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
380
390
  }
381
391
 
392
+ /**
393
+ * Polls an async provider for terminal session state (COMPLETED / FAILED) before re-verification.
394
+ */
395
+ export async function pollSessionState(provider, session, opts = {}) {
396
+ if (!session || !session.id) return { status: "COMPLETED" };
397
+ const initialStatus = String(session.status || session.state || "").toUpperCase();
398
+ if (initialStatus === "COMPLETED" || initialStatus === "FAILED" || opts.dryRun || session.id === "dry-run-session-id") {
399
+ return { status: initialStatus || "COMPLETED" };
400
+ }
401
+
402
+ const maxAttempts = opts.maxPollAttempts || 30;
403
+ const pollIntervalMs = opts.pollIntervalMs || 1000;
404
+ const timeoutMs = opts.pollTimeoutMs || 300000;
405
+ const startTime = Date.now();
406
+
407
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
408
+ if (Date.now() - startTime > timeoutMs) break;
409
+
410
+ let currentSession = null;
411
+ if (provider && typeof provider.getSession === "function") {
412
+ try {
413
+ currentSession = await provider.getSession(session.id, opts);
414
+ } catch (_) {}
415
+ } else if (typeof opts.pollFn === "function") {
416
+ try {
417
+ currentSession = await opts.pollFn(session.id);
418
+ } catch (_) {}
419
+ }
420
+
421
+ if (currentSession) {
422
+ const status = String(currentSession.status || currentSession.state || "").toUpperCase();
423
+ if (status === "COMPLETED" || status === "FAILED") {
424
+ return { ...currentSession, status };
425
+ }
426
+ } else {
427
+ break;
428
+ }
429
+
430
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
431
+ }
432
+
433
+ return { ...session, status: String(session.status || "COMPLETED").toUpperCase() };
434
+ }
435
+
382
436
  function buildRepairPrompt(failure, attempt, _config) {
383
437
  const cleanStderr = redactSecrets(failure.stderr || failure.stdout || "Unknown Error");
384
438
 
@@ -435,32 +489,54 @@ export async function dispatch(task, opts = {}) {
435
489
  );
436
490
  }
437
491
 
438
- export async function run(opts = {}) {
439
- const root = opts.root || process.cwd();
440
- const config = opts.config || loadConfig(root);
441
- const concurrency = opts.concurrency || config.limits.concurrency || 1;
492
+ export async function run(tasksOrOpts = {}, opts = {}) {
493
+ let tasks = null;
494
+ let options = opts;
495
+ if (Array.isArray(tasksOrOpts)) {
496
+ tasks = tasksOrOpts;
497
+ options = opts || {};
498
+ } else if (tasksOrOpts && typeof tasksOrOpts === "object") {
499
+ options = tasksOrOpts;
500
+ }
501
+
502
+ const root = options.root || process.cwd();
503
+ const config = options.config || loadConfig(root);
504
+ const concurrency = options.concurrency || config.limits.concurrency || 1;
505
+ const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
442
506
  const queueDir = getQueueDir(root);
443
507
  const completedDir = join(queueDir, "completed");
444
508
  ensureDir(completedDir);
445
509
 
446
- const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
510
+ let filesToProcess = [];
511
+ if (Array.isArray(tasks)) {
512
+ filesToProcess = tasks.map((t) => (typeof t === "string" ? t : t.id || t.file || t.title));
513
+ } else {
514
+ filesToProcess = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
515
+ }
516
+
447
517
  const results = [];
448
518
 
449
- for (let i = 0; i < files.length; i += concurrency) {
450
- const batch = files.slice(i, i + concurrency);
519
+ for (let i = 0; i < filesToProcess.length; i += concurrency) {
520
+ const batch = filesToProcess.slice(i, i + concurrency);
451
521
  await Promise.all(
452
- batch.map(async (file) => {
453
- const srcPath = join(queueDir, file);
522
+ batch.map(async (fileOrItem) => {
523
+ const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem.id || fileOrItem.file;
524
+ const srcPath = join(queueDir, fileName);
454
525
  try {
455
- const content = readFileSync(srcPath, "utf-8");
456
- const task = { title: file, prompt: content };
457
- const session = await dispatch(task, { root, config, dryRun: opts.dryRun });
458
- const dstPath = join(completedDir, file);
459
- renameSync(srcPath, dstPath);
460
- appendLedger({ event: "task_completed", file, session }, root);
461
- results.push({ file, ok: true, session });
526
+ let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
527
+ if (!prompt && existsSync(srcPath)) {
528
+ prompt = readFileSync(srcPath, "utf-8");
529
+ }
530
+ const task = { title: fileName, prompt };
531
+ const session = await dispatch(task, { root, config, dryRun: isDry });
532
+ const dstPath = join(completedDir, fileName);
533
+ if (existsSync(srcPath)) {
534
+ renameSync(srcPath, dstPath);
535
+ }
536
+ appendLedger({ event: "task_completed", file: fileName, session }, root);
537
+ results.push({ file: fileName, ok: true, session });
462
538
  } catch (err) {
463
- results.push({ file, ok: false, error: err.message });
539
+ results.push({ file: fileName, ok: false, error: err.message });
464
540
  }
465
541
  })
466
542
  );
package/src/envelope.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { git, resolveBase } from "./git.mjs";
1
+ import { git, runCmd, resolveBase } from "./git.mjs";
2
2
  import { checkScope } from "./security.mjs";
3
3
  import { normalizeScope } from "./config.mjs";
4
4
  import { existsSync } from "node:fs";
@@ -70,8 +70,8 @@ export function validateEnvelope(envelope = {}, opts = {}) {
70
70
  const absPath = join(root, relPath);
71
71
  let existsInGit = false;
72
72
  if (resolvedBase) {
73
- const catRes = git(["cat-file", "-e", `${resolvedBase}:${relPath}`], { cwd: root, ignoreError: true });
74
- existsInGit = typeof catRes === "string" && catRes.trim() !== "";
73
+ const catRes = runCmd(["git", "cat-file", "-e", `${resolvedBase}:${relPath}`], { cwd: root, ignoreError: true });
74
+ existsInGit = catRes.status === 0;
75
75
  }
76
76
  const existsOnDisk = existsSync(absPath);
77
77
  if (!existsInGit && !existsOnDisk) {
@@ -83,8 +83,6 @@ export function readVerifyRuns(root, testCmd = null) {
83
83
  }
84
84
  }
85
85
 
86
- export const getVerifyRuns = readVerifyRuns;
87
-
88
86
  /**
89
87
  * Evaluates flaky test verdict using a sliding window of the last n <= 10 runs.
90
88
  */
package/src/git.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { sep } from "node:path";
2
+ import { normalizePath } from "./config.mjs";
3
3
  import { journalIntent, journalDone } from "./journal.mjs";
4
4
 
5
5
  export const NET_GUARD_PRELOAD_URL = new URL("./preload-net-guard.mjs", import.meta.url).href;
@@ -13,11 +13,6 @@ export class GateError extends Error {
13
13
  }
14
14
  }
15
15
 
16
- function normalizePath(p) {
17
- if (!p || typeof p !== "string") return "";
18
- return p.split(sep).join("/").replace(/\\/g, "/");
19
- }
20
-
21
16
  const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
22
17
  const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
23
18
 
package/src/mcp.mjs CHANGED
@@ -9,7 +9,7 @@ import { ProgressBus } from "./mcp-progress.mjs";
9
9
 
10
10
  export const MCP_SERVER_INFO = {
11
11
  name: "jules-orchestrator-kit",
12
- version: "0.23.0",
12
+ version: "1.0.0",
13
13
  };
14
14
 
15
15
  export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
@@ -31,14 +31,14 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
31
31
  tool = "python3 -m py_compile";
32
32
  } else if (ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts") {
33
33
  const hasTsConfig = existsSync(join(root, "tsconfig.json"));
34
- if (hasTsConfig) {
35
- cmd = "npx";
36
- args = ["tsc", "--noEmit", tempFilePath];
34
+ const localTsc = join(root, "node_modules", ".bin", "tsc");
35
+ const hasLocalTsc = existsSync(localTsc);
36
+ if (hasTsConfig && hasLocalTsc) {
37
+ cmd = localTsc;
38
+ args = ["--noEmit", tempFilePath];
37
39
  tool = "tsc --noEmit";
38
40
  } else {
39
- cmd = "node";
40
- args = ["--check", tempFilePath];
41
- tool = "node --check";
41
+ return { ok: true, tool: "ts-skipped-no-tsconfig" };
42
42
  }
43
43
  } else {
44
44
  // JS, MJS, CJS or default
@@ -47,7 +47,7 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
47
47
  tool = "node --check";
48
48
  }
49
49
 
50
- const res = spawnSync(cmd, args, { encoding: "utf8" });
50
+ const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 120000 });
51
51
  const ok = res.status === 0;
52
52
 
53
53
  return {
package/src/state.mjs CHANGED
@@ -349,8 +349,9 @@ export function isPidAlive(pid, expectedStartTime = null) {
349
349
  return true;
350
350
  }
351
351
 
352
- export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
353
- const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
352
+ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot(), opts = {}) {
353
+ const root = typeof rootOrOpts === "string" ? rootOrOpts : (rootOrOpts?.root || resolveRoot());
354
+ const branch = (typeof rootOrOpts === "object" && rootOrOpts?.branch) || opts?.branch || process.env.JULES_BRANCH || process.env.BRANCH_NAME || "";
354
355
  const lockDir = getLockDir(root);
355
356
  const lockFile = join(lockDir, `${taskId}.json`);
356
357
 
@@ -375,6 +376,7 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
375
376
  const payload = {
376
377
  agent: agentName,
377
378
  taskId,
379
+ branch,
378
380
  files,
379
381
  pid: process.pid,
380
382
  processStartTime: startTime,
package/src/telemetry.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  openSync,
3
3
  writeSync,
4
+ readSync,
4
5
  closeSync,
5
6
  readFileSync,
6
7
  existsSync,
@@ -16,6 +17,37 @@ import { safeAtomicWrite } from "./security.mjs";
16
17
  export const MAX_TELEMETRY_SEGMENT_BYTES = 8 * 1024 * 1024; // 8 MB log segment ceiling
17
18
  export const TELEMETRY_GENESIS_HASH = "0".repeat(64);
18
19
 
20
+ /**
21
+ * Reads up to 64 KB from EOF of target log segment to extract true tail hash.
22
+ */
23
+ function readTailHash(filePath, bufferSize = 64 * 1024) {
24
+ if (!existsSync(filePath)) return null;
25
+ try {
26
+ const stat = statSync(filePath);
27
+ if (stat.size === 0) return null;
28
+ const readSize = Math.min(stat.size, bufferSize);
29
+ const position = stat.size - readSize;
30
+ const fd = openSync(filePath, "r");
31
+ const buf = Buffer.alloc(readSize);
32
+ try {
33
+ readSync(fd, buf, 0, readSize, position);
34
+ } finally {
35
+ closeSync(fd);
36
+ }
37
+ const content = buf.toString("utf-8");
38
+ const lines = content.split("\n").filter(Boolean);
39
+ for (let i = lines.length - 1; i >= 0; i--) {
40
+ try {
41
+ const obj = JSON.parse(lines[i]);
42
+ if (obj && typeof obj.hash === "string") {
43
+ return obj.hash;
44
+ }
45
+ } catch (_) {}
46
+ }
47
+ } catch (_) {}
48
+ return null;
49
+ }
50
+
19
51
  /**
20
52
  * Returns telemetry file paths for a given date string sorted by segment index.
21
53
  */
@@ -50,18 +82,10 @@ function coldScanTelemetry(stateDir, dateStr) {
50
82
  for (let i = segments.length - 1; i >= 0; i--) {
51
83
  const seg = segments[i];
52
84
  if (existsSync(seg.path)) {
53
- try {
54
- const content = readFileSync(seg.path, "utf-8");
55
- const lines = content.split("\n").filter(Boolean);
56
- for (let j = lines.length - 1; j >= 0; j--) {
57
- try {
58
- const entry = JSON.parse(lines[j]);
59
- if (entry && typeof entry.hash === "string") {
60
- return { hash: entry.hash, segment: seg.index };
61
- }
62
- } catch (_) {}
63
- }
64
- } catch (_) {}
85
+ const tailHash = readTailHash(seg.path);
86
+ if (tailHash) {
87
+ return { hash: tailHash, segment: seg.index };
88
+ }
65
89
  }
66
90
  }
67
91
 
@@ -70,7 +94,7 @@ function coldScanTelemetry(stateDir, dateStr) {
70
94
 
71
95
  /**
72
96
  * Appends a structured telemetry event to .agent/state/telemetry-<date>.jsonl with SHA-256 hash chaining.
73
- * Uses .head atomic cache for O(1) constant-time appends.
97
+ * Uses .head atomic cache with true tail reconciliation for O(1) constant-time appends.
74
98
  *
75
99
  * @param {string} [rootOrOpts]
76
100
  * @param {string} kind
@@ -102,18 +126,30 @@ export function appendTelemetry(rootOrOpts = resolveRoot(), kind = "event", fiel
102
126
  } catch (_) {}
103
127
  }
104
128
 
105
- if (!headValid) {
106
- const recovered = coldScanTelemetry(stateDir, dateStr);
107
- prevHash = recovered.hash;
108
- activeSegmentIndex = recovered.segment;
109
- }
110
-
111
129
  let activeFileName =
112
130
  activeSegmentIndex === 0
113
131
  ? `telemetry-${dateStr}.jsonl`
114
132
  : `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
115
133
  let activeFilePath = join(stateDir, activeFileName);
116
134
 
135
+ // Reconcile .head hash against true log tail (reading last 64 KB from EOF)
136
+ const tailHash = readTailHash(activeFilePath);
137
+ if (tailHash) {
138
+ if (tailHash !== prevHash) {
139
+ prevHash = tailHash;
140
+ }
141
+ headValid = true;
142
+ } else if (!headValid) {
143
+ const recovered = coldScanTelemetry(stateDir, dateStr);
144
+ prevHash = recovered.hash;
145
+ activeSegmentIndex = recovered.segment;
146
+ activeFileName =
147
+ activeSegmentIndex === 0
148
+ ? `telemetry-${dateStr}.jsonl`
149
+ : `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
150
+ activeFilePath = join(stateDir, activeFileName);
151
+ }
152
+
117
153
  if (existsSync(activeFilePath)) {
118
154
  try {
119
155
  const stat = statSync(activeFilePath);
@@ -1,30 +0,0 @@
1
- # Jules Task Queue Directory
2
-
3
- Drop markdown task specification files here (e.g. `TASK-001-feature-name.md`) to queue tasks for background execution by Google Jules.
4
-
5
- ### File Format Example
6
-
7
- ```markdown
8
- # TASK-001: Implement User Rate Limiting
9
-
10
- ## Objective
11
- Implement sliding window rate limiting for public API routes.
12
-
13
- ## Execution Rules
14
- - Use Redis / memory store for tracking hit counts.
15
- - Must return HTTP 429 Too Many Requests when limit is exceeded.
16
- - Must pass `npm test` before submitting PR.
17
- ```
18
-
19
- Process all queued tasks in batch:
20
-
21
- ```bash
22
- agentctl queue
23
- # or npm run jules:queue
24
- ```
25
-
26
- Or dispatch a single task using `agentctl dispatch`:
27
-
28
- ```bash
29
- agentctl dispatch --title "TASK-001 Rate Limiting" --prompt "$(cat .agent/jules-queue/TASK-001-rate-limiting.md)"
30
- ```
package/.agent/jules.yml DELETED
@@ -1,13 +0,0 @@
1
- # Google Jules Repository Configuration (Version 2)
2
- version: 2
3
- test_cmd: "npm test"
4
- build_cmd: ""
5
- forbidden_paths:
6
- - ".github/**"
7
- - "**/secrets/**"
8
- - "**/*.pem"
9
- - "**/lock-manager/**"
10
- - "scripts/jules-self-audit.mjs"
11
- - ".agent/jules.yml"
12
- allow_paths: []
13
-
package/.env.example DELETED
@@ -1,73 +0,0 @@
1
- # Google Jules Orchestration Kit - Environment Configuration Example
2
- # Copy this file to .env and populate with your credentials/settings.
3
-
4
- # --- Authentication & Core Settings ---
5
- # Google Jules API Key (Required for direct REST API dispatches)
6
- JULES_API_KEY=your_google_jules_api_key_here
7
-
8
- # Alias for API key (fallback if JULES_API_KEY is not set)
9
- # GEMINI_API_KEY=your_gemini_api_key_here
10
-
11
- # Override the Jules REST API URL
12
- # JULES_API_URL=https://jules.googleapis.com/v1alpha/sessions
13
-
14
- # Target GitHub Repository (Format: owner/repo)
15
- JULES_REPO=owner/repo
16
-
17
- # Set to "true" or "1" to run in repoless/serverless mode
18
- # JULES_REPOLESS=false
19
-
20
- # Enable Dry-Run mode to simulate payload dispatch without making API calls
21
- # JULES_DRY_RUN=false
22
-
23
- # --- Budget & Security Gatekeeper ---
24
- # Daily max session limit for autonomous dispatches (Default: 300)
25
- # JULES_DAILY_BUDGET=300
26
-
27
- # Allow modifications to command-defining files (package.json, Cargo.toml, etc.) in PRs (Default: false)
28
- # JULES_ALLOW_COMMAND_FILE_CHANGES=false
29
-
30
- # Allow modifications to agent rule files (AGENTS.md, JULES_RULES_TEMPLATE.md, .agent/rules/**) in PRs (Default: false)
31
- # JULES_ALLOW_AGENT_RULE_CHANGES=false
32
-
33
- # --- Execution Scope & Git ---
34
- # Base Branch for PR Audits & Merge-Base Calculations (Default: main)
35
- BASE_BRANCH=main
36
-
37
- # PR Head Branch (Used by CI to dynamically target branches during OODA repair)
38
- # GITHUB_HEAD_REF=my-feature-branch
39
-
40
- # Root directory of the project (Auto-assigned during swarm executions)
41
- # JULES_PROJECT_ROOT=/path/to/project
42
-
43
- # --- Swarm & Concurrency ---
44
- # Maximum parallel dispatches for swarm runs (Default: 3)
45
- JULES_SWARM_CONCURRENCY=3
46
-
47
- # Dispatch Stagger Interval in Milliseconds (Default: 1500)
48
- JULES_SWARM_STAGGER_MS=1500
49
-
50
- # Rate-limit for the jules:queue command (Default: 500)
51
- # JULES_PACE_MS=500
52
-
53
- # Use Git Worktrees instead of cloning for swarm isolation
54
- # JULES_USE_WORKTREES=false
55
-
56
- # Current slot index for partitioning tasks (Swarm mode)
57
- # JULES_SLOT_INDEX=1
58
-
59
- # Total number of slots for partitioning tasks (Swarm mode)
60
- # JULES_SLOT_TOTAL=3
61
-
62
- # --- CI & Output ---
63
- # Is this running in a CI environment? (Changes log output and fail-fast behaviors)
64
- # CI=true
65
-
66
- # Allow OODA Auto-Repair even in CI environments
67
- # ALLOW_AUTO_REPAIR=true
68
-
69
- # GitHub Actions Step Summary File Path
70
- # GITHUB_STEP_SUMMARY=/path/to/step_summary.md
71
-
72
- # Disable color in terminal output
73
- # NO_COLOR=true
@@ -1,49 +0,0 @@
1
- name: Jules PR Audit & Test Gatekeeper
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- audit-and-test:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- node-version: ['20.x', '22.x', '24.x']
15
- steps:
16
- - name: Checkout repository
17
- uses: actions/checkout@v4
18
- with:
19
- fetch-depth: 0
20
-
21
- - name: Setup Node.js
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
-
26
- - name: Cache OODA state & ledgers
27
- uses: actions/cache@v4
28
- with:
29
- path: .agent/state/
30
- key: ooda-state-${{ runner.os }}-${{ github.run_id }}
31
- restore-keys: |
32
- ooda-state-${{ runner.os }}-
33
-
34
- - name: Install dependencies
35
- run: npm install
36
-
37
- - name: Run Linter
38
- run: npm run lint --if-present
39
-
40
- - name: Run Unit Tests
41
- run: npm test --if-present
42
-
43
- - name: Run Jules PR Self-Audit Gatekeeper
44
- if: matrix.node-version == '22.x' && github.event_name == 'pull_request'
45
- run: node scripts/jules-self-audit.mjs
46
- env:
47
- CI: "true"
48
- BASE_BRANCH: "main"
49
-
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Backward compatibility shim for jules-cleanup.mjs in v0.9.0.
5
- */
6
-
7
- export function extractPrUrls(outputs = []) {
8
- const prs = [];
9
- const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
10
- for (const item of outputs) {
11
- const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
12
- const matches = text.match(regex);
13
- if (matches) {
14
- for (const m of matches) {
15
- if (!prs.includes(m)) prs.push(m);
16
- }
17
- }
18
- }
19
- return prs;
20
- }
21
-
22
- export function auditSessions(sessions = [], opts = {}) {
23
- const staleHoursThreshold = opts.staleHoursThreshold || 24;
24
- const now = Date.now();
25
- const merged = [];
26
- const active = [];
27
- const stale = [];
28
-
29
- for (const s of sessions) {
30
- if (s.state === "MERGED") {
31
- merged.push(s);
32
- } else {
33
- const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
34
- const ageHours = (now - updatedMs) / (1000 * 60 * 60);
35
- if (ageHours > staleHoursThreshold) {
36
- stale.push(s);
37
- } else {
38
- active.push(s);
39
- }
40
- }
41
- }
42
-
43
- return { merged, active, stale };
44
- }
45
-
46
- if (process.argv[1] && process.argv[1].endsWith("jules-cleanup.mjs")) {
47
- console.log("[Shim] Cleanup audit completed.");
48
- process.exit(0);
49
- }