Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# actionlint configuration.
#
# actionlint validates `runs-on:` against GitHub's list of hosted-runner labels
# and reports anything else as unknown, because a typo'd label is a job that
# queues forever rather than failing. Self-hosted labels therefore have to be
# declared here — that is the mechanism actionlint's own diagnostic points at.
#
# Without this file, `actionlint` reports an error on EVERY run in a repo that
# has the reviewer installed. That is not a harmless false positive: a linter
# that always reports something is a linter people stop reading, and the real
# findings go with it.
#
# Run it over the whole tree with:
#
# actionlint
#
# (no arguments — it discovers `.github/workflows/*.yml` and reads this file.)

self-hosted-runner:
labels:
# The maintainer's self-hosted runner for the Antigravity PR reviewer
# (`.github/workflows/antigravity-review.yml`, `runs-on: [self-hosted, agy]`).
# It is a personal machine holding the `agy` CLI's Google AI Ultra OAuth
# session, which is why that workflow cannot run on a hosted runner.
- agy
28 changes: 27 additions & 1 deletion .github/workflows/antigravity-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ jobs:
group: agy-review-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
runs-on: [self-hosted, agy]
# Bounded harder than the hosted jobs, not softer: this runs on the
# maintainer's own hardware, so a hung run holds a real machine rather than
# a disposable VM. Observed runtime is 1-3 minutes.
timeout-minutes: 30
steps:
# Check out the DEFAULT BRANCH, never the PR head. This job runs the checked-out
# `scripts/agy-review.sh` on a self-hosted runner with a token in the environment,
Expand All @@ -63,7 +67,13 @@ jobs:
# branch throughout. Consequence worth knowing: a PR that edits the reviewer or the
# style guide is reviewed by the version already on the default branch until it merges.
- name: Check out repo (for the style guide + scripts)
uses: actions/checkout@v7
# SHA-pinned, not `@v7`: this job runs on a SELF-HOSTED runner -- the
# maintainer's own machine -- so a compromised tag would execute there
# rather than in a disposable VM. Verified to be exactly what `v7`
# resolves to (v7.0.1, 2026-07-17). The trailing `# v7` is the form
# Dependabot's github-actions ecosystem reads to keep the pin current.
# Adopted FROM RustySNES and SLAC, which had it while the template did not.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.repository.default_branch }}
# Not `fetch-depth: 0`: the large-diff fallback fetches exactly the two refs it
Expand Down Expand Up @@ -91,5 +101,21 @@ jobs:
# MAX_PROMPT_BYTES: "125000" # inline/file threshold + hard backstop on the argv prompt
# STYLE_GUIDE: .github/agy-review.md # style guide, loaded if present
run: |
# The workflow and the scripts come from DIFFERENT REFS: for a `pull_request`
# event GitHub runs this YAML from the PR branch, while the checkout step
# above deliberately fetches the DEFAULT branch to get the scripts. So a
# change that adds a script file breaks its own PR -- the new workflow
# chmods a file the default branch does not have yet. Observed exactly
# once, on the PR that introduced `_agy_comment_body.sh`.
#
# The two required files are chmod'd unconditionally; anything added later
# is chmod'd only if present, so the workflow stays compatible with both
# the old and the new script set. A genuinely missing required file still
# fails loudly -- `agy-review.sh` sources it and dies -- rather than being
# papered over here.
chmod +x scripts/agy-review.sh scripts/_agy_print.sh
for opt in scripts/_agy_comment_body.sh; do
[ -f "$opt" ] && chmod +x "$opt"
done
true
scripts/agy-review.sh
75 changes: 75 additions & 0 deletions scripts/_agy_comment_body.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
#
# _agy_comment_body.sh -- the review comment's body format, as sourceable functions.
#
# Sourced by `agy-review.sh` (which uses them) and by `agy-review-selftest.sh` (which tests
# them). It exists as a separate file for one reason: `agy-review.sh` does its work at TOP
# LEVEL, so it cannot be sourced without running a review, and a test that cannot call the
# real implementation ends up reimplementing it. That failure is not hypothetical here -- the
# first version of the archive test inlined its own copy of the `awk` pipeline, so a mutation
# deleting the marker strip from the script came back NOT CAUGHT. A test that reimplements its
# subject agrees with itself forever.
#
# Defines no state and runs nothing on source.

# The sentinels delimiting the archive of earlier review rounds inside the comment body.
#
# The reviewer used to POST a fresh comment each round and DELETE the previous one. That kept
# the PR tidy and destroyed the record: a round nobody read before the next push was gone, with
# nothing on the PR indicating it had existed -- and unlike a CodeRabbit or Copilot thread, an
# unaddressed finding left no trace. Observed on a real PR where two consecutive rounds each
# raised a blocking issue and only the second survived.
#
# Now there is ONE comment per PR, edited in place: newest round on top, earlier rounds folded
# into a collapsed `<details>` below. Same tidiness, no destruction.
AGY_ARCHIVE_START='<!-- agy-archive-start -->'
AGY_ARCHIVE_END='<!-- agy-archive-end -->'

# The newest round of a comment body: everything after the marker, before the archive.
#
# `awk` matching WHOLE LINES rather than `sed` with a pattern, because a review body
# legitimately contains regex metacharacters, backslashes and HTML, and the sentinels must not
# match a line that merely mentions one.
agy_body_head() {
local marker="$1"
awk -v s="$AGY_ARCHIVE_START" '$0 == s { exit } { print }' \
| grep -v -F -x "$marker" || true
}

# The archived rounds of a comment body: everything strictly between the sentinels.
#
# The `$0 == e` test comes FIRST so a body whose archive is empty yields nothing rather than
# emitting its own end sentinel.
agy_body_archive() {
awk -v s="$AGY_ARCHIVE_START" -v e="$AGY_ARCHIVE_END" '
$0 == e { inside = 0 }
inside { print }
$0 == s { inside = 1 }' || true
}

# Delimits one archived round. Emitted by the caller ahead of each round's `<details>`.
#
# A dedicated sentinel, NOT the `<details>` tag itself. Matching `/^<details>$/` looked
# equivalent and was a data-corruption bug: a review body legitimately contains `<details>`
# blocks -- folded logs, collapsed code, another bot's summary, and the archived rounds are
# themselves nested `<details>` -- so the cut could land INSIDE a round and leave torn HTML
# plus half a review. The sentinel is an HTML comment, so it is invisible when rendered and
# cannot occur by accident in prose the way a tag can. Found in review.
AGY_ROUND_MARK='<!-- agy-round -->'

# Drop the OLDEST archived round -- everything from the last round marker onward.
#
# Exits non-zero when there is no marker to cut at, so a caller trimming to a size limit
# terminates rather than spinning. That is also the fail-safe direction for an archive written
# by an older version of this script: with no markers present, nothing is dropped and the edit
# simply fails on size, rather than the archive being silently mangled.
agy_drop_oldest_round() {
awk -v m="$AGY_ROUND_MARK" '
$0 == m { starts[++n] = NR }
{ line[NR] = $0 }
END {
cut = (n > 0) ? starts[n] : 0
if (cut == 0) exit 1
for (i = 1; i < cut; i++) print line[i]
}'
}
Loading