Skip to content

Queue filing

Queue filing #21

Workflow file for this run

name: Queue filing
# The dashboard's 📥 `queue-intake` / 📑 `queue-cite` buttons open prefilled
# issues — from the reading queue or from either suggestion tier
# (`arxiv-inbox.md`, `arxiv-interests.md`), which the issue's `file:` names;
# this workflow has Claude file the named paper — bibliography entry,
# wiki sources section (minimal for cite, full stub for intake, the human's
# notes folded in), DONE-mark on the queue line — and opens a reviewable PR
# that closes the issue. Merge stays human.
#
# Two doors. `labeled` is the dashboard path: GitHub fires it both for labels
# applied at creation and for labels added later, and using it alone avoids
# the double-run an `opened`+`labeled` pair would cause. `workflow_dispatch`
# on an issue number is the hand-over from queue_actions.yml for an issue
# whose label the issue form dropped (a `queue intake: …` title with no
# label at all — it happens on phone submissions), and the door the nightly
# queue_sweep.yml re-dispatches through. Only owner/member/collaborator-
# authored issues act (public repo — same gate as queue_actions.yml), and
# only triage+ users can apply labels at all; the dispatch door re-checks
# the association from the API, since it has no event to read it from.
#
# The PR is created with the runner's GITHUB_TOKEN, so `pull_request`
# workflows (validate.yml) do NOT run on it — `make validate` + pytest run
# here as the gate instead, before anything is pushed.
#
# **The PR needs one repo setting.** GITHUB_TOKEN may open a PR only when
# "Allow GitHub Actions to create and approve pull requests" is on
# (Settings → Actions → General → Workflow permissions). Off, the filing
# still lands on its `queue-filing/issue-<n>` branch, gated, and the issue
# gets a one-tap compare link instead of a PR — but nothing reaches `main`
# until a human opens and merges it, and the board's *Filings awaiting
# merge* section is where those branches wait. Three did, unnoticed, between
# 2026-09-01 and 2026-09-10; the setting is the fix.
#
# A second tap on the same paper does not file it twice: a filing branch
# that already exists for this issue, or for another open issue naming the
# same paper, is answered with its compare link and the run stops clean.
#
# Claude runs with tools scoped to workspace edits plus arXiv lookups; the
# deterministic steps below stage explicit paths only — never `git add -A`.
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to file — the hand-over and sweep door"
required: true
type: string
# contents: write → push the filing branch; pull-requests/issues: write → the
# PR + failure comment; id-token: write → claude-code-action's OIDC startup
# (same requirement as PyAutoMind's arxiv_papers.yml).
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
concurrency:
group: queue-filing-${{ github.event.issue.number || inputs.issue }}
cancel-in-progress: false
jobs:
file-paper:
if: >-
github.event_name == 'workflow_dispatch' ||
(contains(fromJSON('["queue-intake", "queue-cite"]'),
github.event.label.name) &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
github.event.issue.author_association))
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
# gh needs explicit repo context in the pre-checkout secret-guard step.
GH_REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number || inputs.issue }}
steps:
- name: Require the Claude OAuth token
env:
TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "::error::CLAUDE_CODE_OAUTH_TOKEN is not set on this repo — add it under Settings → Secrets and variables → Actions to enable queue filing."
gh issue comment "$ISSUE" --body \
"Queue filing is not enabled yet: the CLAUDE_CODE_OAUTH_TOKEN secret is missing on this repo (Settings → Secrets and variables → Actions). File this paper from a Claude Code session instead, or add the secret and re-apply the label."
exit 1
fi
- uses: actions/checkout@v4
# The board tests import the family look from the Brain
# (PyAutoBrain/board/_theme.py — moved there in dcd1e2c, which added
# this step to validate.yml and knowledge_board.yml but not here).
# Without it the gate's pytest — and Claude's own in-prompt gate, which
# is told to fix its own mistakes until green but cannot fix a missing
# checkout — dies on 30 tests/test_board.py errors after the filing
# work is already done. So it lands before the Claude step, not just
# before the gate.
- uses: actions/checkout@v4
with:
repository: PyAutoLabs/PyAutoBrain
path: PyAutoBrain
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# The gate below (and Claude's own in-sandbox check) runs pytest; the
# bare runner doesn't ship it — its absence failed the first live run.
- name: Install the test runner
run: pip install pytest
# Read the issue back from the API rather than the event: the dispatch
# door has no event payload, and one code path for both doors is the
# point. `label` is the filing label the issue carries, or the one its
# `queue intake:` / `queue cite:` title says the form dropped.
- name: Resolve the request
id: resolve
run: |
issue_json=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE}")
state=$(jq -r '.state' <<<"$issue_json")
assoc=$(jq -r '.author_association' <<<"$issue_json")
title=$(jq -r '.title' <<<"$issue_json")
labels=$(jq -r '[.labels[].name] | join(",")' <<<"$issue_json")
# env, never inline interpolation — issue text is untrusted input.
jq -r '.body // ""' <<<"$issue_json" > /tmp/issue_body.txt
printf '%s' "$title" > /tmp/issue_title.txt
skip=no
if [ "$state" != "open" ]; then
echo "issue #${ISSUE} is ${state} — nothing to file"; skip=yes
fi
case "$assoc" in
OWNER|MEMBER|COLLABORATOR) ;;
*) echo "issue #${ISSUE} author is ${assoc} — not acting"; skip=yes ;;
esac
label="${{ github.event.label.name }}"
if [ -z "$label" ]; then
case ",$labels," in
*,queue-intake,*) label=queue-intake ;;
*,queue-cite,*) label=queue-cite ;;
esac
fi
if [ -z "$label" ]; then
case "$title" in
"queue intake:"*) label=queue-intake ;;
"queue cite:"*) label=queue-cite ;;
esac
if [ -n "$label" ]; then
gh issue edit "$ISSUE" --add-label "$label" || true
echo "issue #${ISSUE} carried no label — read '${label}' from its title"
fi
fi
if [ -z "$label" ]; then
echo "issue #${ISSUE} is not a filing request (title: ${title}) — nothing to do"
skip=yes
fi
printf '%s' "$label" > /tmp/issue_label.txt
echo "label=${label}" >> "$GITHUB_OUTPUT"
echo "skip=${skip}" >> "$GITHUB_OUTPUT"
# Idempotence for re-taps and re-dispatches: a filing that already
# reached a branch is answered with where it is, not filed again (a
# second push to the same branch would not fast-forward anyway, and a
# second branch for the same paper is a merge conflict waiting to
# happen). The sweep treats the comment as "touched" and leaves it.
- name: Refuse a duplicate filing
id: dup
if: steps.resolve.outputs.skip != 'yes'
run: |
skip=no
compare="${GITHUB_SERVER_URL}/${GH_REPO}/compare/main...queue-filing/issue-"
existing=$(git ls-remote --heads origin 'refs/heads/queue-filing/*' \
| sed -n 's#.*refs/heads/queue-filing/issue-\([0-9]*\)$#\1#p')
if grep -qx "$ISSUE" <<<"$existing"; then
gh issue comment "$ISSUE" --body \
"This paper is already filed on branch \`queue-filing/issue-${ISSUE}\` — nothing was filed again. Open its PR with one tap: ${compare}${ISSUE}?expand=1 — merging it closes this issue."
skip=yes
else
title=$(cat /tmp/issue_title.txt)
paper="${title#queue intake: }"; paper="${paper#queue cite: }"
for n in $existing; do
other=$(gh api "repos/${GH_REPO}/issues/${n}" --jq '.title' 2>/dev/null || true)
o_paper="${other#queue intake: }"; o_paper="${o_paper#queue cite: }"
if [ -n "$paper" ] && [ "$paper" = "$o_paper" ]; then
gh issue comment "$ISSUE" --body \
"This paper is already filed on branch \`queue-filing/issue-${n}\` (from #${n}) — nothing was filed again. Open that PR with one tap: ${compare}${n}?expand=1 — then close this issue."
skip=yes
break
fi
done
fi
echo "skip=${skip}" >> "$GITHUB_OUTPUT"
- name: File the paper with Claude
if: steps.resolve.outputs.skip != 'yes' && steps.dup.outputs.skip != 'yes'
uses: anthropics/claude-code-action@v1
with:
# Claude subscription OAuth token — NOT an API key (the org pattern
# from PyAutoMind arxiv_papers.yml / morning_status.yml).
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Default-hidden output masks real failures — surface everything.
show_full_output: true
# Workspace edits + the repo's own gates + arXiv metadata lookups.
# No git/gh: the deterministic steps below own branch/commit/PR.
claude_args: '--allowedTools "Read,Glob,Grep,Edit,Write,Bash(make:*),Bash(python:*),Bash(python3:*),WebFetch(domain:arxiv.org),WebFetch(domain:export.arxiv.org)"'
prompt: |
You are filing one paper into this PyAutoMemory checkout.
Read /tmp/issue_body.txt (a dashboard queue request). Extract:
- `file:` — which file the line lives in: `reading-queue.md`
(already queued to read), or one of the two overnight suggestion
tiers, NOT yet in the queue — `arxiv-inbox.md` (strong lensing)
and `arxiv-interests.md` (everything else). Absent means
reading-queue.md.
- `section:` — the reading-queue section header,
- `line:` — the exact line as written. A suggestion-tier line is
prefixed `<YYYY-MM-DD> — `, and an arxiv-interests.md one may
carry a further `[Topic] ` after that date. Neither is part of
the title: strip both before using the title anywhere. (The
topic is the same value as `section:`, kept on the line so the
file round-trips on its own.)
- `notes:` — the human's free-text notes; treat as absent if it
still holds the "(optional — replace this…)" placeholder.
Read /tmp/issue_label.txt: `queue-intake` (full filing) or
`queue-cite` (minimal, citeable-only).
Follow this repo's documented workflow — bibliography/README.md
"Adding a paper" and wiki/CLAUDE.md's schema:
1. Identify the paper. Use the arXiv ref if the line has one;
otherwise search arXiv by title (WebFetch against arxiv.org /
export.arxiv.org only). Get authoritative metadata: authors,
year, arXiv id, journal if published.
2. Add the canonical BibTeX entry to the bibliography (the single
canonical .bib file per bibliography/README.md). Follow the
README's key convention exactly.
3. Wiki sources page: pick the sub-wiki whose domain matches the
section (e.g. "Strong Lensing" → wiki/lensing/); check the
sub-wiki index if unsure. In the matching sources/*.md page:
- queue-cite → add a MINIMAL section: heading, the
`**Canonical BibTeX key:**` marker with the new key, and the
human's notes (lightly cleaned up) — nothing deeper.
- queue-intake → add a full stub section per wiki/CLAUDE.md,
folding the notes into the summary.
4. Record it as read, which depends on `file:`:
- `reading-queue.md` → prefix the exact `line:` inside its
`## section` with `DONE <today's date> — ` (never delete it).
- `arxiv-inbox.md` or `arxiv-interests.md` → there is no queue
line yet. Append `DONE <today's date> — <title>[ — <ref>]` to
the end of the `## section` in reading-queue.md (the paper is
being filed now, so it enters the queue already read), then
delete the line from whichever of those two files `file:`
names. A suggestion line is not reading history, so deleting
it is correct — the DONE line it becomes is the history.
- The line may have MOVED since the request was written: an
earlier ➕ tap moves a suggestion into reading-queue.md
unread, and the nightly sweep lapses old inbox lines. So if
`file:` names a suggestion tier and the line is not there,
look for the same paper (by arXiv ref first, then title) in
reading-queue.md: an unread line there is DONE-marked in
place, exactly as the `reading-queue.md` case above. If it
is already DONE-marked anywhere in reading-queue.md, the
paper has been filed before — stop and report that, without
editing anything.
5. Run `make validate` and `python -m pytest tests/ -q`; fix your
own filing mistakes until both are green.
Touch ONLY bibliography/, wiki/, reading-queue.md,
arxiv-inbox.md and arxiv-interests.md. Do not
commit — the workflow handles git. If the paper cannot be
identified confidently, or its line cannot be found in any of
the three files, write the reason to /tmp/filing_error.txt and
stop without editing anything.
- name: Abort cleanly if Claude could not identify the paper
if: steps.resolve.outputs.skip != 'yes' && steps.dup.outputs.skip != 'yes'
run: |
if [ -f /tmp/filing_error.txt ]; then
gh issue comment "$ISSUE" --body-file /tmp/filing_error.txt
echo "::error::filing aborted — reason posted to the issue"
exit 1
fi
- name: Gate the filing (validate + tests)
if: steps.resolve.outputs.skip != 'yes' && steps.dup.outputs.skip != 'yes'
run: |
make validate
python -m pytest tests/ -q
- name: Branch, commit, push, open the PR (explicit paths only)
if: steps.resolve.outputs.skip != 'yes' && steps.dup.outputs.skip != 'yes'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
branch="queue-filing/issue-${ISSUE}"
git checkout -b "$branch"
git add bibliography/ wiki/ reading-queue.md arxiv-inbox.md arxiv-interests.md
if git diff --cached --quiet; then
echo "::error::Claude produced no changes"; exit 1
fi
git commit -m "queue: file paper from #${ISSUE}"
# claude-code-action rewires the checkout git credentials for its
# own short-lived OIDC token (the first live run died here with
# 'Invalid username or token') — push with the job token explicitly.
git push "https://x-access-token:${GH_TOKEN}@github.com/${GH_REPO}.git" "HEAD:refs/heads/${branch}"
{
echo "Files the paper requested in #${ISSUE} (label: $(cat /tmp/issue_label.txt))."
echo
echo "Rendered by claude-code-action from the issue's section/line/notes;"
echo "\`make validate\` + \`pytest tests/\` ran green in the filing workflow"
echo "(GITHUB_TOKEN-created PRs do not trigger validate.yml)."
echo
echo "Closes #${ISSUE}."
} > /tmp/pr_body.md
if gh pr create --base main --head "$branch" \
--title "queue filing: paper from #${ISSUE}" \
--body-file /tmp/pr_body.md; then
gh issue comment "$ISSUE" --body \
"Filing PR opened: $(gh pr view "$branch" --json url --jq .url). Review and merge to complete the intake."
else
# Repo/org policy can forbid Actions-created PRs. The branch is
# pushed and gated — degrade to a one-tap PR link, not a failure.
# The board lists the branch under "Filings awaiting merge" until
# it lands or is deleted.
gh issue comment "$ISSUE" --body \
"Filing branch pushed and gated: \`${branch}\` — but this repo's policy blocks Actions from opening PRs. Open it with one tap: ${GITHUB_SERVER_URL}/${GH_REPO}/compare/main...${branch}?expand=1 — or enable 'Allow GitHub Actions to create and approve pull requests' (Settings → Actions → General) to make this automatic. Until it is merged, the paper is on that branch and not in memory; the dashboard lists it under *Filings awaiting merge*."
fi
- name: Report failure on the issue
if: failure()
run: |
gh issue comment "$ISSUE" --body \
"Queue filing failed — see the workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. The issue stays open; file manually from a Claude Code session if needed." || true