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
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init
#
# Do not edit this file directly — it will be overwritten on regeneration.
# To customize behavior, edit the relevant skill (for example,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
106 changes: 90 additions & 16 deletions .github/workflows/tend-mention.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init
#
# Do not edit this file directly — it will be overwritten on regeneration.
# To customize behavior, edit the relevant skill (for example,
Expand Down Expand Up @@ -90,20 +90,20 @@ jobs:
-f "client_payload[id]=${{ github.event.review.id || github.event.comment.id }}"

verify:
# Skip comments on `tend-outage` issues: the action's Report-failure step
# auto-comments on those when Claude invocation fails, and without this
# guard those comments re-trigger tend-mention during a persistent outage
# (e.g. Anthropic 401), producing a self-sustaining ~1 run/minute loop
# until the outage clears. The prompt's self-loop guard can't help here
# because the model never executes — the action fails before Claude starts.
# A relayed review enters as `repository_dispatch` and is judged in the
# check step below, against the record the API holds.
# Skip comments on the issues tend files about its own health: the action
# auto-comments on those when a run fails or is refused, and without this
# guard those comments re-trigger tend-mention, producing a
# self-sustaining ~1 run/minute loop until the underlying condition
# clears. The prompt's self-loop guard can't help here because the model
# never executes — the action fails before Claude starts. A relayed review
# enters as `repository_dispatch` and is judged in the check step below,
# against the record the API holds.
if: |
github.event_name == 'repository_dispatch' ||
(github.event_name == 'issues' &&
contains(github.event.issue.body, '@prql-bot')) ||
(github.event_name == 'issue_comment' &&
!contains(github.event.issue.labels.*.name, 'tend-outage'))
contains(github.event.issue.labels.*.name, 'tend-outage') == false && contains(github.event.issue.labels.*.name, 'tend-rate-limit') == false)
runs-on: ubuntu-24.04
environment:
name: tend
Expand Down Expand Up @@ -195,8 +195,11 @@ jobs:
# comments on purpose: the pull_request_review *submission* kind is
# deliberately left out, since a review the bot leaves on its own PR
# is its reviewer role (the prompt is told to action it), not a
# self-loop. The only self-review skip is the terminal empty-body
# APPROVED gate below.
# self-loop. The three self-review skips below are author-keyed, but
# each is also narrowed to a case that leaves this run nothing to do:
# the terminal empty-body APPROVED gate, the synthetic reply
# container, and a bot review on a PR the bot did not author. None of
# them licenses a blanket self-review skip.
if { [ "$KIND" = "issue_comment" ] || [ "$KIND" = "pull_request_review_comment" ]; } \
&& [ "$COMMENT_AUTHOR" = "prql-bot" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -242,14 +245,58 @@ jobs:
# A review's record includes review.body (checked above) but NOT the
# bodies of the inline comments attached to the review. Fetch them
# so a first-contact @-mention inside an inline comment is detected
# on PRs where the bot has no prior engagement.
# on PRs where the bot has no prior engagement. One object per line,
# so `--paginate` concatenates pages instead of reducing within one.
# Keep the `{body, in_reply_to_id}` construction: `in_reply_to_id` is
# an *optional* property, absent rather than null on a fresh comment,
# and building the object normalizes absent to null so the `== null`
# select below counts both shapes. A bare `.in_reply_to_id` stream
# would emit nothing for a fresh comment and skip every container.
if [ "$KIND" = "pull_request_review" ]; then
if gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \
--jq '.[].body' | grep -qF '@prql-bot'; then
INLINE=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \
--jq '.[] | {body, in_reply_to_id}')

if printf '%s\n' "$INLINE" | jq -r '.body' | grep -qF '@prql-bot'; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=mention" >> "$GITHUB_OUTPUT"
exit 0
fi

# Replying to a review thread wraps the reply in a synthetic
# zero-body COMMENTED review, so the bot's own inline reply arrives
# here as a `pull_request_review` submission as well as the
# `pull_request_review_comment` the skip above already drops. Same
# artifact, second event path: without this the engagement heuristic
# below sees a bot-authored PR (or BOT_REVIEWS > 0) and spins up a
# full handle job that the prompt's self-loop guard then exits.
# Narrower than the comment skip on purpose — a real review the bot
# submits with inline comments and no body carries actionable signal,
# so require that *every* inline comment be a reply
# (`in_reply_to_id` set). A container with any fresh inline comment
# still fires.
if [ "$REVIEW_AUTHOR" = "prql-bot" ] \
&& [ -z "$COMMENT_BODY" ] \
&& [ "$(printf '%s\n' "$INLINE" | jq -s '[.[] | select(.in_reply_to_id == null)] | length')" = "0" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Somebody else's content-free approval is terminal too. The gate
# above is author-keyed, so a human's bare APPROVED falls through to
# the PR-author short-circuit (on a bot-authored PR) or to
# BOT_REVIEWS (on one the bot has reviewed) and starts a session
# whose only possible outcome is a silent exit: an approval with no
# body and no inline comments asks for nothing, and the bot cannot
# merge on its own. Unlike the bot-authored gates this one requires
# `$INLINE` to be empty rather than reply-only — an approval whose
# nits live inline is a request to the PR's author, which on a
# bot-authored PR is a role the bot has to act in.
if [ "$REVIEW_STATE" = "approved" ] \
&& [ -z "$COMMENT_BODY" ] \
&& [ -z "$INLINE" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi

# Non-mention: check bot engagement
Expand Down Expand Up @@ -290,6 +337,26 @@ jobs:
echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0
fi

# A review the bot leaves on someone else's PR leaves this run
# nothing to do: whatever the review warranted, the tend-review
# session that submitted it has already done — left the findings for
# a human author to act on (pushing to their branch unbidden is
# barred by conduct rules), or, on a dependency-bot PR where no
# author will act, pushed the fix itself. The BOT_REVIEWS heuristic
# below counts this very review, so without this gate the session
# always starts and always exits silently. Keyed on author alone —
# the APPROVED + empty-body gate above is the same shape narrowed to
# its one terminal leg, and reusing those clauses here would let every
# bodied COMMENTED review through. Placement carries the rest of the
# design: *after* the PR_AUTHOR short-circuit, which has already
# exited when the PR is the bot's own, so the reviewer-to-author
# handoff on a bot PR still fires; *after* the body and inline
# @-mention scans, so an explicit summons still wins.
if [ "$KIND" = "pull_request_review" ] \
&& [ "$REVIEW_AUTHOR" = "prql-bot" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi

# Captured, not counted — see the note on the issue-comment lookup above.
BOT_REVIEWS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \
--jq '.[] | select(.user.login == "prql-bot") | .id')
Expand Down Expand Up @@ -391,7 +458,7 @@ jobs:
# the API record — the dispatch payload never carries one to spoof.
EVENT_TS: ${{ github.event.comment.updated_at || needs.verify.outputs.ts || github.event.issue.updated_at }}

- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down Expand Up @@ -420,6 +487,13 @@ jobs:
|| format('A user commented on an issue/PR where you previously participated ({0}). Read the full context. Only respond if the comment is directed at you, asks a question you can help with, or requests changes you can make. If the conversation is between other participants, exit silently.', github.event.comment.html_url)
}}

- name: Restore local setup actions for POST cleanup
if: always()
run: |
dir=.github/actions/tend-setup
git checkout "$GITHUB_SHA" -- "$dir" ||
echo "::warning::could not restore $dir from $GITHUB_SHA; POST cleanup of the local action may fail"

- name: Remove eyes reaction
if: |
always()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init
#
# Do not edit this file directly — it will be overwritten on regeneration.
# To customize behavior, edit the relevant skill (for example,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
20 changes: 15 additions & 5 deletions .github/workflows/tend-notifications.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init
#
# Do not edit this file directly — it will be overwritten on regeneration.
# To customize behavior, edit the relevant skill (for example,
Expand Down Expand Up @@ -27,7 +27,19 @@ jobs:
steps:
- name: Check for unread notifications
id: check
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
BOT_NAME: prql-bot
run: |
# shellcheck shell=bash
# Pre-check for tend-notifications: decide whether the agent needs to boot at
# all, and clear inbox noise no agent run is needed for.
#
# Inlined into the generated workflow (adopter repos have no copy of this
# file), so it stays self-contained: env in, GITHUB_OUTPUT out.
#
# env: BOT_NAME, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

# Fetch notifications once, tolerating transient non-JSON responses.
# GitHub occasionally returns an HTML error page (even with a 200)
# during a brief API blip; under `bash -e` an untolerated `gh api`
Expand Down Expand Up @@ -95,7 +107,7 @@ jobs:
PR_INFO=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUM" --jq '"\(.user.login) \(.state)"' 2>/dev/null) || continue
PR_AUTHOR=${PR_INFO%% *}
PR_STATE=${PR_INFO##* }
if [ "$PR_AUTHOR" = "prql-bot" ] && [ "$PR_STATE" = "closed" ]; then
if [ "$PR_AUTHOR" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done
Expand Down Expand Up @@ -125,8 +137,6 @@ jobs:
else
echo "$COUNT processable notification(s) — proceeding"
fi
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}

- uses: actions/checkout@v7
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
Expand All @@ -138,7 +148,7 @@ jobs:

- uses: ./.github/actions/tend-setup
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init
#
# Do not edit this file directly — it will be overwritten on regeneration.
# To customize behavior, edit the relevant skill (for example,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading