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
55 changes: 45 additions & 10 deletions .github/workflows/reusable-pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ jobs:
outputs:
skip: ${{ steps.state.outputs.skip }}
agents: ${{ steps.state.outputs.agents }}
skipped: ${{ steps.state.outputs.skipped }}
round: ${{ steps.state.outputs.round }}
steps:
- name: Validate GitHub App secret configuration
Expand Down Expand Up @@ -152,7 +153,10 @@ jobs:
# (idempotent — `--force` updates color/description in place).
- name: Re-arm trigger and bootstrap labels
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
# github.token, never the app token: default-token events don't
# trigger workflows, so our own label writes can't spawn a newer
# (skipped) run that shadows this round in the PR checks view.
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
PRESSED: ${{ github.event.label.name }}
run: |
Expand Down Expand Up @@ -201,14 +205,27 @@ jobs:
echo "::error::Unknown agent '$a' (allowed: review, security, tests)"; exit 1 ;;
esac
done
echo "agents=$(printf '%s' "$AGENTS_JSON" | jq -c .)" >> "$GITHUB_OUTPUT"

gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate --jq '.[]' | jq -s '.' > /tmp/all_comments.json

ALL_CURRENT=true
gh api "repos/$GITHUB_REPOSITORY/issues/$PR/labels" --paginate --jq '.[].name' > /tmp/pr_labels.txt || : > /tmp/pr_labels.txt
has_label() { grep -qxF "$1" /tmp/pr_labels.txt; }

# Per-agent skip: an agent that already reviewed THIS head SHA and
# whose positive label still stands doesn't burn another session —
# its verdict is read from the labels. Agents that failed (or never
# ran) at this SHA DO re-run on a press, so a press doubles as a
# re-roll of open verdicts. The force label re-runs everyone, and
# removing an agent's positive label by hand forces just that one.
RUN_AGENTS='[]'
SKIPPED_AGENTS='[]'
MAX_ROUND=0
LAST_SHA=""
for a in $(printf '%s' "$AGENTS_JSON" | jq -r '.[]'); do
case "$a" in
review) POS="review:approved" ;;
security) POS="security:clear" ;;
tests) POS="tests:sufficient" ;;
esac

BODY=$(jq -r --arg m "<!-- muse:${a} sha:" '[.[] | select(.body | contains($m))] | last | .body // empty' /tmp/all_comments.json)
SHA=$(printf '%s' "$BODY" | grep -oE "<!-- muse:${a} sha:[0-9a-f]+" | grep -oE '[0-9a-f]{7,}' | head -1 || true)
ROUND=$(printf '%s' "$BODY" | grep -oE "round:[0-9]+" | head -1 | cut -d: -f2 || true)
Expand All @@ -220,25 +237,34 @@ jobs:
echo '[]' > "context/prev_${a}.json"
fi

[ "$SHA" = "$HEAD_SHA" ] || ALL_CURRENT=false
[ -n "$SHA" ] && LAST_SHA="$SHA"
[ -n "${ROUND:-}" ] && [ "$ROUND" -gt "$MAX_ROUND" ] && MAX_ROUND=$ROUND
echo " $a: last sha=${SHA:-none} round=${ROUND:-0} open=$(jq 'length' "context/prev_${a}.json")"

if [ "$FORCED" != "true" ] && [ "$SHA" = "$HEAD_SHA" ] && has_label "$POS"; then
SKIPPED_AGENTS=$(printf '%s' "$SKIPPED_AGENTS" | jq -c --arg a "$a" '. + [$a]')
echo " $a: already positive at ${SHA:0:7} ($POS stands) — skipping"
else
RUN_AGENTS=$(printf '%s' "$RUN_AGENTS" | jq -c --arg a "$a" '. + [$a]')
echo " $a: run (last sha=${SHA:-none} round=${ROUND:-0} open=$(jq 'length' "context/prev_${a}.json"))"
fi
done

if [ "$ALL_CURRENT" = "true" ] && [ "$FORCED" != "true" ]; then
if [ "$(printf '%s' "$RUN_AGENTS" | jq 'length')" = "0" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Head $HEAD_SHA already reviewed by every requested agentno-op. Add the force label to re-run."
echo "Every requested agent is already positive at $HEAD_SHAnothing to run. Use the force label to re-review."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
echo "agents=$(printf '%s' "$RUN_AGENTS" | jq -c .)" >> "$GITHUB_OUTPUT"
echo "skipped=$(printf '%s' "$SKIPPED_AGENTS" | jq -c .)" >> "$GITHUB_OUTPUT"
echo "round=$((MAX_ROUND + 1))" >> "$GITHUB_OUTPUT"
echo "last_sha=$LAST_SHA" >> "$GITHUB_OUTPUT"

- name: Mark round in progress
if: steps.state.outputs.skip != 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
# github.token, never the app token — see "Re-arm trigger" above.
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: gh pr edit "$PR" --repo "$GITHUB_REPOSITORY" --add-label "muse:reviewing"

Expand Down Expand Up @@ -606,13 +632,22 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
AGENTS_JSON: ${{ needs.prepare.outputs.agents }}
SKIPPED_JSON: ${{ needs.prepare.outputs.skipped }}
run: |
set -euo pipefail
add() { gh pr edit "$PR" --repo "$GITHUB_REPOSITORY" --add-label "$1" 2>/dev/null || true; }
remove() { gh pr edit "$PR" --repo "$GITHUB_REPOSITORY" --remove-label "$1" 2>/dev/null || true; }

ERROR=false
ALL_POSITIVE=true

# Agents skipped by prepare were skipped BECAUSE their positive
# label stands at this SHA — they count as positive and their
# labels are left untouched.
for a in $(printf '%s' "${SKIPPED_JSON:-[]}" | jq -r '.[]?'); do
echo "${a}: skipped (already positive at this SHA)"
done

for a in $(printf '%s' "$AGENTS_JSON" | jq -r '.[]'); do
case "$a" in
review) POS="review:approved"; NEG="review:needs-changes" ;;
Expand Down
11 changes: 10 additions & 1 deletion docs/muse-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ Both comments in that snippet are **load-bearing** — copy them with the code.
(everything positive) or `muse:error` (an agent failed).
5. **New commits** (`synchronize`) strip the stale outcome labels and cancel
an in-flight round. Re-pressing `muse:review` runs a **delta round**.
6. **Same-SHA presses are no-ops** — `muse:force` overrides.
6. **A press re-runs only the agents that need it.** An agent whose sticky
marker already covers the head SHA *and* whose positive label still stands
is skipped — its verdict is read from the labels, no session spent. Agents
that failed (or errored) at this SHA re-run, so a press doubles as a
re-roll of open verdicts. All agents positive at the current SHA → the
whole press is a no-op. `muse:force` re-runs everyone; removing an agent's
positive label by hand forces just that agent.

### Delta rounds (why turns aren't wasted)

Expand Down Expand Up @@ -237,6 +243,9 @@ wanted (Settings → Repository defaults → Labels).
startup failure; merge platform-tools first.
- Human discussion on the PR (minus the sticky comments) is included in the
agents' context, so pushback and decisions are visible to the next round.
- All of the loop's own label writes use `github.token`, whose events never
trigger workflows — so mid-round label changes can't spawn a newer
(skipped) run that would shadow the live round in the PR checks view.

### Inputs

Expand Down