pyauto-arxiv-papers #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: pyauto-arxiv-papers | |
| # A daily digest of new strong-lensing papers from arXiv, summarised by Claude | |
| # and posted to Slack #papers. Sibling of morning_status.yml (the commit | |
| # digest) — same three-step shape: fetch data → Claude summarises into | |
| # slack_payload.json → POST to a Slack incoming webhook. The only new secret is | |
| # the #papers webhook; the Claude step reuses CLAUDE_CODE_OAUTH_TOKEN (the | |
| # Claude subscription OAuth token already set for the commit digest — no | |
| # Anthropic API key, no metered billing). | |
| # | |
| # Scope (user, 2026-07-10): STRONG LENSING ONLY — astro-ph.CO / astro-ph.GA | |
| # papers whose abstract/title names strong lensing (tightest signal). Format: | |
| # CURATED — 1–3 AI-chosen highlights, each with an author byline and a | |
| # 4–5 sentence summary, then a compact list of the rest. | |
| # Cadence: weekday mornings. Empty days STILL post a short "no new strong- | |
| # lensing papers today" heartbeat (user, 2026-07-13) — so silence in #papers | |
| # always means a broken run, never a genuinely empty one. | |
| # | |
| # Timing: arXiv announces new astro-ph at 20:00 US Eastern, Sun–Thu, which is | |
| # ~01:00 UK (00:00 UTC in summer / 01:00 UTC in winter) — the papers are live in | |
| # the small hours, NOT the morning. GitHub cron only ever jitters *later* (0–3 h | |
| # under load), never earlier, so there is no way to pin an exact delivery time; | |
| # the nominal is set early enough that even a worst-case slip still lands before | |
| # a UK scientist wakes. Nominal 02:00 UTC (03:00 BST / 02:00 GMT) → worst case | |
| # ~05:00–06:00 UK, so the digest is already waiting in #papers over morning | |
| # coffee, ~2 h after the overnight announcement. Mon–Fri only: arXiv does not | |
| # announce at weekends; the Tuesday run's band covers Friday→Monday in one | |
| # 3-day sweep. | |
| # | |
| # Window: each run takes the announcement band that just became searchable — | |
| # arxiv_fetch.py derives it from the run time against arXiv's schedule (20:00 ET | |
| # announcements, 14:00 ET deadlines). Consecutive runs are disjoint and gapless | |
| # with no cross-run state, and the band is stable for ~22 h, so cron slip cannot | |
| # move it. Do NOT reintroduce a rolling "last N hours" window: papers are only | |
| # searchable once announced, 1–3 days after submission, so a submission-anchored | |
| # window drops them permanently (PyAutoMind#79). | |
| on: | |
| schedule: | |
| - cron: "0 2 * * 1-5" | |
| workflow_dispatch: | |
| inputs: | |
| lookback_hours: | |
| description: "Rolling look-back window (hours) instead of the announcement band — for a manual test-fire or a backfill (e.g. 168 to sweep a week). Blank = the normal announcement band." | |
| required: false | |
| default: "" | |
| permissions: | |
| contents: read | |
| # claude-code-action@v1 needs OIDC to mint a short-lived GitHub token at | |
| # startup (setupGitHubToken → getOidcToken); without it the action exits | |
| # before reaching the prompt. Same requirement as morning_status.yml. | |
| id-token: write | |
| jobs: | |
| arxiv-papers: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # claude-code-action runs `git config` in the workspace at startup and | |
| # dies with "fatal: not in a git directory" if nothing is checked out — | |
| # so the checkout is load-bearing even though the digest only reads the | |
| # arXiv API. (Also gives the fetch step its committed helper script.) | |
| - uses: actions/checkout@v4 | |
| # Cheap guard on the band maths (no network). It fires before the fetch so | |
| # a DST or schedule regression fails loudly here rather than silently | |
| # posting an empty digest. | |
| - name: Self-test the announcement-band window | |
| run: python3 .github/scripts/arxiv_fetch.py --selftest | |
| - name: Fetch newly announced strong-lensing papers from arXiv | |
| id: fetch | |
| run: | | |
| set -euo pipefail | |
| # No window logic here: arxiv_fetch.py derives the announcement band | |
| # from the run time. A manual dispatch may override it with a rolling | |
| # look-back (test-fire, or a backfill), in which case we pass it | |
| # through; blank means the band. | |
| override="${{ github.event.inputs.lookback_hours }}" | |
| if [ -n "$override" ]; then | |
| export LOOKBACK_HOURS="$override" | |
| fi | |
| export UK_DATE="$(TZ=Europe/London date '+%Y-%m-%d (%a)')" | |
| python3 .github/scripts/arxiv_fetch.py | |
| count=$(python3 -c "import json;print(json.load(open('arxiv_papers.json'))['count'])") | |
| mode=$(python3 -c "import json;print(json.load(open('arxiv_papers.json'))['mode'])") | |
| echo "count=$count" >> "$GITHUB_OUTPUT" | |
| echo "Fetched $count strong-lensing paper(s) (window mode: $mode)." | |
| - name: Build the "no new papers" notice | |
| # A genuinely empty day (zero keyword hits). We skip Claude entirely to | |
| # spend no subscription usage, but still write the heartbeat payload so | |
| # the POST step below has something to send. uk_date is read from | |
| # arxiv_papers.json, which the fetch always writes even at count=0. | |
| if: ${{ steps.fetch.outputs.count == '0' }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json | |
| uk_date = json.load(open("arxiv_papers.json"))["uk_date"] | |
| title = f"No new strong-lensing papers — {uk_date}" | |
| payload = { | |
| "text": title, | |
| "blocks": [ | |
| {"type": "header", | |
| "text": {"type": "plain_text", "text": title}}, | |
| {"type": "section", | |
| "text": {"type": "mrkdwn", | |
| "text": "No new strong-lensing papers on arXiv today. :telescope:"}}, | |
| ], | |
| } | |
| with open("slack_payload.json", "w") as f: | |
| json.dump(payload, f, indent=2) | |
| print(f"Wrote no-papers heartbeat for {uk_date}.") | |
| PY | |
| - name: Summarise papers with Claude | |
| # Skip Claude on empty days to spend zero subscription usage — the | |
| # heartbeat above already wrote the notice. This runs only when there is | |
| # something to summarise. | |
| if: ${{ steps.fetch.outputs.count != '0' }} | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| # Claude subscription OAuth token — NOT an API key. Reused from the | |
| # commit digest; a once-daily summary of a handful of abstracts is | |
| # negligible against the subscription's usage allowance. | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| # Default-hidden output masks real failures (an expired token reads as | |
| # "slack_payload.json missing" in the POST step) — surface everything. | |
| show_full_output: true | |
| # Default permission mode denies all writes, so Claude could summarise | |
| # but not save slack_payload.json. Allow only Write; the runner's | |
| # GITHUB_TOKEN is contents:read, so nothing here can leave the runner | |
| # except via the POST step. | |
| claude_args: '--allowedTools "Write"' | |
| prompt: | | |
| Read `arxiv_papers.json` in this directory. It has shape: | |
| { "uk_date": "YYYY-MM-DD (Mon)", | |
| "mode": "announcement-band" | "lookback", | |
| "since": "<ISO-UTC>", | |
| "until": "<ISO-UTC>", | |
| "count": <int>, | |
| "papers": [ { "title": "<title>", | |
| "authors": ["<name>", ...], | |
| "abstract": "<full abstract>", | |
| "url": "https://arxiv.org/abs/XXXX.XXXXXvN", | |
| "primary_category": "astro-ph.CO", | |
| "published": "<ISO-UTC submission date>" }, ... ] } | |
| Each paper is a new strong-gravitational-lensing paper announced on | |
| arXiv in the last day (astro-ph.CO / astro-ph.GA). | |
| Audience: strong-lensing researchers (working astronomers) reading | |
| #papers over their morning coffee. Tone: concise, scientific, | |
| plain — say what the paper is *about* and why it might matter, not | |
| marketing. | |
| First, DROP anything that is clearly not genuinely about strong | |
| gravitational lensing despite matching the keyword search (e.g. a | |
| weak-lensing or microlensing paper that merely mentions the phrase | |
| "strong lensing" in passing, or a paper where lensing is incidental). | |
| Use the abstract to judge. If that leaves nothing, follow the | |
| empty-day rule below. | |
| Then produce a Slack post: | |
| - HIGHLIGHTS: pick the 1–3 most notable surviving papers. For each, | |
| write a 4–5 sentence plain-language summary (what they did + the | |
| method / data used + the headline result + why it's interesting | |
| or any caveats). Lead each with the title in bold and a link, | |
| then a byline line naming the authors: list the first up to 5 | |
| authors, followed by "et al." if there are more than 5. | |
| - THE REST: a compact bulleted list of the remaining surviving | |
| papers, one line each: `• <title> — <first author> et al. (<link>)`. | |
| (If ≤3 papers total, everything is a highlight and this list is | |
| omitted.) | |
| Convert to Slack mrkdwn: links are `<url|text>`, bold is `*text*`. | |
| Write `slack_payload.json` in the repo root with this exact shape: | |
| { | |
| "text": "New strong-lensing papers — <uk_date>", | |
| "blocks": [ | |
| { "type": "header", | |
| "text": { "type": "plain_text", | |
| "text": "New strong-lensing papers — <uk_date>" } }, | |
| { "type": "section", | |
| "text": { "type": "mrkdwn", | |
| "text": "<intro: e.g. '*N* new strong-lensing paper(s) on arXiv today.'>" } }, | |
| { "type": "section", | |
| "text": { "type": "mrkdwn", | |
| "text": "*<title>* (<link>)\n_<authors: first up to 5, then 'et al.' if more>_\n<4–5 sentence summary>" } } | |
| ] | |
| } | |
| Add one section block per highlight, then (if any) a final section | |
| block holding the "*More:*" bulleted list of the rest. | |
| IF, after dropping off-topic papers, NOTHING genuinely about strong | |
| lensing remains, do NOT omit the file — instead write | |
| `slack_payload.json` with this exact "no new papers" notice (an empty | |
| day still gets a post, so silence in #papers always signals a broken | |
| run, never an empty one): | |
| { | |
| "text": "No new strong-lensing papers — <uk_date>", | |
| "blocks": [ | |
| { "type": "header", | |
| "text": { "type": "plain_text", | |
| "text": "No new strong-lensing papers — <uk_date>" } }, | |
| { "type": "section", | |
| "text": { "type": "mrkdwn", | |
| "text": "No new strong-lensing papers on arXiv today. :telescope:" } } | |
| ] | |
| } | |
| Do not POST anything yourself, do not commit, do not modify any | |
| other file. | |
| - name: POST to Slack | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.PYAUTO_PAPERS_WEBHOOK_URL }} | |
| run: | | |
| # Every path now writes slack_payload.json: the count=0 step writes the | |
| # "no new papers" heartbeat, and the Claude step writes either a digest | |
| # or that same notice. A MISSING file here is therefore no longer an | |
| # empty day — it means the Claude step failed silently (e.g. an expired | |
| # CLAUDE_CODE_OAUTH_TOKEN, the failure mode show_full_output guards | |
| # against). Fail loudly rather than masquerade as "nothing found". | |
| if [ ! -s slack_payload.json ]; then | |
| echo "::error::slack_payload.json missing — the Claude summarise step failed to write it (check for an expired CLAUDE_CODE_OAUTH_TOKEN). Empty days write the no-papers notice, so this is a real failure, not a quiet day." | |
| exit 1 | |
| fi | |
| if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then | |
| echo "::error::PYAUTO_PAPERS_WEBHOOK_URL not set — create the #papers Slack incoming webhook and add it as a repo secret." | |
| exit 1 | |
| fi | |
| curl -sS -X POST \ | |
| -H "Content-Type: application/json" \ | |
| --data @slack_payload.json \ | |
| --fail-with-body \ | |
| "$SLACK_WEBHOOK_URL" |