Add Conley (1999) spatial HAC SE on DiD/TWFE/MultiPeriodDiD (Phase 1 of spillover-conley) #1595
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: AI PR Review | |
| on: | |
| pull_request: | |
| types: [opened] | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: ai-pr-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| review: | |
| runs-on: ubuntu-latest | |
| # Run if: | |
| # - PR opened, OR | |
| # - Comment "/ai-review" on a PR by a collaborator/member/owner (issue or inline diff comment) | |
| if: | | |
| (github.event_name == 'pull_request') || | |
| ( | |
| github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request != null && | |
| startsWith(github.event.comment.body, '/ai-review') && | |
| ( | |
| github.event.comment.author_association == 'OWNER' || | |
| github.event.comment.author_association == 'MEMBER' || | |
| github.event.comment.author_association == 'COLLABORATOR' | |
| ) | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_review_comment' && | |
| startsWith(github.event.comment.body, '/ai-review') && | |
| ( | |
| github.event.comment.author_association == 'OWNER' || | |
| github.event.comment.author_association == 'MEMBER' || | |
| github.event.comment.author_association == 'COLLABORATOR' | |
| ) | |
| ) | |
| steps: | |
| - name: Resolve PR number + metadata | |
| id: pr | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const prNumber = | |
| context.payload.pull_request?.number ?? | |
| context.payload.issue?.number ?? | |
| context.payload.pull_request_review?.pull_request?.number ?? | |
| (() => { | |
| const url = context.payload.pull_request_url; | |
| if (!url) return null; | |
| const m = url.match(/\/pulls\/(\d+)$/); | |
| return m ? Number(m[1]) : null; | |
| })(); | |
| if (!prNumber) { | |
| throw new Error("Could not determine PR number from event payload"); | |
| } | |
| const { owner, repo } = context.repo; | |
| const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); | |
| core.setOutput("number", prNumber); | |
| core.setOutput("title", pr.data.title || ""); | |
| core.setOutput("body", pr.data.body || ""); | |
| core.setOutput("base_sha", pr.data.base.sha); | |
| core.setOutput("head_sha", pr.data.head.sha); | |
| core.setOutput("base_ref", pr.data.base.ref); | |
| core.setOutput("head_ref", pr.data.head.ref); | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: refs/pull/${{ steps.pr.outputs.number }}/merge | |
| - name: Pre-fetch base and head refs | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin \ | |
| "${{ steps.pr.outputs.base_ref }}" \ | |
| +refs/pull/${{ steps.pr.outputs.number }}/head | |
| - name: Fetch previous AI review (if any) | |
| id: prev_review | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const issue_number = Number('${{ steps.pr.outputs.number }}'); | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const aiComments = comments.filter(c => | |
| (c.body || "").includes("<!-- ai-pr-review:codex:") && | |
| c.user?.login === "github-actions[bot]" | |
| ); | |
| const last = aiComments.length > 0 ? aiComments[aiComments.length - 1] : null; | |
| core.setOutput("body", last ? last.body : ""); | |
| core.setOutput("found", last ? "true" : "false"); | |
| - name: Build review inputs (diff + previous review) | |
| env: | |
| BASE_SHA: ${{ steps.pr.outputs.base_sha }} | |
| HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| IS_RERUN: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' }} | |
| PREV_REVIEW: ${{ steps.prev_review.outputs.body }} | |
| PREV_REVIEW_FOUND: ${{ steps.prev_review.outputs.found }} | |
| run: | | |
| set -euo pipefail | |
| # Exclude large generated/data files from the full diff to stay | |
| # within the model's input limit. The --name-status output still | |
| # lists them. Narrowed to real-data assets and notebook outputs. | |
| git --no-pager diff --unified=5 "$BASE_SHA" "$HEAD_SHA" \ | |
| -- . ':!benchmarks/data/real/*.json' ':!benchmarks/data/real/*.csv' \ | |
| ':!docs/tutorials/*.ipynb' \ | |
| > /tmp/pr-diff.patch | |
| git --no-pager diff --name-status "$BASE_SHA" "$HEAD_SHA" \ | |
| > /tmp/pr-files.txt | |
| if [ "$IS_RERUN" = "true" ] && [ "$PREV_REVIEW_FOUND" = "true" ]; then | |
| printf '%s\n' "$PREV_REVIEW" > /tmp/previous-review.md | |
| fi | |
| - name: Run AI review (single-shot Responses API) | |
| id: run_review | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| BRANCH: ${{ steps.pr.outputs.head_ref }} | |
| PR_TITLE: ${{ steps.pr.outputs.title }} | |
| PR_BODY: ${{ steps.pr.outputs.body }} | |
| IS_RERUN: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' }} | |
| run: | | |
| set -euo pipefail | |
| # Pin --model gpt-5.5 explicitly so future bumps to the script's | |
| # DEFAULT_MODEL don't silently ship to CI without review. | |
| # Use --key=value form for untrusted PR title/body so argparse | |
| # cannot misinterpret an option-looking value (e.g. a PR body | |
| # starting with "--") as a separate flag and break the job. | |
| ARGS=(--ci-mode --full-registry --context standard --model gpt-5.5 | |
| --review-criteria .github/codex/prompts/pr_review.md | |
| --registry docs/methodology/REGISTRY.md | |
| --diff /tmp/pr-diff.patch | |
| --changed-files /tmp/pr-files.txt | |
| --output /tmp/review-output.md | |
| --branch-info "$BRANCH" | |
| "--pr-title=$PR_TITLE" | |
| "--pr-body=$PR_BODY" | |
| --repo-root "$(pwd)") | |
| if [ "$IS_RERUN" = "true" ] && [ -f /tmp/previous-review.md ]; then | |
| ARGS+=(--previous-review /tmp/previous-review.md) | |
| fi | |
| python3 .claude/scripts/openai_review.py "${ARGS[@]}" | |
| - name: Post PR comment (new on /ai-review, update on opened) | |
| uses: actions/github-script@v9 | |
| env: | |
| PR_NUMBER: ${{ steps.pr.outputs.number }} | |
| HEAD_SHA: ${{ steps.pr.outputs.head_sha }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let msg = ''; | |
| try { | |
| msg = fs.readFileSync('/tmp/review-output.md', 'utf8').trim(); | |
| } catch (e) { | |
| core.setFailed(`Could not read review output: ${e.message}`); | |
| return; | |
| } | |
| if (!msg) return; | |
| const { owner, repo } = context.repo; | |
| const issue_number = Number(process.env.PR_NUMBER); | |
| // If this run was triggered by /ai-review (issue_comment or review_comment), create a NEW comment. | |
| const isRerun = | |
| context.eventName === "issue_comment" || | |
| context.eventName === "pull_request_review_comment"; | |
| // Marker for the "canonical" auto comment. Kept as :codex: for | |
| // backward compatibility with historical PR comments — the marker | |
| // is just an identifier for the canonical auto comment, not a | |
| // declaration of the backend. | |
| const marker = "<!-- ai-pr-review:codex:auto -->"; | |
| // For reruns, use a unique marker so nothing ever gets overwritten | |
| const rerunMarker = `<!-- ai-pr-review:codex:rerun:${process.env.GITHUB_RUN_ID} -->`; | |
| const header = isRerun | |
| ? `🔁 **AI review rerun** (requested by @${context.actor})\n\n**Head SHA:** \`${process.env.HEAD_SHA}\`\n\n---\n` | |
| : ""; | |
| const body = `${isRerun ? rerunMarker : marker}\n\n${header}${msg}`.trim(); | |
| if (isRerun) { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| return; | |
| } | |
| // Auto run: update existing canonical comment if present | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => (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 }); | |
| } |