Queue filing #22
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: 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 |