From ee11c2d316f3c2d2e0f8d07fbb2a55422101382e Mon Sep 17 00:00:00 2001 From: IsraelAraujo70 Date: Thu, 30 Apr 2026 14:43:14 -0300 Subject: [PATCH 1/2] ci: add AI PR review workflow (Codex spike) Smoke-test workflow that runs OpenAI Codex CLI against every PR using a ChatGPT subscription auth (no API key). First step toward a YAML-driven, per-area PR review that gates merges via REQUEST_CHANGES. Auth flow: ~/.codex/auth.json is seeded from the CODEX_AUTH_JSON repo secret on cache miss, then persisted between runs via actions/cache so Codex's auto-refreshed token survives. Setup instructions in .github/AI_PR_REVIEW_SETUP.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/AI_PR_REVIEW_SETUP.md | 35 ++++++++ .github/workflows/ai-pr-review.yml | 127 +++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 .github/AI_PR_REVIEW_SETUP.md create mode 100644 .github/workflows/ai-pr-review.yml diff --git a/.github/AI_PR_REVIEW_SETUP.md b/.github/AI_PR_REVIEW_SETUP.md new file mode 100644 index 0000000..4a9bbd5 --- /dev/null +++ b/.github/AI_PR_REVIEW_SETUP.md @@ -0,0 +1,35 @@ +# AI PR review — setup + +The `ai-pr-review.yml` workflow runs OpenAI Codex CLI against every PR using a ChatGPT subscription (no API key). This file documents the one-time setup required. + +## 1. Generate the Codex auth file locally + +On your dev machine, with the ChatGPT subscription you want to use: + +```bash +npm install -g @openai/codex +codex login # opens a browser — sign in with ChatGPT +cat ~/.codex/auth.json +``` + +Copy the **entire** contents of `~/.codex/auth.json`. Treat it like a password. + +## 2. Add the repo secret + +GitHub repo → **Settings → Secrets and variables → Actions → New repository secret**: + +- Name: `CODEX_AUTH_JSON` +- Value: paste the JSON from step 1 + +## 3. How the workflow uses it + +- On the first run (or after the auth cache is purged), the workflow seeds `~/.codex/auth.json` from this secret. +- Codex refreshes the file in place during use; the workflow caches it back so subsequent runs reuse the refreshed token. +- The cache key is `codex-auth--`, with restore-key `codex-auth--` — every run saves a fresh copy and restores the most recent. + +If the token ever expires beyond Codex's auto-refresh window, re-run `codex login` locally and update the secret. + +## Notes + +- **Forks:** PRs from forks do not have access to repo secrets, so the workflow will fail on community PRs. For an internal-only repo (current Prism setup) this is fine. If we later need fork support, switch to `pull_request_target` with explicit safeguards. +- **Spike status:** the current workflow only posts a smoke-test comment. Per-area rubrics, structured findings, and `REQUEST_CHANGES` blocking come in follow-up steps. diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml new file mode 100644 index 0000000..8afb70e --- /dev/null +++ b/.github/workflows/ai-pr-review.yml @@ -0,0 +1,127 @@ +name: AI PR review + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + ai-review: + name: Codex review + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Restore Codex auth cache + id: codex-cache + uses: actions/cache@v4 + with: + path: ~/.codex/auth.json + key: codex-auth-${{ github.repository }}-${{ github.run_id }} + restore-keys: | + codex-auth-${{ github.repository }}- + + - name: Seed Codex auth from secret on cache miss + if: steps.codex-cache.outputs.cache-hit != 'true' + env: + CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} + run: | + set -euo pipefail + if [ -z "${CODEX_AUTH_JSON:-}" ]; then + echo "::error::CODEX_AUTH_JSON secret is not set and no cached auth was restored. Run 'codex login' locally and paste the contents of ~/.codex/auth.json into the CODEX_AUTH_JSON repo secret." + exit 1 + fi + mkdir -p ~/.codex + printf '%s' "$CODEX_AUTH_JSON" > ~/.codex/auth.json + chmod 600 ~/.codex/auth.json + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Codex CLI + run: npm install -g @openai/codex + + - name: Verify Codex auth + run: | + codex --version + codex auth status || true + + - name: Compute PR diff + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + git fetch --no-tags --depth=1 origin "$BASE_SHA" + git diff "$BASE_SHA"..."$HEAD_SHA" > /tmp/pr.diff + echo "Diff size: $(wc -c "$PROMPT_FILE" + codex exec \ + --sandbox read-only \ + --skip-git-repo-check \ + --output-last-message /tmp/codex-out.txt \ + "$(cat $PROMPT_FILE)" \ + 2>&1 | tee /tmp/codex-log.txt + echo "--- Final message ---" + cat /tmp/codex-out.txt + + - name: Post review comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ''; + const ok = '${{ steps.codex.outcome }}' === 'success'; + let body; + if (ok && fs.existsSync('/tmp/codex-out.txt')) { + const out = fs.readFileSync('/tmp/codex-out.txt', 'utf8').trim() || '_(empty response)_'; + body = [marker, '## 🤖 AI PR review (spike)', '', out].join('\n'); + } else { + const log = fs.existsSync('/tmp/codex-log.txt') + ? fs.readFileSync('/tmp/codex-log.txt', 'utf8').slice(-3000) + : '(no log captured)'; + body = [ + marker, + '## 🤖 AI PR review (spike) — failed', + '', + 'Codex execution failed. Last log lines:', + '', + '```', + log, + '```', + ].join('\n'); + } + + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, per_page: 100, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } From abd8dbd44cdb9942d3ba846e28475a0d7e28860e Mon Sep 17 00:00:00 2001 From: IsraelAraujo70 Date: Thu, 30 Apr 2026 14:50:51 -0300 Subject: [PATCH 2/2] ci(ai-review): per-area rubrics + reasoning=high Replaces the single smoke-test prompt with a YAML-driven, per-area review: - pr-review.yml defines areas (rust, frontend, ci, docs) with a path glob and a rubric derived from CLAUDE.md. - pr_review.py parses the YAML, computes which areas the PR touches, and runs one `codex exec` invocation per area with a scoped diff and the area's rubric. Each area returns a VERDICT (PASS/CONCERNS) plus findings. - Workflow drops the auth.json cache (Codex itself flagged the leak risk on PR #11) and re-seeds from the secret on every run; the refresh_token in CODEX_AUTH_JSON is stable so token refresh still works. - Comment now shows a verdict table + per-area sections instead of a single freeform paragraph. - Reasoning effort bumped to high; depth matters more than speed for reviewer role. REQUEST_CHANGES gating + branch protection still on the roadmap. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/AI_PR_REVIEW_SETUP.md | 35 ------ .github/pr-review.yml | 82 ++++++++++++++ .github/scripts/pr_review.py | 171 +++++++++++++++++++++++++++++ .github/workflows/ai-pr-review.yml | 132 ++++++++++++---------- 4 files changed, 326 insertions(+), 94 deletions(-) delete mode 100644 .github/AI_PR_REVIEW_SETUP.md create mode 100644 .github/pr-review.yml create mode 100644 .github/scripts/pr_review.py diff --git a/.github/AI_PR_REVIEW_SETUP.md b/.github/AI_PR_REVIEW_SETUP.md deleted file mode 100644 index 4a9bbd5..0000000 --- a/.github/AI_PR_REVIEW_SETUP.md +++ /dev/null @@ -1,35 +0,0 @@ -# AI PR review — setup - -The `ai-pr-review.yml` workflow runs OpenAI Codex CLI against every PR using a ChatGPT subscription (no API key). This file documents the one-time setup required. - -## 1. Generate the Codex auth file locally - -On your dev machine, with the ChatGPT subscription you want to use: - -```bash -npm install -g @openai/codex -codex login # opens a browser — sign in with ChatGPT -cat ~/.codex/auth.json -``` - -Copy the **entire** contents of `~/.codex/auth.json`. Treat it like a password. - -## 2. Add the repo secret - -GitHub repo → **Settings → Secrets and variables → Actions → New repository secret**: - -- Name: `CODEX_AUTH_JSON` -- Value: paste the JSON from step 1 - -## 3. How the workflow uses it - -- On the first run (or after the auth cache is purged), the workflow seeds `~/.codex/auth.json` from this secret. -- Codex refreshes the file in place during use; the workflow caches it back so subsequent runs reuse the refreshed token. -- The cache key is `codex-auth--`, with restore-key `codex-auth--` — every run saves a fresh copy and restores the most recent. - -If the token ever expires beyond Codex's auto-refresh window, re-run `codex login` locally and update the secret. - -## Notes - -- **Forks:** PRs from forks do not have access to repo secrets, so the workflow will fail on community PRs. For an internal-only repo (current Prism setup) this is fine. If we later need fork support, switch to `pull_request_target` with explicit safeguards. -- **Spike status:** the current workflow only posts a smoke-test comment. Per-area rubrics, structured findings, and `REQUEST_CHANGES` blocking come in follow-up steps. diff --git a/.github/pr-review.yml b/.github/pr-review.yml new file mode 100644 index 0000000..46d6625 --- /dev/null +++ b/.github/pr-review.yml @@ -0,0 +1,82 @@ +version: 1 + +defaults: + reasoning_effort: high + max_diff_chars_per_area: 60000 + +areas: + - id: rust + label: "Backend (Rust / Tauri)" + paths: + - "src-tauri/**" + rubric: | + You are reviewing changes to the Rust/Tauri backend of Prism. Apply these conventions + from CLAUDE.md strictly. Treat each as a hard rule unless the diff contains an explicit + justification: + + 1. The GitHub OAuth token must NEVER cross to the frontend. Token load/save/delete + lives only in `src-tauri/src/auth.rs`. Frontend may only learn auth status (boolean), + never the token value. + 2. Errors must use the `AppError` enum from `src-tauri/src/error.rs`. Avoid `.unwrap()` + and `.expect()` in `#[tauri::command]` handlers — they panic the host. + 3. For GitHub features, prefer `Client::graphql` (one query with aliases) over + multiple REST calls. New REST usage needs a justification in the diff or commit body. + 4. Schema changes in `db.rs` must be additive and idempotent (use + `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN` guarded by a migration + check). Never silently drop columns. + 5. New crate dependencies in `Cargo.toml` need a clear reason — flag any addition + without one. + + - id: frontend + label: "Frontend (React / TypeScript)" + paths: + - "src/**" + rubric: | + You are reviewing changes to the React/TypeScript frontend of Prism. Apply these + conventions from CLAUDE.md strictly: + + 1. `src/lib/api.ts` is the single boundary to Rust. Every `invoke()` wrapper here must + mirror its Rust counterpart's argument and return types — flag any drift. + 2. UI uses shadcn/ui primitives in `src/components/ui/` and Tailwind v4 tokens. Avoid + hand-rolled CSS that duplicates an existing primitive. + 3. The app is dark-mode only (`class="dark"` is hardcoded on ``). Light-mode-only + styles or branches are dead code — flag them. + 4. UI state (e.g. sidebar collapsed, expanded orgs) goes to `localStorage` with the + `prism.*` prefix. User data (watched repos, tracked orgs) goes to SQLite via Tauri + commands. Mixing these layers is a smell. + 5. New npm dependencies need a clear reason — flag any addition without one. + + - id: ci + label: "CI / GitHub Actions" + paths: + - ".github/**" + rubric: | + You are reviewing changes to GitHub Actions / CI configuration. Apply these checks: + + 1. Secrets are NEVER echoed, logged, or written to artifacts. `set -x`, `echo` of an + env var holding a secret, and uploading files containing secrets are all violations. + 2. Workflows triggered by `pull_request_target` execute with repo write permissions + and access secrets — flag any change from `pull_request` to `pull_request_target` + unless the diff includes explicit fork-safety review (no checkout of PR head, no + execution of PR-supplied code). + 3. Third-party actions (anything not under `actions/`) should be pinned to a tagged + version or a SHA. Pure `@main` references are a supply-chain risk. + 4. Permissions blocks should follow least-privilege. Flag any `permissions: write-all` + or broadening of an existing block without justification. + + - id: docs + label: "Documentation" + paths: + - "docs/**" + - "*.md" + - "**/*.md" + rubric: | + You are reviewing documentation changes. Apply these checks: + + 1. `docs/PRD.md` is the source of truth for product scope. Changes that alter scope + or conventions must also be reflected in `CLAUDE.md` (or vice versa) — flag drift + between the two. + 2. Code, identifiers, and commit messages stay in English. PT-BR is acceptable in + user-facing UI copy and in user-conversation transcripts but not in code. + 3. Avoid stating things the code already says clearly. Documentation should explain + the WHY (constraints, motivations, past decisions), not narrate the WHAT. diff --git a/.github/scripts/pr_review.py b/.github/scripts/pr_review.py new file mode 100644 index 0000000..05d5749 --- /dev/null +++ b/.github/scripts/pr_review.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Run Codex CLI as a per-area PR reviewer. + +Reads `.github/pr-review.yml`, computes which areas the PR diff touches, +and runs one `codex exec` invocation per affected area with a scoped diff +plus the area's rubric. Aggregates findings into a single JSON file that +the workflow uses to post a PR comment. +""" +import argparse +import fnmatch +import json +import subprocess +import sys +import time +from pathlib import Path + +import yaml + + +def changed_files(base_sha: str, head_sha: str) -> list[str]: + out = subprocess.check_output( + ["git", "diff", "--name-only", f"{base_sha}...{head_sha}"], + text=True, + ) + return [line for line in out.splitlines() if line.strip()] + + +def match_area(area: dict, files: list[str]) -> list[str]: + patterns = area.get("paths", []) or [] + matched: list[str] = [] + for f in files: + for pat in patterns: + if fnmatch.fnmatch(f, pat): + matched.append(f) + break + return matched + + +def scoped_diff(base_sha: str, head_sha: str, files: list[str]) -> str: + result = subprocess.run( + ["git", "diff", f"{base_sha}...{head_sha}", "--"] + files, + capture_output=True, text=True, check=True, + ) + return result.stdout + + +def build_prompt(area: dict, diff_text: str, files: list[str]) -> str: + file_list = "\n".join(f"- {f}" for f in files) + return f"""You are reviewing a pull request, scoped to the area: {area.get('label', area['id'])}. + +Files in this area touched by the PR: +{file_list} + +Apply the following rubric. Each rule is a hard requirement unless the diff +includes an explicit justification. + +{area['rubric']} + +Output format (plain text, no markdown headers): +- Line 1: VERDICT: PASS or VERDICT: CONCERNS +- If CONCERNS, follow with a numbered list. Each item: + N. :. Suggestion: . +- Cite a specific line in the diff for every concern. Do not invent issues. +- Do not flag style or lint issues unless the rubric explicitly demands it. +- Be concise but specific. No filler. + +--- DIFF (scoped to this area) --- +{diff_text} +--- END DIFF --- +""" + + +def run_codex(prompt: str, reasoning_effort: str, output_file: Path) -> tuple[int, str, str]: + if output_file.exists(): + output_file.unlink() + cmd = [ + "codex", "exec", + "--sandbox", "read-only", + "--skip-git-repo-check", + "-c", f"model_reasoning_effort={reasoning_effort}", + "--output-last-message", str(output_file), + prompt, + ] + started = time.time() + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + elapsed = time.time() - started + final = output_file.read_text().strip() if output_file.exists() else "" + log = ((result.stdout or "") + (result.stderr or "")).strip() + print(f" codex exec finished in {elapsed:.1f}s, rc={result.returncode}, " + f"final_len={len(final)}, log_len={len(log)}", flush=True) + return result.returncode, final, log + + +def parse_verdict(output: str) -> str: + for line in output.splitlines(): + s = line.strip().upper() + if s.startswith("VERDICT:"): + v = s.split(":", 1)[1].strip() + if v.startswith("PASS"): + return "pass" + if v.startswith("CONCERN"): + return "concerns" + return "unknown" + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--config", default=".github/pr-review.yml") + p.add_argument("--base-sha", required=True) + p.add_argument("--head-sha", required=True) + p.add_argument("--out", default="/tmp/pr-review-results.json") + args = p.parse_args() + + config = yaml.safe_load(Path(args.config).read_text()) + defaults = config.get("defaults", {}) or {} + reasoning = defaults.get("reasoning_effort", "medium") + max_diff_chars = int(defaults.get("max_diff_chars_per_area", 60_000)) + + files = changed_files(args.base_sha, args.head_sha) + print(f"Changed files ({len(files)}):", flush=True) + for f in files: + print(f" {f}", flush=True) + + results = [] + for area in config.get("areas", []) or []: + matched = match_area(area, files) + if not matched: + print(f"\n[{area['id']}] no matching files, skipping", flush=True) + results.append({ + "area": area["id"], + "label": area.get("label", area["id"]), + "skipped": True, + "files": [], + "verdict": "skipped", + "output": "", + }) + continue + + print(f"\n[{area['id']}] {len(matched)} matching file(s):", flush=True) + for f in matched: + print(f" {f}", flush=True) + + diff_text = scoped_diff(args.base_sha, args.head_sha, matched) + truncated = False + if len(diff_text) > max_diff_chars: + diff_text = diff_text[:max_diff_chars] + "\n\n[... diff truncated ...]" + truncated = True + + prompt = build_prompt(area, diff_text, matched) + out_path = Path(f"/tmp/codex-final-{area['id']}.txt") + rc, final, log = run_codex(prompt, reasoning, out_path) + + results.append({ + "area": area["id"], + "label": area.get("label", area["id"]), + "skipped": False, + "files": matched, + "diff_truncated": truncated, + "ok": rc == 0, + "verdict": parse_verdict(final) if rc == 0 else "error", + "output": final, + "log_tail": log[-2000:] if rc != 0 else "", + }) + + Path(args.out).write_text(json.dumps(results, indent=2)) + print(f"\nWrote {len(results)} area result(s) to {args.out}", flush=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ai-pr-review.yml b/.github/workflows/ai-pr-review.yml index 8afb70e..9564733 100644 --- a/.github/workflows/ai-pr-review.yml +++ b/.github/workflows/ai-pr-review.yml @@ -12,30 +12,20 @@ jobs: ai-review: name: Codex review runs-on: ubuntu-22.04 - timeout-minutes: 10 + timeout-minutes: 25 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - - name: Restore Codex auth cache - id: codex-cache - uses: actions/cache@v4 - with: - path: ~/.codex/auth.json - key: codex-auth-${{ github.repository }}-${{ github.run_id }} - restore-keys: | - codex-auth-${{ github.repository }}- - - - name: Seed Codex auth from secret on cache miss - if: steps.codex-cache.outputs.cache-hit != 'true' + - name: Seed Codex auth from secret env: CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} run: | set -euo pipefail if [ -z "${CODEX_AUTH_JSON:-}" ]; then - echo "::error::CODEX_AUTH_JSON secret is not set and no cached auth was restored. Run 'codex login' locally and paste the contents of ~/.codex/auth.json into the CODEX_AUTH_JSON repo secret." + echo "::error::CODEX_AUTH_JSON secret is not set. Run 'codex login' locally and store the contents of ~/.codex/auth.json in the CODEX_AUTH_JSON repo secret." exit 1 fi mkdir -p ~/.codex @@ -46,76 +36,100 @@ jobs: with: node-version: 20 - - name: Install Codex CLI - run: npm install -g @openai/codex + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - - name: Verify Codex auth + - name: Install Codex CLI and pyyaml run: | - codex --version - codex auth status || true + npm install -g @openai/codex + pip install pyyaml - - name: Compute PR diff + - name: Run AI review per area + id: review env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail git fetch --no-tags --depth=1 origin "$BASE_SHA" - git diff "$BASE_SHA"..."$HEAD_SHA" > /tmp/pr.diff - echo "Diff size: $(wc -c "$PROMPT_FILE" - codex exec \ - --sandbox read-only \ - --skip-git-repo-check \ - --output-last-message /tmp/codex-out.txt \ - "$(cat $PROMPT_FILE)" \ - 2>&1 | tee /tmp/codex-log.txt - echo "--- Final message ---" - cat /tmp/codex-out.txt - - - name: Post review comment + - name: Post aggregated review comment if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); const marker = ''; - const ok = '${{ steps.codex.outcome }}' === 'success'; + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + + const verdictBadge = (v) => { + if (v === 'pass') return '✅ PASS'; + if (v === 'concerns') return '⚠️ CONCERNS'; + if (v === 'skipped') return '⏭️ skipped'; + if (v === 'error') return '💥 error'; + return '❓ unknown'; + }; + let body; - if (ok && fs.existsSync('/tmp/codex-out.txt')) { - const out = fs.readFileSync('/tmp/codex-out.txt', 'utf8').trim() || '_(empty response)_'; - body = [marker, '## 🤖 AI PR review (spike)', '', out].join('\n'); + const reviewOk = '${{ steps.review.outcome }}' === 'success'; + + if (reviewOk && fs.existsSync('/tmp/pr-review-results.json')) { + const results = JSON.parse(fs.readFileSync('/tmp/pr-review-results.json', 'utf8')); + + const lines = [marker, '## 🤖 AI PR review', '']; + + lines.push('| Area | Verdict | Files |'); + lines.push('| --- | --- | ---: |'); + for (const r of results) { + const filesCell = r.skipped ? '—' : String(r.files.length); + lines.push(`| ${r.label} | ${verdictBadge(r.verdict)} | ${filesCell} |`); + } + lines.push(''); + + const reviewed = results.filter(r => !r.skipped); + if (reviewed.length === 0) { + lines.push('_No area in `pr-review.yml` matched the PR diff._'); + } else { + for (const r of reviewed) { + lines.push(`### ${r.label} — ${verdictBadge(r.verdict)}`); + if (r.diff_truncated) { + lines.push('_Diff truncated for this area; review may be partial._'); + } + if (r.ok) { + lines.push(''); + lines.push(r.output || '_(empty response)_'); + } else { + lines.push(''); + lines.push('Codex execution failed. Last log lines:'); + lines.push('```'); + lines.push(r.log_tail || '(no log captured)'); + lines.push('```'); + } + lines.push(''); + } + } + + lines.push('---'); + lines.push('_Reviewed with Codex CLI · rubrics in `.github/pr-review.yml`_'); + body = lines.join('\n'); } else { - const log = fs.existsSync('/tmp/codex-log.txt') - ? fs.readFileSync('/tmp/codex-log.txt', 'utf8').slice(-3000) - : '(no log captured)'; body = [ marker, - '## 🤖 AI PR review (spike) — failed', - '', - 'Codex execution failed. Last log lines:', + '## 🤖 AI PR review — failed', '', - '```', - log, - '```', + 'The review script did not produce a results file. Check the workflow logs.', ].join('\n'); } - const { owner, repo } = context.repo; - const issue_number = context.payload.pull_request.number; const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100, });