Skip to content

fix(pr-agent): fork gate for /commands, and AI_TIMEOUT under its own step cap - #90

Merged
yakimoto merged 8 commits into
mainfrom
fix/418-fork-gate-and-ai-timeout
Sep 8, 2026
Merged

fix(pr-agent): fork gate for /commands, and AI_TIMEOUT under its own step cap#90
yakimoto merged 8 commits into
mainfrom
fix/418-fork-gate-and-ai-timeout

Conversation

@yakimoto

@yakimoto yakimoto commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

User description

This repo merged the inline pr-agent lane before two defects in it were found. The 16 repos whose adoption PRs are still open were re-synced in place; this one had already merged, so it needs its own PR.

Source of truth: wave-av/wave-foundation-public#73. Findings tracked as wave-pen#418; the fan-out wave as wave-pen#417.

1. Forks were unchecked on the issue_comment arm — and not by omission

The job-level if: refuses forks on pull_request via head.repo.fork == false. The issue_comment arm carried no such check, while the header comment claimed "Forks skipped (no secrets there)" — true of one arm, false of the other.

The reason it was missing is structural. Fork status is not in an issue_comment payload. Measured, with a positive control so the absence is a measurement and not a guess:

$ gh api repos/wave-av/<repo>/issues/47 --jq ".pull_request | keys"
["diff_url","html_url","merged_at","patch_url","url"]

$ gh api repos/wave-av/<repo>/pulls/47 --jq ".head.repo.fork"
false

Five URLs. No head, no repo. There was never an expression to write — so the check moves to a fork gate step that asks the pulls endpoint, which does carry it.

It fails closed. Only a literal false yields fork=false; everything else skips. Each branch was driven against a stubbed gh, not reasoned about:

gh returns meaning result
false same-repo PR fork=false — proceed
true fork PR fork=true — skip, warn
exit 1 / 404 token revoked, PR gone fork=true — skip
empty rate limit, network fork=true — skip
null fork deleted after the PR opened fork=true — skip

"I could not tell" must not reach the same answer as "not a fork" on the arm that carries OPENAI_KEY. The cost of erring this way is one skipped advisory review.

Severity, stated precisely rather than inflated

This lane runs no actions/checkout. Fork code is never fetched or executed, so there was no exfiltration path. What a /review on a fork PR actually reaches is the fork's diff, sent to the LLM router on our key — cost surface, already narrowed by the author_association allowlist.

So this is defence in depth. The durable risk was the comment, not the missing check: it told the next editor the guard was already there, and the day someone adds a checkout step to this lane, that belief is what would make it real.

2. CONFIG__AI_TIMEOUT was 600s inside a 360s step — in both env blocks

Unreachable by construction. The runner killed the step first, so pr-agent never reached its own timeout, never fell back to CONFIG__FALLBACK_MODELS, and returned no error the retry could classify. It also undercut the per-attempt classifier, which reasons about STEP_BUDGET_S: "360" — a budget the AI layer inside the step did not respect.

Now 300: 60s of headroom under the cap, and above both observed successful reviews (64s, 180s).

3. A latent classifier bug the gate exposed — fixed at the root

stamp attempt 2 end carries if: always(), so it fires even when attempt 2 never ran, and END - ${START:-0} then subtracted from zero. Running the unmodified classifier against that state:

::warning::pr-agent TIMED OUT — the longest attempt ran 1787580408s against a 360s per-attempt budget

A 56-year attempt, reported as a confident diagnosis. Fixed in the arithmetic rather than by special-casing the caller, and the verdict gains an explicit skipped branch so a gated skip is not misread as "failed after 2 attempts".

Receipts

  • actionlint clean · zizmor --persona=regular clean · both new run: blocks shellcheck clean.
  • Classifier executed old-vs-new across five states. The skipped path goes 56-year-timeout → a correct notice; success, cancelled, never-ran, real-double-failure, and a genuine 350s timeout are all byte-identical between old and new.
  • Every event-derived value crosses into the shell through env:, never ${{ }} in a script body.
  • Already proven in the fleet: wave-av/api-spec merged this exact file and its main is byte-identical to the template.

The job id stays pr_agent, so the check-run context is unchanged and no branch protection rule needs touching.

Refs wave-pen#418, wave-pen#417, wave-pen#388


Note

Medium Risk
Changes guard a workflow lane that holds OPENAI_KEY and pull-request write permissions; behavior is fail-closed but misconfiguration could skip legitimate reviews or still affect cost/abuse surface on fork diffs.

Overview
Hardens the inline pr-agent GitHub Actions workflow so slash commands and concurrency behave correctly and timeouts are classified accurately.

Concurrency now distinguishes PR vs plain issue in the group key, so an unrelated issue comment cannot cancel an in-flight review on a PR that shares the same numeric id.

Fork handling on / commands: a new fork gate step resolves fork status via the pulls API for issue_comment events (where the job if: cannot), defaults to refusing, and only runs PR-Agent when fork == 'false'. Maintainer /review on fork PRs is skipped with a warning instead of using OPENAI_KEY on the fork diff.

CONFIG__AI_TIMEOUT is lowered from 600s to 300s in both agent steps so the AI budget stays inside the 6-minute step cap and fallback/retry logic can run.

The verdict step treats fork-gated skipped as a benign exit and fixes attempt-2 duration math when retry never started (avoiding bogus multi-billion-second “TIMED OUT” warnings). CHANGELOG documents these three fixes.

Reviewed by Cursor Bugbot for commit 2f76b95. Bugbot is set up for automated code reviews on this repo. Configure here.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic


PR Type

Bug fix


Description

  • Added fork gate step for issue_comment events to prevent execution on forks

  • Reduced CONFIG__AI_TIMEOUT from 600s to 300s to fit within step budget

  • Fixed timeout detection logic to handle failed attempts correctly

  • Updated changelog with detailed defect fixes


Diagram Walkthrough

flowchart LR
  A["pr-agent.yml"] --> B["Add fork gate step"]
  A --> C["Adjust AI timeout"]
  A --> D["Fix timeout detection"]
  E["CHANGELOG.md"] --> F["Add defect fix entry"]
Loading

File Walkthrough

Relevant files
Bug fix
pr-agent.yml
Enhanced fork protection and timeout management                   

.github/workflows/pr-agent.yml

  • Added fork gate step to explicitly check for forks in issue_comment
    events
  • Reduced CONFIG__AI_TIMEOUT from 600 to 300 seconds to fit within step
    budget
  • Fixed timeout detection logic to handle cases where second attempt
    didn't run
  • Updated concurrency grouping to differentiate between PRs and issues
+126/-5 
Documentation
CHANGELOG.md
Updated changelog with defect fixes                                           

CHANGELOG.md

  • Added detailed entry for three defects fixed: fork gate handling, AI
    timeout budget, and timeout detection logic
  • Documented impact on forked PR reviews and maintainer workflows
+26/-0   

…step cap

This repo merged the inline pr-agent lane before two defects in it were found.
The 16 repos whose adoption PRs are still open were re-synced in place; this one
already merged, so it needs its own PR. Source of truth: wave-foundation-public#73.

1. Fork status is now RESOLVED for slash commands, not assumed. The job-level
   `if:` refuses forks on the `pull_request` arm; it structurally cannot on
   `issue_comment`, because fork status is absent from that payload — measured,
   with a positive control: `issues/<n>.pull_request` carries exactly [diff_url,
   html_url, merged_at, patch_url, url], while `pulls/<n>.head.repo.fork`
   answers. A `fork gate` step asks the pulls endpoint and FAILS CLOSED: only a
   literal `false` proceeds; a 404, a revoked token, a rate limit and
   `.head.repo = null` (fork deleted after the PR opened) all skip.

   Scope, stated rather than inflated: this lane runs no `actions/checkout`, so
   fork code is never fetched or executed and no exfiltration path existed. What
   a /review on a fork PR reaches is the fork diff, sent to the LLM router on
   our key — cost surface, already narrowed by the author_association allowlist.
   The durable defect was the COMMENT claiming "Forks skipped (no secrets
   there)": true of one arm, false of the other, and exactly what would mislead
   whoever adds a checkout step later.

2. CONFIG__AI_TIMEOUT 600 -> 300, in both env blocks. A 600s AI budget inside a
   360s step is unreachable: the runner killed the step first, so pr-agent never
   reached its own timeout, never fell back to CONFIG__FALLBACK_MODELS, and
   returned no error the retry could classify.

3. A latent classifier bug the gate exposed. `stamp attempt 2 end` runs under
   `if: always()`, so when attempt 2 never ran the arithmetic subtracted from
   ZERO and reported a 1787580408-second attempt as a confident TIMED OUT.
   Fixed at the arithmetic rather than by special-casing the caller; the verdict
   also gains an explicit `skipped` branch.

The job id stays `pr_agent`, so the check-run context is unchanged and no branch
protection rule needs touching.

Refs wave-pen#418, wave-pen#417, wave-pen#388

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codeant-ai

codeant-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed 2f76b95 Sep 07, 2026 · 23:50 23:51
✅ Incremental review completed 29ea004 Sep 06, 2026 · 22:17 22:18
✅ Reviewed your PR 0df27da Aug 24, 2026 · 14:16 14:16

@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_80699c96-3406-4f2f-bd26-637077914da4)

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: e68fe75f-8c0d-4a3b-8498-cc06b397454e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: f3cfbf9e-77c3-411d-996e-d6d71d664f75

📥 Commits

Reviewing files that changed from the base of the PR and between 0e59286 and 29ea004.

📒 Files selected for processing (2)
  • .github/workflows/pr-agent.yml
  • CHANGELOG.md

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🧰 Additional context used
📓 Path-based instructions (1)
Conventional Commit titles; update `CHANGELOG.md` (`Unreleased`) for user-facing changes.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • CHANGELOG.md
🪛 zizmor (1.29.0)
.github/workflows/pr-agent.yml

[info] 65-65: workflow or action definition without a name (anonymous-definition): this job

(anonymous-definition)


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Fork-triggered pull-request commands are now blocked safely and return a warning instead of running reviews.
    • Pull-request and issue activity no longer interfere with each other during automated reviews.
    • Missing retry attempts no longer produce invalid timing information.
    • Automated review attempts now use a 300-second timeout to prevent exceeding workflow limits.
  • Documentation

    • Added an Unreleased changelog entry describing these workflow and review-handling fixes.

Walkthrough

The PR updates the agent workflow to block fork-triggered reviews, separate event concurrency, reduce AI timeouts, handle intentional skips, and avoid invalid retry-duration values. The changelog records these fixes.

Changes

PR agent workflow safeguards

Layer / File(s) Summary
Fork detection and routing
.github/workflows/pr-agent.yml
The concurrency key separates event contexts. A fail-closed fork gate queries pull-request metadata and permits the agent only when fork is exactly false.
Agent execution and verdict handling
.github/workflows/pr-agent.yml
Both agent attempts use a 300-second AI timeout. The verdict step treats fork-gated skips as successful refusals and emits a notice.
Retry accounting and release notes
.github/workflows/pr-agent.yml, CHANGELOG.md
Retry duration is zero when attempt 2 has no valid start timestamp. The changelog documents the workflow fixes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 29ea0

This updates the PR-agent workflow to safely reject fork-triggered commands, prevent conflicting comment runs, bound AI execution time, and correctly report skipped retries. No current merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant issue_comment
  participant PullRequestAPI
  participant fork_gate
  participant agent_step
  participant verdict_step
  issue_comment->>PullRequestAPI: Request pull-request metadata
  PullRequestAPI-->>fork_gate: Return fork status
  fork_gate->>agent_step: Run only when fork is false
  fork_gate->>verdict_step: Report intentional skip when fork is true or unreadable
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary changes: adding a fork gate for slash commands and reducing the AI timeout to fit the step limit.
Description check ✅ Passed The description is detailed, relevant, and explains the changes, motivation, behavior, testing, and risk. It does not use the template headings or include the checklist, but the required What and Why …
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/418-fork-gate-and-ai-timeout
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/418-fork-gate-and-ai-timeout

Comment @coderabbitai help to get the list of available commands.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Aug 24, 2026
@macroscopeapp

macroscopeapp Bot commented Aug 24, 2026

Copy link
Copy Markdown

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes a secret-bearing GitHub Actions lane that processes fork pull requests, including its fork authorization gate and conditions for running AI review work. Although the timeout and classifier fixes are focused and well-contained, the security-sensitive execution changes require human review.

Not approved because:

  • Credit balance exhausted. Approvability relies on correctness review in order to determine eligibility

No code changes detected at 2f76b95. Prior analysis still applies.

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Gate pr-agent /commands on forks and align AI timeout with step budget

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Resolve fork status for /commands via pulls API lookup and fail closed.
• Skip PR-Agent execution on forks/unknown, emitting explicit warnings/notices.
• Set AI timeout below step budget and fix retry-duration accounting in verdict logic.
Diagram

graph TD
  E["GitHub event"] --> J["Job-level guard"] --> G["Fork gate step"] --> C{"fork=false?"}
  C -->|"yes"| A["PR-Agent step"] --> V["Verdict step"]
  C -->|"no"| S["Skip + warn"]
  G --> Q["GitHub pulls API"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split into two workflows (pull_request vs issue_comment)
  • ➕ Eliminates mixed-event complexity and reduces conditional branching
  • ➕ Can tune permissions/secrets per workflow more explicitly
  • ➖ More duplication (agent config, stamps, verdict logic)
  • ➖ Harder to keep behavior consistent across events
2. Handle slash commands via GitHub App/bot + repository_dispatch
  • ➕ Moves authorization and fork checks into a dedicated service with clearer auditing
  • ➕ Decouples runtime from Actions secrets/permissions footprint
  • ➖ Requires operating and securing a bot/service
  • ➖ Higher implementation and maintenance cost than a workflow-only fix

Recommendation: Keep the PR’s approach: querying the pulls endpoint and failing closed is the smallest, most reliable fix for the structural limitation of issue_comment payloads. The added step-level gate also composes cleanly with existing job-level checks and makes future changes (e.g., adding checkout) safer by construction.

Files changed (1) +101 / -4

Bug fix (1) +101 / -4
pr-agent.ymlAdd fork-gate for issue_comment and fix AI timeout/budget handling +101/-4

Add fork-gate for issue_comment and fix AI timeout/budget handling

• Replaces the misleading fork-related header comment with an explicit explanation of the event-payload limitation and introduces a new "fork gate" step that resolves fork status via the pulls API and fails closed. Conditions the PR-Agent step on the gate result, adds an explicit notice path when the agent is skipped, reduces CONFIG__AI_TIMEOUT to remain under the step budget, and corrects attempt-2 duration arithmetic to avoid false timeout classification when attempt 2 never started.

.github/workflows/pr-agent.yml

@gitar-bot

gitar-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your team has used its included automatic processing for this billing period (headroom scales with your seat count). You can still comment "Gitar review" to run one anytime, and automatic reviews resume on their own by September 1. Add seats for more headroom.
Learn more

Code Review ✅ Approved

Adds a fail-closed fork gate for issue comment commands and reduces AI timeout limits to fit within step budgets. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@qodo-code-review

qodo-code-review Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Grey Divider


Action required

1. GH_TOKEN passed to shell 📜 Skill insight ⛨ Security
Description
The new fork gate step injects ${{ secrets.GITHUB_TOKEN }} into a shell environment (GH_TOKEN)
and uses it to call the GitHub API via gh, which is direct raw-token usage rather than an opaque
capability grant mechanism.
Code

.github/workflows/pr-agent.yml[R115-116]

+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          EVENT_NAME: ${{ github.event_name }}
Relevance

●● Moderate

Same-file raw-token concern exists, but its historical outcome is undetermined; no decisive
acceptance precedent yet.

PR-#88

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2798870 forbids directly handling raw tokens when interacting with third-party
accounts (e.g., GitHub). The added workflow step exports ${{ secrets.GITHUB_TOKEN }} into
GH_TOKEN and then performs a GitHub API call with gh api, which uses that raw token for
authentication.

.github/workflows/pr-agent.yml[111-118]
.github/workflows/pr-agent.yml[125-126]
Skill: wave-custody

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The workflow introduces a new direct/raw GitHub token usage by exporting `${{ secrets.GITHUB_TOKEN }}` as `GH_TOKEN` for a shell step that calls `gh api`. This violates the requirement to avoid exposing raw tokens and instead use opaque capability grants.

## Issue Context
This change is in the new `fork gate (issue_comment only)` step and is part of the PR's added logic.

## Fix Focus Areas
- .github/workflows/pr-agent.yml[109-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Fork gate can fail-open ✓ Resolved 🐞 Bug ⛨ Security
Description
PR-Agent runs when steps.gate.outputs.fork is anything other than the literal string 'true',
so a missing/empty/changed output value would allow the secret-bearing step to run despite the “fail
closed” intent. This weakens the new fork protection on the issue_comment arm, where the job-level
if: cannot enforce head.repo.fork == false.
Code

.github/workflows/pr-agent.yml[R155-157]

      - name: PR-Agent (OSS qodo-merge)
        id: agent
+        if: steps.gate.outputs.fork != 'true'
Relevance

●●● Strong

The inequality is a deterministic fail-open bug directly contradicting the PR’s stated fail-closed
intent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR introduces the gate step that writes an output fork=true|false, but the PR-Agent step is
guarded by != 'true', which is not fail-closed. If the output is absent/empty/unexpected, it would
still pass the != 'true' test and run the step that exposes OPENAI_KEY.

.github/workflows/pr-agent.yml[109-138]
.github/workflows/pr-agent.yml[155-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The fork gate is meant to be fail-closed, but the PR-Agent step currently uses `if: steps.gate.outputs.fork != 'true'`, which is permissive: if the `fork` output is missing/empty/unexpected, the condition evaluates true and the PR-Agent step runs.

### Issue Context
This matters most on the `issue_comment` arm where the job-level `if:` cannot check fork status and the PR-Agent step carries `OPENAI_KEY`.

### Fix Focus Areas
- .github/workflows/pr-agent.yml[155-158]

### Suggested fix
Change the PR-Agent step condition to a strict allow-list:
- `if: steps.gate.outputs.fork == 'false'`

Optionally, make the same “only run on explicit false” pattern consistent anywhere else the gate output is used in conditions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 14 rules
✅ Skills: wave-custody
✅ REVIEW.md
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/workflows/pr-agent.yml
Comment thread .github/workflows/pr-agent.yml Outdated
@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

✅ Merged (0) · ☑ Fixed (0)

Process

  • No fixes were applied (no_fixes_applied)

…ce of a true

Review of this wave found the fail-closed gate had a fail-OPEN consumer. Two
reviewers flagged it independently, on two different repos, and they were right.

    if: steps.gate.outputs.fork != 'true'      # grants when the output is EMPTY

The gate could only fail closed if it always wrote an output. It did, on every
path — so this did not fail open today, and the implicit success() on the
consumer covers a gate that errors outright. But the safety rested on an
argument rather than on the structure, and it is the very argument this change
exists to delete: absence must not read as permission.

Two independent changes, so neither carries the invariant alone:

  - the gate now assigns a shell variable that STARTS at `true` and writes ONCE
    at the end, so no future edit adding an early exit can emit nothing;
  - the consumer requires `== 'false'`, an explicit affirmative, so an empty or
    missing output skips the agent.

Also braces both sides of the A2 subtraction in the verdict step. The bare
`ATTEMPT2_START` was CORRECT — POSIX arithmetic expansion evaluates a bare name
as a variable, verified identical (180 == 180) — but a reviewer read it as a
literal token and filed it High. An expression that reads wrong on 27 repos gets
re-filed on 27 repos, so it is normalised rather than defended.

RECEIPTS. actionlint clean; zizmor clean; shellcheck clean. The gate was driven
through all six branches plus the reviewers' no-output scenario: only a literal
`false` reaches AGENT RUNS. The verdict was re-run across all six states and is
unchanged on the five that already worked.

LIVE: wave-av/api-spec merged the previous revision and its pull_request run
executed `fork gate (issue_comment only) -> success` in production, then ran the
agent — so the gate does not wrongly refuse a legitimate same-repo PR.

Upstream: wave-av/wave-foundation-public#73. Refs wave-pen#418, wave-pen#417.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7cb72b88-4787-41fd-87e2-cc77adbc6a95)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @yakimoto, you have reached your weekly rate limit of 250000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the pr-agent workflow with a fail-closed fork check for issue-comment commands, a 300-second AI timeout that fits the step budget, and corrected verdict logic for gated skips and absent retry attempts.

Sequence diagram for the fail-closed fork gate

sequenceDiagram
    participant Comment as issue_comment
    participant Workflow as pr_agent workflow
    participant GitHub as GitHub pulls API
    participant Agent as PR-Agent

    Comment->>Workflow: Trigger slash command
    Workflow->>GitHub: gh api repos/{repo}/pulls/{number}
    GitHub-->>Workflow: .head.repo.fork
    alt fork is false
        Workflow->>Agent: Run with OPENAI_KEY
    else fork is true or response unusable
        Workflow-->>Comment: Skip and emit warning
    end
Loading

Flow diagram for the bounded AI review and verdict

flowchart LR
    A["Fork gate"] -->|fork=false| B["PR-Agent step"]
    A -->|fork=true or unknown| C["agent outcome: skipped"]
    B --> D["AI timeout: 300s"]
    D -->|within 360s step cap| E["fallback or retry classification"]
    C --> F["verdict: notice and success"]
Loading

Flow diagram for corrected retry duration classification

flowchart TD
    A["Read attempt stamps"] --> B{"Attempt 2 start exists and is > 0?"}
    B -->|yes| C["Compute A2 from end - start"]
    B -->|no| D["Set A2 = 0"]
    C --> E["Clamp negative durations"]
    D --> E
    E --> F["Select longest attempt"]
    F --> G["Classify timeout or failure"]
Loading

File-Level Changes

Change Details Files
Add a fail-closed fork gate for slash-command executions where the event payload lacks fork metadata.
  • Query the pull request endpoint for fork status on issue_comment events.
  • Default to skipping when the API response is true, empty, null, malformed, or unavailable.
  • Run the agent only when the gate output is explicitly false and report deliberate skips as notices.
.github/workflows/pr-agent.yml
Align both agent attempts with the workflow step budget.
  • Reduce CONFIG__AI_TIMEOUT from 600 seconds to 300 seconds in both environment blocks.
  • Preserve headroom for fallback models and retry classification under the 360-second step cap.
.github/workflows/pr-agent.yml
Correct verdict classification for skipped or never-started retry attempts.
  • Handle a skipped agent run as a successful gated notice rather than a failure.
  • Calculate attempt-two duration only when a valid start timestamp exists, preventing false multi-year timeout reports.
  • Retain existing classifications for successful, cancelled, real failure, and genuine timeout scenarios.
.github/workflows/pr-agent.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…mber space

Review found a SECOND concurrency collision, on a different axis from the one
this template already documents (wave-pen#386).

`issue_comment` fires for ISSUES as well as PRs, and GitHub draws both from ONE
number sequence. So a comment on Issue #30 and a `/review` on PR #30 entered the
same concurrency group. Concurrency is evaluated at WORKFLOW level, BEFORE the
job-level `if:` runs — so the Issue comment cancelled the PR review already in
flight, and was then skipped itself, having done nothing.

That is the identical shape as the #386 defect the block above exists to fix,
one axis over: a run that will not review taking the lane from the run that
would have. #386 separated the two EVENTS; it did not separate the two number
spaces inside one event.

    pull_request        PR 433   -> pr-agent-pull_request-pr-433
    issue_comment on PR  30      -> pr-agent-issue_comment-pr-30
    issue_comment on ISSUE 30    -> pr-agent-issue_comment-issue-30

The last two used to be one group. actionlint and zizmor clean.

Upstream: wave-av/wave-foundation-public#73. Refs wave-pen#418, wave-pen#417.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_88fb8546-d2bb-4002-be06-f2119d31a3bf)

A reviewer flagged the missing entry on wave-modules#41. 25 of the 28 repos in
this wave keep the same Keep-a-Changelog convention, so the entry lands in all
of them rather than only the repo whose review happened to catch it — fixing the
reported instance and leaving the class is the pattern this wave keeps undoing.

The change IS user-visible, which is why it belongs here: a maintainer's
`/review` on a fork PR is now declined with a warning instead of silently
running, so contributors on forks see different behaviour.

Refs wave-pen#418, wave-av/wave-foundation-public#73

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_5ab38202-81ac-4038-a097-02b716a51107)

@yakimoto

Copy link
Copy Markdown
Contributor Author

Working as intended, and the alternatives are worse — but the rule is pointing at something real, so here is the reasoning rather than a dismissal.

The fork gate step passes the job's own secrets.GITHUB_TOKEN as GH_TOKEN and makes exactly one read:

gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.repo.fork'

Three things bound it:

  1. It is the job's own scoped token, not a standing credential. The workflow declares permissions: {issues: write, pull-requests: write, contents: read}, so the token is minted per-run with that ceiling and expires with the job. There is no PAT and nothing stored.
  2. One read, on a public endpoint. .head.repo.fork — no write, no secret material, no user data.
  3. It crosses into the shell through env:, never ${{ }} in the script body, along with every other event-derived value in that step. That is the injection hygiene the rule's neighbouring concern is usually about, and it is satisfied.

The alternatives, and why none is an improvement:

  • actions/github-script — the idiomatic "capability" wrapper. It authenticates with the same GITHUB_TOKEN, so it is the same grant behind a different door, plus a new pinned dependency in a workflow whose whole subject this week is supply-chain surface.
  • Raw curl — same token, worse ergonomics, and hand-rolled JSON parsing in a step whose correctness is the security property.
  • Drop the token and call the endpoint unauthenticated. Tempting, since pulls/<n> is public for a public repo — and it would actively break the gate. Unauthenticated GitHub API is 60 requests/hour per IP, shared across the runner fleet. This gate fails closed: an unreadable answer is treated as a fork and skips. So a rate limit would not degrade gracefully, it would silently refuse legitimate reviews, and the failure would look exactly like the gate working. A fail-closed check must be reliably readable or it becomes a fail-always check.

So the token is what makes the fail-closed posture honest rather than decorative.

One thing I will grant: the read is the only reason this job needs an API call at all, and if GitHub ever surfaced head.repo.fork in the issue_comment payload the step could be deleted outright. That absence is measured — issues/<n>.pull_request carries exactly [diff_url, html_url, merged_at, patch_url, url] — and it is the whole reason the step exists. Tracked in wave-pen#418.

CHANGELOG conflict resolved by keeping BOTH sides: this branch keeps its
Unreleased entry for the pr-agent fork gate and AI_TIMEOUT fix, and main
2.1.3 released section is retained below it.
@codeant-ai

codeant-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8ac4f461-dd6e-410a-a203-d90679eabc0e)

CHANGELOG Unreleased conflict resolved by keeping BOTH sides: this branch
pr-agent fork-gate entry, then the Changed/Fixed blocks main picked up from
the live-gateway contract work.
@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ab089e40-38fb-402a-82db-0495f92e7ec3)

CHANGELOG Unreleased conflict resolved by keeping BOTH sides: this branch
pr-agent fork-gate Fixed entry, then the Added block main picked up from the
compose module work.
@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_974a8ecc-7b74-4d8f-b820-b6d6d8e3358e)

@cursor

cursor Bot commented Sep 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_639424d0-8a88-421c-880c-a21de86ac146)

@yakimoto
yakimoto added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 3b20d2d Sep 8, 2026
23 checks passed
@yakimoto
yakimoto deleted the fix/418-fork-gate-and-ai-timeout branch September 8, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant