agent-gauntlet 0.10.0 → 0.11.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.
Files changed (71) hide show
  1. package/README.md +25 -23
  2. package/dist/index.js +9226 -0
  3. package/dist/index.js.map +65 -0
  4. package/dist/scripts/status.js +280 -0
  5. package/dist/scripts/status.js.map +10 -0
  6. package/package.json +22 -8
  7. package/src/built-in-reviews/code-quality.md +0 -25
  8. package/src/built-in-reviews/index.ts +0 -28
  9. package/src/bun-plugins.d.ts +0 -4
  10. package/src/cli-adapters/claude.ts +0 -327
  11. package/src/cli-adapters/codex.ts +0 -290
  12. package/src/cli-adapters/cursor.ts +0 -128
  13. package/src/cli-adapters/gemini.ts +0 -510
  14. package/src/cli-adapters/github-copilot.ts +0 -141
  15. package/src/cli-adapters/index.ts +0 -250
  16. package/src/cli-adapters/thinking-budget.ts +0 -23
  17. package/src/commands/check.ts +0 -311
  18. package/src/commands/ci/index.ts +0 -15
  19. package/src/commands/ci/init.ts +0 -96
  20. package/src/commands/ci/list-jobs.ts +0 -90
  21. package/src/commands/clean.ts +0 -54
  22. package/src/commands/detect.ts +0 -173
  23. package/src/commands/health.ts +0 -169
  24. package/src/commands/help.ts +0 -34
  25. package/src/commands/index.ts +0 -13
  26. package/src/commands/init.ts +0 -1878
  27. package/src/commands/list.ts +0 -33
  28. package/src/commands/review.ts +0 -311
  29. package/src/commands/run.ts +0 -29
  30. package/src/commands/shared.ts +0 -267
  31. package/src/commands/stop-hook.ts +0 -567
  32. package/src/commands/validate.ts +0 -20
  33. package/src/commands/wait-ci.ts +0 -518
  34. package/src/config/ci-loader.ts +0 -33
  35. package/src/config/ci-schema.ts +0 -28
  36. package/src/config/global.ts +0 -87
  37. package/src/config/loader.ts +0 -301
  38. package/src/config/schema.ts +0 -165
  39. package/src/config/stop-hook-config.ts +0 -130
  40. package/src/config/types.ts +0 -65
  41. package/src/config/validator.ts +0 -592
  42. package/src/core/change-detector.ts +0 -137
  43. package/src/core/diff-stats.ts +0 -442
  44. package/src/core/entry-point.ts +0 -190
  45. package/src/core/job.ts +0 -96
  46. package/src/core/run-executor.ts +0 -621
  47. package/src/core/runner.ts +0 -290
  48. package/src/gates/check.ts +0 -118
  49. package/src/gates/resolve-check-command.ts +0 -21
  50. package/src/gates/result.ts +0 -54
  51. package/src/gates/review.ts +0 -1333
  52. package/src/hooks/adapters/claude-stop-hook.ts +0 -99
  53. package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
  54. package/src/hooks/adapters/types.ts +0 -94
  55. package/src/hooks/stop-hook-handler.ts +0 -748
  56. package/src/index.ts +0 -47
  57. package/src/output/app-logger.ts +0 -214
  58. package/src/output/console-log.ts +0 -168
  59. package/src/output/console.ts +0 -359
  60. package/src/output/logger.ts +0 -126
  61. package/src/output/sinks/console-sink.ts +0 -59
  62. package/src/output/sinks/file-sink.ts +0 -110
  63. package/src/scripts/status.ts +0 -433
  64. package/src/templates/workflow.yml +0 -79
  65. package/src/types/gauntlet-status.ts +0 -79
  66. package/src/utils/debug-log.ts +0 -392
  67. package/src/utils/diff-parser.ts +0 -103
  68. package/src/utils/execution-state.ts +0 -472
  69. package/src/utils/log-parser.ts +0 -696
  70. package/src/utils/sanitizer.ts +0 -3
  71. package/src/utils/session-ref.ts +0 -91
@@ -1,433 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Gauntlet Status Script
4
- *
5
- * Parses the configured log_dir (default: gauntlet_logs/) to produce a structured
6
- * summary of the most recent gauntlet session from the .debug.log, plus a file
7
- * inventory of all log/JSON files for further inspection.
8
- *
9
- * This script handles structured data only (debug log events). Detailed failure
10
- * analysis (reading individual check logs, review JSONs) is left to the caller
11
- * (the /gauntlet-status skill) since log formats vary by check type.
12
- */
13
-
14
- import fs from "node:fs";
15
- import path from "node:path";
16
-
17
- // --- Types ---
18
-
19
- interface RunStart {
20
- timestamp: string;
21
- mode: string;
22
- baseRef?: string;
23
- filesChanged: number;
24
- linesAdded: number;
25
- linesRemoved: number;
26
- gates: number;
27
- }
28
-
29
- interface GateResult {
30
- timestamp: string;
31
- gateId: string;
32
- cli?: string;
33
- status: string;
34
- duration: string;
35
- violations?: number;
36
- }
37
-
38
- interface RunEnd {
39
- timestamp: string;
40
- status: string;
41
- fixed: number;
42
- skipped: number;
43
- failed: number;
44
- iterations: number;
45
- duration: string;
46
- }
47
-
48
- interface StopHookEntry {
49
- timestamp: string;
50
- decision: string;
51
- reason: string;
52
- }
53
-
54
- interface SessionRun {
55
- start: RunStart;
56
- gates: GateResult[];
57
- end?: RunEnd;
58
- stopHook?: StopHookEntry;
59
- }
60
-
61
- // --- Parsing helpers ---
62
-
63
- function parseKeyValue(text: string): Record<string, string> {
64
- const result: Record<string, string> = {};
65
- for (const match of text.matchAll(/(\w+)=(\S+)/g)) {
66
- const key = match[1];
67
- const value = match[2];
68
- if (key && value) result[key] = value;
69
- }
70
- return result;
71
- }
72
-
73
- function parseTimestamp(line: string): string {
74
- const m = line.match(/^\[([^\]]+)\]/);
75
- return m?.[1] ?? "";
76
- }
77
-
78
- function parseEventType(line: string): string {
79
- const m = line.match(/^\[[^\]]+\]\s+(\S+)/);
80
- return m?.[1] ?? "";
81
- }
82
-
83
- function parseEventBody(line: string): string {
84
- const m = line.match(/^\[[^\]]+\]\s+\S+\s*(.*)/);
85
- return m?.[1] ?? "";
86
- }
87
-
88
- // --- Debug log parsing ---
89
-
90
- function parseDebugLog(content: string, sessionStartTime?: Date): SessionRun[] {
91
- const lines = content.split("\n").filter((l) => l.trim());
92
- const sessions: SessionRun[] = [];
93
- let current: SessionRun | null = null;
94
-
95
- for (const line of lines) {
96
- const event = parseEventType(line);
97
- const body = parseEventBody(line);
98
- const ts = parseTimestamp(line);
99
-
100
- switch (event) {
101
- case "RUN_START": {
102
- // Skip runs that predate the current session's log files
103
- if (sessionStartTime && new Date(ts) < sessionStartTime) {
104
- current = null;
105
- break;
106
- }
107
- const kv = parseKeyValue(body);
108
- current = {
109
- start: {
110
- timestamp: ts,
111
- mode: kv.mode ?? "unknown",
112
- baseRef: kv.base_ref,
113
- filesChanged: Number(kv.files_changed ?? kv.changes ?? 0),
114
- linesAdded: Number(kv.lines_added ?? 0),
115
- linesRemoved: Number(kv.lines_removed ?? 0),
116
- gates: Number(kv.gates ?? 0),
117
- },
118
- gates: [],
119
- };
120
- sessions.push(current);
121
- break;
122
- }
123
- case "GATE_RESULT": {
124
- if (!current) break;
125
- const gateIdMatch = body.match(/^(\S+)/);
126
- const kv = parseKeyValue(body);
127
- current.gates.push({
128
- timestamp: ts,
129
- gateId: gateIdMatch?.[1] ?? "unknown",
130
- cli: kv.cli,
131
- status: kv.status ?? "unknown",
132
- duration: kv.duration ?? "?",
133
- violations:
134
- kv.violations !== undefined ? Number(kv.violations) : undefined,
135
- });
136
- break;
137
- }
138
- case "RUN_END": {
139
- if (!current) break;
140
- const kv = parseKeyValue(body);
141
- current.end = {
142
- timestamp: ts,
143
- status: kv.status ?? "unknown",
144
- fixed: Number(kv.fixed ?? 0),
145
- skipped: Number(kv.skipped ?? 0),
146
- failed: Number(kv.failed ?? 0),
147
- iterations: Number(kv.iterations ?? 0),
148
- duration: kv.duration ?? "?",
149
- };
150
- break;
151
- }
152
- case "STOP_HOOK": {
153
- if (!current) break;
154
- const kv = parseKeyValue(body);
155
- current.stopHook = {
156
- timestamp: ts,
157
- decision: kv.decision ?? "unknown",
158
- reason: kv.reason ?? "unknown",
159
- };
160
- break;
161
- }
162
- }
163
- }
164
-
165
- return sessions;
166
- }
167
-
168
- /**
169
- * Find the earliest mtime of non-hidden log files in the directory.
170
- * This marks the start of the current session.
171
- */
172
- function getSessionStartTime(logDir: string): Date | undefined {
173
- const entries = fs
174
- .readdirSync(logDir)
175
- .filter((f) => !f.startsWith(".") && f !== "previous");
176
- let earliest: number | undefined;
177
- for (const entry of entries) {
178
- const mtime = fs.statSync(path.join(logDir, entry)).mtimeMs;
179
- if (earliest === undefined || mtime < earliest) {
180
- earliest = mtime;
181
- }
182
- }
183
- return earliest !== undefined ? new Date(earliest) : undefined;
184
- }
185
-
186
- // --- File inventory ---
187
-
188
- function formatFileInventory(logDir: string): string[] {
189
- const lines: string[] = [];
190
- const entries = fs
191
- .readdirSync(logDir)
192
- .filter((f) => !f.startsWith(".") && f !== "previous");
193
- if (entries.length === 0) return lines;
194
-
195
- const checks: string[] = [];
196
- const reviews: string[] = [];
197
- const other: string[] = [];
198
-
199
- for (const entry of entries.sort()) {
200
- const fullPath = path.join(logDir, entry);
201
- const stat = fs.statSync(fullPath);
202
- const sizeKB = (stat.size / 1024).toFixed(1);
203
- const line = `- ${fullPath} (${sizeKB} KB)`;
204
-
205
- if (entry.startsWith("review_")) {
206
- reviews.push(line);
207
- } else if (entry.startsWith("check_")) {
208
- checks.push(line);
209
- } else {
210
- other.push(line);
211
- }
212
- }
213
-
214
- lines.push("### Log Files");
215
- lines.push("");
216
- if (checks.length > 0) {
217
- lines.push("**Check logs:**");
218
- lines.push(...checks);
219
- }
220
- if (reviews.length > 0) {
221
- lines.push("**Review logs/JSON:**");
222
- lines.push(...reviews);
223
- }
224
- if (other.length > 0) {
225
- lines.push("**Other:**");
226
- lines.push(...other);
227
- }
228
- lines.push("");
229
-
230
- return lines;
231
- }
232
-
233
- // --- Summary output ---
234
-
235
- function formatStatusLine(end: RunEnd): string {
236
- return end.status === "pass"
237
- ? "PASSED"
238
- : end.status === "fail"
239
- ? "FAILED"
240
- : end.status.toUpperCase();
241
- }
242
-
243
- function formatAllRuns(sessions: SessionRun[]): string[] {
244
- const lines: string[] = [];
245
- lines.push("### All Runs in Session");
246
- lines.push("");
247
- for (let i = 0; i < sessions.length; i++) {
248
- const s = sessions[i];
249
- if (!s) continue;
250
- const status = s.end ? s.end.status : "in-progress";
251
- const duration = s.end ? s.end.duration : "?";
252
- lines.push(
253
- `${i + 1}. [${s.start.timestamp}] mode=${s.start.mode} status=${status} duration=${duration}`,
254
- );
255
- }
256
- lines.push("");
257
- return lines;
258
- }
259
-
260
- function formatSession(sessions: SessionRun[], logDir: string): string {
261
- if (sessions.length === 0) {
262
- return "No gauntlet runs found in logs.";
263
- }
264
-
265
- const lastComplete = [...sessions].reverse().find((s) => s.end);
266
- const session = lastComplete ?? sessions[sessions.length - 1];
267
- if (!session) return "No gauntlet runs found in logs.";
268
-
269
- const lines: string[] = [];
270
-
271
- // Header
272
- lines.push("## Gauntlet Session Summary");
273
- lines.push("");
274
-
275
- // Overall status
276
- if (session.end) {
277
- lines.push(`**Status:** ${formatStatusLine(session.end)}`);
278
- lines.push(`**Iterations:** ${session.end.iterations}`);
279
- lines.push(`**Duration:** ${session.end.duration}`);
280
- lines.push(
281
- `**Fixed:** ${session.end.fixed} | **Skipped:** ${session.end.skipped} | **Failed:** ${session.end.failed}`,
282
- );
283
- } else {
284
- lines.push("**Status:** In Progress (no RUN_END found)");
285
- }
286
- lines.push("");
287
-
288
- // Diff stats
289
- lines.push("### Diff Stats");
290
- lines.push(`- Mode: ${session.start.mode}`);
291
- if (session.start.baseRef) {
292
- lines.push(`- Base ref: ${session.start.baseRef}`);
293
- }
294
- lines.push(`- Files changed: ${session.start.filesChanged}`);
295
- lines.push(
296
- `- Lines: +${session.start.linesAdded} / -${session.start.linesRemoved}`,
297
- );
298
- lines.push(`- Gates: ${session.start.gates}`);
299
- lines.push("");
300
-
301
- // Gate results
302
- lines.push("### Gate Results");
303
- lines.push("");
304
- lines.push("| Gate | CLI | Status | Duration | Violations |");
305
- lines.push("|------|-----|--------|----------|------------|");
306
- for (const gate of session.gates) {
307
- const violations =
308
- gate.violations !== undefined ? String(gate.violations) : "-";
309
- const statusIcon = gate.status === "pass" ? "pass" : "FAIL";
310
- lines.push(
311
- `| ${gate.gateId} | ${gate.cli ?? "-"} | ${statusIcon} | ${gate.duration} | ${violations} |`,
312
- );
313
- }
314
- lines.push("");
315
-
316
- // Stop hook
317
- if (session.stopHook) {
318
- lines.push("### Stop Hook");
319
- lines.push(`- Decision: ${session.stopHook.decision}`);
320
- lines.push(`- Reason: ${session.stopHook.reason}`);
321
- lines.push("");
322
- }
323
-
324
- // File inventory
325
- lines.push(...formatFileInventory(logDir));
326
-
327
- // All sessions summary (if multiple runs)
328
- if (sessions.length > 1) {
329
- lines.push(...formatAllRuns(sessions));
330
- }
331
-
332
- return lines.join("\n");
333
- }
334
-
335
- // --- Main ---
336
-
337
- /**
338
- * Read the configured log_dir from .gauntlet/config.yml.
339
- * Falls back to "gauntlet_logs" if not found.
340
- */
341
- function getLogDir(cwd: string): string {
342
- const configPath = path.join(cwd, ".gauntlet", "config.yml");
343
- try {
344
- const content = fs.readFileSync(configPath, "utf-8");
345
- const match = content.match(/^log_dir:\s*(.+)$/m);
346
- if (match?.[1]) return match[1].trim();
347
- } catch {
348
- // Config not found — use default
349
- }
350
- return "gauntlet_logs";
351
- }
352
-
353
- /**
354
- * Resolve the log directory and debug log path.
355
- * Returns null if no logs are found (after printing a message).
356
- */
357
- function resolveLogPaths(
358
- activeDir: string,
359
- ): { logDir: string; debugLogPath: string } | null {
360
- const previousDir = path.join(activeDir, "previous");
361
- const debugLogPath = path.join(activeDir, ".debug.log");
362
-
363
- // Check active directory first for non-debug log files
364
- const activeHasLogs =
365
- fs.existsSync(activeDir) &&
366
- fs
367
- .readdirSync(activeDir)
368
- .some((f) => !f.startsWith(".") && f !== "previous");
369
-
370
- if (activeHasLogs) {
371
- return { logDir: activeDir, debugLogPath };
372
- }
373
-
374
- if (!fs.existsSync(previousDir)) {
375
- console.log("No gauntlet_logs directory found.");
376
- return null;
377
- }
378
-
379
- // Fall back to previous directory — cleanLogs archives files directly here
380
- const logDir = resolvePreviousLogDir(previousDir);
381
- if (!logDir) return null;
382
-
383
- // Debug log stays in the main gauntlet_logs dir, not in previous/
384
- return { logDir, debugLogPath };
385
- }
386
-
387
- function resolvePreviousLogDir(previousDir: string): string | null {
388
- const prevEntries = fs.readdirSync(previousDir);
389
- const hasDirectFiles = prevEntries.some(
390
- (f) => f.endsWith(".log") || f.endsWith(".json"),
391
- );
392
-
393
- if (hasDirectFiles) return previousDir;
394
-
395
- // Legacy: check for timestamped subdirectories
396
- const prevDirs = prevEntries
397
- .map((d) => path.join(previousDir, d))
398
- .filter((d) => fs.statSync(d).isDirectory())
399
- .sort()
400
- .reverse();
401
-
402
- if (prevDirs.length === 0) {
403
- console.log("No gauntlet logs found.");
404
- return null;
405
- }
406
-
407
- return prevDirs[0] as string;
408
- }
409
-
410
- function main(): void {
411
- const cwd = process.cwd();
412
- const logDirName = getLogDir(cwd);
413
- const activeDir = path.join(cwd, logDirName);
414
-
415
- const paths = resolveLogPaths(activeDir);
416
- if (!paths) {
417
- process.exit(0);
418
- }
419
-
420
- // Parse debug log, filtering to current session based on log file timestamps
421
- let sessions: SessionRun[] = [];
422
- if (fs.existsSync(paths.debugLogPath)) {
423
- const debugContent = fs.readFileSync(paths.debugLogPath, "utf-8");
424
- const sessionStart = getSessionStartTime(paths.logDir);
425
- sessions = parseDebugLog(debugContent, sessionStart);
426
- }
427
-
428
- // Format and output
429
- const output = formatSession(sessions, paths.logDir);
430
- console.log(output);
431
- }
432
-
433
- main();
@@ -1,79 +0,0 @@
1
- name: Gauntlet CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- discover:
11
- name: Discover Jobs
12
- runs-on: ubuntu-latest
13
- outputs:
14
- matrix: ${{ steps.discover.outputs.matrix }}
15
- runtimes: ${{ steps.discover.outputs.runtimes }}
16
- steps:
17
- - uses: actions/checkout@v4
18
-
19
- - name: Install agent-gauntlet
20
- run: |
21
- curl -fsSL https://bun.sh/install | bash
22
- echo "$HOME/.bun/bin" >> $GITHUB_PATH
23
- export PATH="$HOME/.bun/bin:$PATH"
24
- bun add -g pacaplan/agent-gauntlet
25
-
26
- - name: Discover gauntlet jobs
27
- id: discover
28
- run: |
29
- output=$(agent-gauntlet ci list-jobs)
30
- echo "matrix=$(echo "$output" | jq -c '.matrix')" >> $GITHUB_OUTPUT
31
- echo "runtimes=$(echo "$output" | jq -c '.runtimes')" >> $GITHUB_OUTPUT
32
-
33
- checks:
34
- name: ${{ matrix.job.name }} (${{ matrix.job.entry_point }})
35
- runs-on: ubuntu-latest
36
- needs: discover
37
- if: ${{ needs.discover.outputs.matrix != '[]' }}
38
- strategy:
39
- fail-fast: false
40
- matrix:
41
- job: ${{ fromJson(needs.discover.outputs.matrix) }}
42
-
43
- # Services will be injected here by agent-gauntlet
44
-
45
- steps:
46
- - uses: actions/checkout@v4
47
-
48
- - name: Set up Ruby
49
- if: contains(matrix.job.runtimes, 'ruby')
50
- uses: ruby/setup-ruby@v1
51
- with:
52
- ruby-version: ${{ fromJson(needs.discover.outputs.runtimes).ruby.version }}
53
- bundler-cache: ${{ fromJson(needs.discover.outputs.runtimes).ruby.bundler_cache }}
54
- working-directory: ${{ matrix.job.working_directory }}
55
-
56
- - name: Set up Node
57
- if: contains(matrix.job.runtimes, 'node')
58
- uses: actions/setup-node@v4
59
- with:
60
- node-version: ${{ fromJson(needs.discover.outputs.runtimes).node.version }}
61
-
62
- - name: Set up Bun
63
- if: contains(matrix.job.runtimes, 'bun')
64
- uses: oven-sh/setup-bun@v1
65
- with:
66
- bun-version: ${{ fromJson(needs.discover.outputs.runtimes).bun.version }}
67
-
68
- - name: Run global setup
69
- if: ${{ matrix.job.global_setup != '' }}
70
- run: ${{ matrix.job.global_setup }}
71
-
72
- - name: Run check setup
73
- if: ${{ matrix.job.setup != '' }}
74
- working-directory: ${{ matrix.job.working_directory }}
75
- run: ${{ matrix.job.setup }}
76
-
77
- - name: Run check
78
- working-directory: ${{ matrix.job.working_directory }}
79
- run: ${{ matrix.job.command }}
@@ -1,79 +0,0 @@
1
- /**
2
- * All possible outcomes from gauntlet operations.
3
- * Used by both the run executor and stop-hook - NO MAPPING REQUIRED.
4
- */
5
- export type GauntletStatus =
6
- // Run outcomes (from executor)
7
- | "passed" // All gates passed
8
- | "passed_with_warnings" // Some issues were skipped
9
- | "no_applicable_gates" // No gates matched current changes
10
- | "no_changes" // No changes detected
11
- | "failed" // Gates failed, retries remaining
12
- | "retry_limit_exceeded" // Max retries reached
13
- | "lock_conflict" // Another run in progress
14
- | "error" // Unexpected error (includes config errors)
15
- | "pr_push_required" // Gates passed but PR needs to be created/updated
16
- // CI workflow statuses (after PR is pushed)
17
- | "ci_pending" // CI checks still running
18
- | "ci_failed" // CI checks failed or review changes requested
19
- | "ci_passed" // CI checks passed, no blocking reviews
20
- | "ci_timeout" // CI wait attempts exhausted
21
- // Stop-hook pre-checks (before running executor)
22
- | "no_config" // No .gauntlet/config.yml found
23
- | "stop_hook_active" // Infinite loop prevention
24
- | "interval_not_elapsed" // Run interval hasn't passed
25
- | "invalid_input" // Failed to parse hook JSON input
26
- | "stop_hook_disabled"; // Stop hook disabled via configuration
27
-
28
- export interface RunResult {
29
- status: GauntletStatus;
30
- /** Human-friendly message explaining the outcome */
31
- message: string;
32
- /** Number of gates that ran */
33
- gatesRun?: number;
34
- /** Number of gates that failed */
35
- gatesFailed?: number;
36
- /** Path to latest console log file */
37
- consoleLogPath?: string;
38
- /** Error message if status is "error" */
39
- errorMessage?: string;
40
- /** Interval minutes (when status is "interval_not_elapsed") */
41
- intervalMinutes?: number;
42
- /** Individual gate results (available when gates were executed) */
43
- gateResults?: Array<{
44
- jobId: string;
45
- status: "pass" | "fail" | "error";
46
- logPath?: string;
47
- logPaths?: string[];
48
- subResults?: Array<{
49
- nameSuffix: string;
50
- status: "pass" | "fail" | "error";
51
- logPath?: string;
52
- }>;
53
- }>;
54
- }
55
-
56
- /**
57
- * Determine if a status should block the stop hook.
58
- */
59
- export function isBlockingStatus(status: GauntletStatus): boolean {
60
- return (
61
- status === "failed" ||
62
- status === "pr_push_required" ||
63
- status === "ci_pending" ||
64
- status === "ci_failed"
65
- );
66
- }
67
-
68
- /**
69
- * Determine if a status indicates successful completion (exit code 0).
70
- */
71
- export function isSuccessStatus(status: GauntletStatus): boolean {
72
- return (
73
- status === "passed" ||
74
- status === "passed_with_warnings" ||
75
- status === "no_applicable_gates" ||
76
- status === "no_changes" ||
77
- status === "ci_passed"
78
- );
79
- }