chore: update tend workflows (0.1.14 → 0.1.15) (#3814)

Automated nightly regeneration of tend's workflow files.

**tend version:** 0.1.14 → 0.1.15

**Notable changes**

- **Rate-limit handling**: a maintainer can approve past the spike limit
(max-sixty/tend#874), and `tend-mention` now skips comments on
`tend-rate-limit`-labelled issues as well as `tend-outage` ones — the
same self-trigger loop guard, widened to every issue tend files about
its own health.
- **Review flow**: a push is queued behind an examined-HEAD gate instead
of cancelling the in-flight review (max-sixty/tend#903), the review is
submitted before a fix is pushed (max-sixty/tend#834), a force-push
triggers a re-review rather than trusting the re-anchored SHA
(max-sixty/tend#884), and the `/code-review` second pass is
unconditional (max-sixty/tend#937).
- **CI monitoring**: both the check poll and the `gh run rerun --failed`
poll now end terminally when the cap is hit instead of reading as done
(max-sixty/tend#876, max-sixty/tend#951), and a failed GitHub-status
probe no longer reads as "no incident" (max-sixty/tend#913).
- **Nightly**: conflicted bot PRs are test-merged locally instead of
filtered on the lazy `mergeable` field (max-sixty/tend#898), and mention
runs skip the bot's own review and a third party's content-free approval
(max-sixty/tend#916, max-sixty/tend#955).
- **`tend check`**: an unreadable ruleset bypass list is reported as
unknown rather than ungated (max-sixty/tend#825), environment names are
read one per line and addressed encoded (max-sixty/tend#879), and
`credential-environments` no longer points at the setting it rejects
(max-sixty/tend#900) — this repo currently `SKIP`s that check, so its
wording should improve here.

Full comparison:
https://github.com/max-sixty/tend/compare/0.1.14...0.1.15

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
This commit is contained in:
Worktrunk Bot
2026-08-13 05:41:10 -07:00
committed by GitHub
parent 7aba380f0c
commit c31e827cba
8 changed files with 201 additions and 35 deletions
+2 -2
View File
@@ -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,
@@ -34,7 +34,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}
+90 -16
View File
@@ -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,
@@ -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, '@worktrunk-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
@@ -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" = "worktrunk-bot" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
@@ -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 '@worktrunk-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 '@worktrunk-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" = "worktrunk-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
@@ -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" = "worktrunk-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 == "worktrunk-bot") | .id')
@@ -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 }}
@@ -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/claude-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()
+2 -2
View File
@@ -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,
@@ -34,7 +34,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}
+15 -5
View File
@@ -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,
@@ -27,7 +27,19 @@ jobs:
steps:
- name: Check for unread notifications
id: check
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
BOT_NAME: worktrunk-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`
@@ -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" = "worktrunk-bot" ] && [ "$PR_STATE" = "closed" ]; then
if [ "$PR_AUTHOR" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done
@@ -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'
@@ -138,7 +148,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}
+2 -2
View File
@@ -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,
@@ -34,7 +34,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}
+85 -3
View File
@@ -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,
@@ -15,7 +15,11 @@ jobs:
review:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
# A push mid-review queues a replacement run rather than killing the
# session: the running review folds the push in and stamps the commits
# it examined, and the gate step below lets the queued run exit without
# booting an agent when its HEAD is already covered.
cancel-in-progress: false
runs-on: ubuntu-24.04
environment:
name: tend
@@ -26,16 +30,84 @@ jobs:
actions: read
issues: write
steps:
- name: Skip when the live HEAD is already examined
id: gate
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
PR: ${{ github.event.pull_request.number }}
EVENT_ACTION: ${{ github.event.action }}
run: |
# shellcheck shell=bash
# Pre-check for tend-review: decide whether the agent needs to boot at all.
#
# The review job runs without cancel-in-progress, so a push mid-review queues
# a replacement run while the live session keeps going, folds the push in, and
# stamps each commit it examined with a `tend-review/<pr>` commit status (review
# skill, "Stamp examined HEADs"). The concurrency group holds this run until
# that session ends; by then the live HEAD is usually stamped and there is
# nothing left to do. Judged against the live PR, not the event payload — a
# queued run's payload is stale by construction.
#
# Inlined into the generated workflow (adopter repos have no copy of this
# file), so it stays self-contained: env in, GITHUB_OUTPUT out. Any write-scoped
# actor could forge the stamp to suppress a review; that actor can already
# cancel the run itself, so the merge gate — not this check — remains the
# security boundary.
#
# env: PR, EVENT_ACTION, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN
# Only `synchronize` can be a stale duplicate of an examination that already
# happened: `opened` has no prior run, and `reopened` / `ready_for_review`
# ask for a fresh pass even on a stamped commit.
if [ "$EVENT_ACTION" != "synchronize" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Fail open on API errors: a redundant agent run beats a silently skipped
# review. The parse belongs inside the guard — GitHub sometimes returns an
# HTML error page with a 200 during a blip, so a zero `gh` exit doesn't mean
# the body is JSON, and an unguarded `jq` under the run block's `bash -e`
# would fail the step (fail-closed) instead.
if ! PR_INFO=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" 2>/dev/null) \
|| ! STATE=$(echo "$PR_INFO" | jq -re '.state'); then
echo "PR #$PR fetch failed — proceeding without the pre-check"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$STATE" != "open" ]; then
echo "PR #$PR is $STATE — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# The stamp context carries the PR number: one branch can be two open PRs
# (same head, different base), and each base means a different diff, so an
# examination of one must not gate the other.
HEAD=$(echo "$PR_INFO" | jq -r '.head.sha')
STAMPED=$(gh api "repos/$GITHUB_REPOSITORY/commits/$HEAD/status?per_page=100" 2>/dev/null \
| jq --arg ctx "tend-review/$PR" \
'[.statuses[]? | select(.context == $ctx and .state == "success")] | length' \
|| echo 0)
if [ "${STAMPED:-0}" -gt 0 ]; then
echo "HEAD $HEAD already examined (tend-review/$PR) — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "should_run=true" >> "$GITHUB_OUTPUT"
# Two checkouts: `setup:` runs against the base tree, and the PR's own
# tree lands after it. Setup executes as the runner user, outside the
# containment the harness builds for the contributor's code.
- uses: actions/checkout@v7
if: steps.gate.outputs.should_run == 'true'
with:
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.TEND_BOT_TOKEN }}
- uses: ./.github/actions/claude-setup
if: steps.gate.outputs.should_run == 'true'
# GitHub only materializes refs/pull/N/merge for mergeable PRs — on
# conflicting PRs it 404s and every downstream step cascades as skipped.
@@ -44,6 +116,7 @@ jobs:
# tree.
- name: Resolve PR checkout ref
id: pr_ref
if: steps.gate.outputs.should_run == 'true'
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
PR: ${{ github.event.pull_request.number }}
@@ -55,6 +128,7 @@ jobs:
echo "::notice::refs/pull/$PR/merge unavailable (likely merge conflict); falling back to /head"
fi
- uses: actions/checkout@v7
if: steps.gate.outputs.should_run == 'true'
with:
ref: ${{ steps.pr_ref.outputs.ref }}
allow-unsafe-pr-checkout: true
@@ -63,7 +137,8 @@ jobs:
fetch-tags: true
token: ${{ secrets.TEND_BOT_TOKEN }}
- uses: max-sixty/tend/claude@0.1.14
- uses: max-sixty/tend/claude@0.1.15
if: steps.gate.outputs.should_run == 'true'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
@@ -72,3 +147,10 @@ jobs:
model: opus
prompt: >-
${{ format('/tend-ci-runner:review {0}', github.event.pull_request.number) }}
- name: Restore local setup actions for POST cleanup
if: always() && (steps.gate.outputs.should_run == 'true')
run: |
dir=.github/actions/claude-setup
git checkout "$GITHUB_SHA" -- "$dir" ||
echo "::warning::could not restore $dir from $GITHUB_SHA; POST cleanup of the local action may fail"
+3 -3
View File
@@ -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,
@@ -17,7 +17,7 @@ concurrency:
jobs:
triage:
if: github.repository_owner == 'max-sixty' && contains(github.event.issue.labels.*.name, 'tend-outage') == false
if: github.repository_owner == 'max-sixty' && 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
@@ -37,7 +37,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}
+2 -2
View File
@@ -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,
@@ -34,7 +34,7 @@ jobs:
- uses: ./.github/actions/claude-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 }}