Skip to content

Commit 1268219

Browse files
committed
Read the Codex outcome from events instead of waiting for it
The previous version polled for Codex's thumbs-up, which held an Actions runner for up to fifteen minutes on every pull request Claude approved. The reaction turned out to be redundant: the clean path leaves a pull request comment too, so every outcome Codex has been observed to produce leaves either a review or a comment -- and both are workflow triggers. So the watching moves to its own event-driven workflow and the wait disappears. found issues review -> reviewed it and left findings clean comment -> reviewed it and found nothing never ran comment (quota) -> could not run Only the quota notice is matched on its text; anything else Codex says counts as having responded. That errs toward releasing the hold, which is the right direction when the hold is timing-only -- it never changes what is required to merge, so waiting too long costs more than releasing early. The handoff is conditional, and a handoff that doesn't happen leaves Codex nothing to respond to, so the review workflow now reports that case itself, immediately. Silence is what makes an agent undeclarable. Not covered: Codex being triggered and then saying nothing at all, which has not been observed across 100 measured runs. Covering it would mean holding a runner open again.
1 parent ef5fd1c commit 1268219

2 files changed

Lines changed: 81 additions & 72 deletions

File tree

.github/workflows/claude-code-review.yml

Lines changed: 11 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -99,82 +99,21 @@ jobs:
9999
echo "Claude review state: ${state:-<none>} — not handing off to Codex."
100100
exit 0
101101
fi
102-
# Recorded before the trigger so the watcher below only counts what
103-
# Codex does in RESPONSE to it -- a review or a quota notice left on
104-
# an earlier commit must not be read as this run's outcome.
105-
echo "since=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
106-
comment_id=$(gh api "repos/$REPO/issues/$PR/comments" \
107-
-f body="@codex review" --jq .id)
108-
echo "comment_id=$comment_id" >> "$GITHUB_OUTPUT"
109-
echo "Triggered Codex (comment $comment_id)"
102+
gh pr comment "$PR" -R "$REPO" --body "@codex review"
110103
111-
# ADAPTER: turn whatever Codex did into a submitted review.
112-
#
113-
# Codex cannot be named as a PullApprove agent directly. It emits no check
114-
# run at all, and signals a clean result with a 👍 reaction rather than a
115-
# review, so there is nothing for PullApprove to read. This step watches
116-
# those native signals and submits a review under this workflow's own
117-
# identity -- so the account to declare in CODEREVIEW.toml is
118-
# github-actions[bot], not Codex's.
119-
#
120-
# It reports on EVERY path, including "Codex never ran". That is the whole
121-
# point: a declared agent that sometimes stays silent holds human review
122-
# requests forever, which is worse than not declaring it. Roughly a third
123-
# of Codex runs never execute at all (quota), and the handoff above is
124-
# itself conditional -- so silence is the common case, not the edge case.
125-
#
126-
# `!cancelled()` rather than `always()`: a run superseded by a new push
127-
# should leave the reporting to its replacement.
128-
- name: Report the Codex outcome
129-
if: ${{ !cancelled() && steps.pr.outputs.fork == 'false' }}
104+
# When the handoff doesn't happen, Codex leaves nothing behind, so
105+
# codex-outcome.yml never fires and the account that workflow reports as
106+
# would stay silent on this pull request. A declared agent that sometimes
107+
# stays silent holds human review requests forever -- so say so here
108+
# instead, immediately and without waiting on anything.
109+
- name: Report a skipped Codex handoff
110+
if: ${{ !cancelled() && steps.codex.outputs.skipped != '' }}
130111
env:
131112
GH_TOKEN: ${{ github.token }}
132113
PR: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
133114
REPO: ${{ github.repository }}
134-
CODEX: "chatgpt-codex-connector[bot]"
135-
COMMENT_ID: ${{ steps.codex.outputs.comment_id }}
136-
SINCE: ${{ steps.codex.outputs.since }}
137-
SKIPPED: ${{ steps.codex.outputs.skipped }}
138-
TIMEOUT_SECONDS: "900"
115+
REASON: ${{ steps.codex.outputs.skipped }}
139116
run: |
140-
report() {
141-
echo "Reporting: $1"
142-
gh pr review "$PR" -R "$REPO" --comment --body "Codex second opinion — $1"
143-
exit 0
144-
}
145-
146-
[ -n "$SKIPPED" ] && report "not run: $SKIPPED."
147-
[ -z "$COMMENT_ID" ] && report "not run: the review step did not finish."
148-
149-
deadline=$(( $(date +%s) + TIMEOUT_SECONDS ))
150-
while [ "$(date +%s)" -lt "$deadline" ]; do
151-
# Findings: Codex submits a review (always COMMENTED, never a
152-
# verdict), with its findings as inline comments.
153-
if gh api --paginate --slurp "repos/$REPO/pulls/$PR/reviews" \
154-
| jq -e --arg u "$CODEX" --arg t "$SINCE" \
155-
'add | map(select(.user.login == $u and .submitted_at > $t)) | length > 0' \
156-
>/dev/null 2>&1; then
157-
report "reviewed it and left findings."
158-
fi
159-
160-
# Clean: a 👍 on the trigger comment, and no review at all. This is
161-
# the outcome that makes Codex undeclarable on its own.
162-
if gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
163-
| jq -e 'map(select(.content == "+1")) | length > 0' >/dev/null 2>&1; then
164-
report "reviewed it and found nothing."
165-
fi
166-
167-
# Never ran: it says so in a PR comment. Same channel as the clean
168-
# notice, so the reaction above has to be checked first.
169-
if gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" \
170-
| jq -e --arg u "$CODEX" --arg t "$SINCE" \
171-
'add | map(select(.user.login == $u and .created_at > $t
172-
and (.body | test("usage limits"; "i")))) | length > 0' \
173-
>/dev/null 2>&1; then
174-
report "could not run (Codex usage limits reached)."
175-
fi
176-
177-
sleep 30
178-
done
117+
gh pr review "$PR" -R "$REPO" --comment \
118+
--body "Codex second opinion — not run: $REASON."
179119
180-
report "did not respond within $((TIMEOUT_SECONDS / 60)) minutes."
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: codex-outcome
2+
3+
# ADAPTER: turn whatever Codex did into a submitted review.
4+
#
5+
# Codex cannot be named as a PullApprove agent directly. It emits no check run
6+
# at all, and its clean result is a 👍 reaction rather than a review, so there
7+
# is nothing for PullApprove to read. This watches what it *does* leave behind
8+
# and submits a review under this workflow's own identity — so the account to
9+
# declare in CODEREVIEW.toml is github-actions[bot], not Codex's.
10+
#
11+
# Every outcome Codex has been observed to produce leaves either a review or a
12+
# pull request comment, and both are workflow triggers — so this is entirely
13+
# event-driven. An earlier version polled for the 👍, which held an Actions
14+
# runner for up to fifteen minutes per pull request; the reaction turned out to
15+
# be redundant, because the clean path leaves a comment too.
16+
#
17+
# found issues review -> reviewed it and left findings
18+
# clean comment -> reviewed it and found nothing
19+
# never ran comment (quota) -> could not run
20+
#
21+
# Only the quota notice is matched on its text. Anything else Codex says is
22+
# treated as "it responded", which errs toward releasing the hold: waiting too
23+
# long costs the whole point of the feature, while releasing early costs
24+
# minutes. The `[[agents]]` hold is timing-only — it never changes what is
25+
# required to merge — so that is the right direction to be wrong in.
26+
#
27+
# Not covered: Codex being triggered and then saying nothing at all. That has
28+
# not been observed across 100 measured runs (even the quota case comments),
29+
# and covering it would mean going back to holding a runner open.
30+
31+
on:
32+
issue_comment:
33+
types: [created]
34+
pull_request_review:
35+
types: [submitted]
36+
37+
permissions:
38+
pull-requests: write
39+
40+
jobs:
41+
report:
42+
# Only Codex's own activity, and only on pull requests. `issue_comment`
43+
# fires for plain issues too, which have no review to submit.
44+
if: >-
45+
(github.event_name == 'issue_comment' &&
46+
github.event.issue.pull_request &&
47+
github.event.comment.user.login == 'chatgpt-codex-connector[bot]') ||
48+
(github.event_name == 'pull_request_review' &&
49+
github.event.review.user.login == 'chatgpt-codex-connector[bot]')
50+
runs-on: ubuntu-latest
51+
steps:
52+
- name: Report the Codex outcome
53+
env:
54+
GH_TOKEN: ${{ github.token }}
55+
PR: ${{ github.event.issue.number || github.event.pull_request.number }}
56+
REPO: ${{ github.repository }}
57+
EVENT: ${{ github.event_name }}
58+
BODY: ${{ github.event.comment.body }}
59+
run: |
60+
if [ "$EVENT" = "pull_request_review" ]; then
61+
outcome="reviewed it and left findings."
62+
elif printf '%s' "$BODY" | grep -qi "usage limits"; then
63+
outcome="could not run (Codex usage limits reached)."
64+
else
65+
outcome="reviewed it and found nothing."
66+
fi
67+
68+
echo "Reporting: $outcome"
69+
gh pr review "$PR" -R "$REPO" --comment \
70+
--body "Codex second opinion — $outcome"

0 commit comments

Comments
 (0)