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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 All@@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All@@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# failure would abort the whole step red. A failed fetch just means
# this cycle can't enumerate — the next scheduled cycle picks up
# anything missed — so retry briefly, then skip cleanly.
#
# On every failed attempt, log what actually came back: the plain
# fetch hides the body behind `2>/dev/null`, and a transient blip
# that recovers on retry would otherwise leave no trace of the bad
# response. `gh api -i` re-fetches with the HTTP status line +
# headers ahead of the body, so a recurring failure shows the real
# status code and body — not just jq's parse complaint. It exits
# non-zero on an error status, hence `|| true`. The extra call runs
# only on the (rare) failing attempt; the happy path is one fetch.
NOTIFS=""
for attempt in 1 2 3; do
if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then
break
fi
NOTIFS=""
echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---"
gh api notifications -i 2>&1 | head -c 1000 || true
echo
[ "$attempt" -lt 3 ] && sleep "$attempt"
done
if [ -z "$NOTIFS" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "notifications fetch failed after retries — skipping this cycle"
exit 0
fi
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- Layer B: drop notifications shadowed by recent dedicated runs ---
# Event workflows mark their own notifications read via the harness
# action's post-step on success; this sweeps the case where Claude failed
# (post-step is gated by `if: success()`) so the notification still
# gets cleared without burning Claude turns to rediscover it.
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

if [ -n "$RECENT_PRS" ]; then
for pr in $RECENT_PRS; do
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do
[ -n "$tid" ] || continue
gh api "notifications/threads/$tid" -X PATCH || true
done
done
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
fi

# --- Layer C: drop notifications on bot-authored closed PRs ---
# The bot auto-subscribes to its own PRs. After merge/close, leftover
# subscription notifications are pure noise — no action needed.
# Reuses the snapshot fetched above; marking a thread read is
# idempotent, so a stale snapshot at worst re-PATCHes a read thread.
echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do
[ -n "$tid" ] || continue
PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last')
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" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then
gh api "notifications/threads/$tid" -X PATCH || true
fi
done

# --- Layer D: count processable notifications ---
# Same-repo notifications younger than 10 minutes are deferred: a
# dedicated workflow (tend-review/mention/triage/ci-fix) is likely
# still starting up or mid-flight and hasn't posted its response yet.
# Processing them now risks duplicating work. Cross-repo notifications
# are exempt — no dedicated workflow handles them.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
# Re-fetch so the count reflects threads marked read in Layers B/C.
# Tolerate a transient failure by falling back to the pre-mutation
# snapshot — an over-count at worst spends one agent run, never fails.
if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then
REMAINING="$NOTIFS"
fi
COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length')
echo "count=$COUNT" >> "$GITHUB_OUTPUT"

if [ "$COUNT" = "0" ]; then
TOTAL=$(echo "$REMAINING" | jq 'length')
if [ "$TOTAL" = "0" ]; then
echo "All notifications handled by pre-checks — skipping"
else
echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping"
fi
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All@@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All@@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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 DownExpand Up@@ -34,7 +34,7 @@ jobs:

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

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