Automation Β· Product Engineering
Close stale pull requests every day
Each day, close open PRs in one GitHub repo that are more than 3 weeks old and weren't reopened recently, rate how important each one was, and post a summary in Slack with the full table in a thread.
Created by Komran Rashidov from type.com
What it does
- Closes PRs opened more than 21 days ago, with a comment explaining how to reopen.
- Checks for recent reopens right before closing, and skips any PR it couldn't check.
- Rates each closed PR High, Medium, Medium-Low or Low so important work isn't lost.
- Posts a short Slack summary with the full table in a thread, plus a full report in Type.
- Never merges, and never touches PRs newer than 3 weeks or other repositories.
Before you start
- GitHub connected with permission to close pull requests in the repository.
- Slack connected through the Type Slack app, with the bot in the report channel.
See an example
Example output from a sample run. Unless the example says otherwise, company names and figures are sample data.
Example Slack post (illustrative, not real data)
π§Ή Stale PR cleanup β closed 4 PRs older than 3 weeks (cutoff 2026-01-12). Skipped 1 reopened PR. π΄ 1 high Β· π 1 medium Β· π‘ 1 medium-low Β· π’ 1 low. Reopen candidates: #812. Full table in thread π
Thread reply
| PR | Title | Author | Opened | Reopened | Importance | Assessment |
|---|---|---|---|---|---|---|
| #812 | Check workspace ID on file downloads | dev-a | 2025-12-01 | β | π΄ High | Closes a cross-tenant access gap and was approved; worth reopening. |
| #830 | Retry failed webhook deliveries | dev-b | 2025-12-10 | β | π Medium | Real reliability fix with one review round; re-verify before reopening. |
| #838 | Add CSV export to the audit log | dev-d | 2025-12-12 | β | π‘ Medium-Low | Modest feature with no reviews; reopen only if it is still planned. |
| #845 | Fix button spacing on settings page | dev-c | 2025-12-15 | β | π’ Low | Cosmetic change, no reviews. |
Skipped (reopened within the last 3 weeks) β’ #790 β reopened 2026-01-20T16:02:11Z
Skipped (reopen check failed) β’ none
Bottom line: reopen or re-verify #812.
How to use Close stale pull requests every day
- Select Use automation to open a new conversation with the automation attached.
- Choose a space and describe any changes you want before sending the message.
- Work with the agent to connect your integrations and review the automation before turning it on.
Browse the automation instructions
---
name: "Close stale pull requests (older than 3 weeks)"
description: "Every day, close open pull requests in one GitHub repository that were opened more than 21 days ago and not reopened in the last 21 days, rate how important each closed PR was, and post the results in Type and Slack."
connection_needs:
- "GitHub (original provider: GitHub via the gh CLI in Type's managed sandbox): list open PRs, read PR details and issue events/timeline, and close PRs with a comment in {{input:github_repository}}. Requires full automation access (arbitrary gh CLI commands cannot be limited to read-only) and pull request write access."
- "Slack (original provider: Slack via Type's Slack integration): post a top-level message and a threaded reply in {{input:slack_channel_id}}."
inputs:
- "github_repository: owner/name of the repository to clean up. Chosen at setup."
- "github_connection: the GitHub connection/account with write access to {{input:github_repository}}. Chosen at setup."
- "slack_connection: the Slack connection used to post. Chosen at setup."
- "slack_channel_id: ID of the Slack channel that receives the summary and threaded table. Chosen at setup."
- "slack_channel_name: display name of that channel (without #), used in the instructions. Chosen at setup."
- "low_priority_labels: PR labels that mark already-deprioritized work; a closed PR carrying one rates π’ Low. Chosen at setup. Source: `wontfix` and `likely-stale`."
- "schedule_time and schedule_timezone: when the daily run starts. Source: every day at 09:00 America/Los_Angeles."
---
# Close stale pull requests (older than 3 weeks)
## Purpose
Keep the open pull request list of one repository short. Once a day the automation closes every open PR in {{input:github_repository}} that was created more than 3 weeks (21 days) ago **and** has not been reopened within the last 21 days. It then rates each closed PR's importance so the team can spot anything worth reopening, and reports the result in the automation's own Type thread and in Slack channel #{{input:slack_channel_name}}.
Scope: one repository only. It never merges anything, never closes PRs newer than 21 days, never closes a PR whose reopen check did not run successfully, and never touches other repositories.
## Setup
- **GitHub connection** ({{input:github_connection}}): must be able to list PRs, read issue events/timeline and PR details, and close PRs with a comment in {{input:github_repository}}. It needs **full** automation access, because arbitrary `gh` CLI commands cannot be limited to read-only. The source used an organization GitHub App account, so closing comments appear from the app; a personal account posts them as that person. The source uses the `gh` CLI inside Type's managed sandbox; the token comes from the `TYPE_GITHUB_CONNECTIONS` environment variable (see Workflow step 1). Never print token values.
- **Slack connection** ({{input:slack_connection}}): must be able to post messages and threaded replies in {{input:slack_channel_id}}. The bot must be a member of that channel.
- **Repository** ({{input:github_repository}}): the installer picks it during setup.
- **Slack channel** ({{input:slack_channel_id}} / #{{input:slack_channel_name}}): the installer picks it during setup.
- **Low-priority labels** ({{input:low_priority_labels}}): the repository's labels for work the team already deprioritized. The source used `wontfix` and `likely-stale`.
- **Type destination**: the automation's own Type space and run thread (no separate input). The full report is the run's Type message.
- **Schedule**: every day (MonβSun) at {{input:schedule_time}} {{input:schedule_timezone}}. Source default: 09:00 America/Los_Angeles.
- No skills, private schemas or documents are required.
Creating the automation (`create_task`):
- `instructions`: the agent instructions under Reference implementation, with every input filled in. Do not put connection IDs in the instructions.
- `agentModeReason`: the source is an agent-only automation. The agent executes and interprets each PR's reopen check before closing it, and writes the importance assessment for every closed PR.
- `scheduleConfig`: `{ mode: "specific_time", time: "{{input:schedule_time}}", days: ["mon","tue","wed","thu","fri","sat","sun"], timezone: "{{input:schedule_timezone}}" }`.
- `integrationAccounts`: the chosen GitHub and Slack accounts.
- Do **not** set `slackChannelId`: the instructions post the two Slack messages themselves, and setting it would also post the run's final Type message to Slack as a third message.
- Run `check_automation_readiness` for GitHub (full access) and Slack first. Create it paused (`isEnabled: false`), review the saved settings, then enable it; the first run can close a large backlog of old PRs at once.
## Triggers and execution
- Execution type: agent (prompt) automation. There is no code-first stage; the agent follows the workflow below using the managed sandbox shell (`gh`, `jq`, `date`) and the Slack integration.
- Trigger: schedule, every day of the week at {{input:schedule_time}} {{input:schedule_timezone}} (source: `specific_time` mode, 09:00, all seven days, America/Los_Angeles).
- Model/run settings: workspace default model and effort (the source has no run configuration override).
- Each run is independent. Nothing is passed between runs.
## Workflow
### Part 1: Close stale PRs
1. In the managed sandbox, read `TYPE_GITHUB_CONNECTIONS` and use the `tokenEnv` for "{{input:github_repository}}" as `GH_TOKEN` for the `gh` CLI. Do not print token values. `TYPE_GITHUB_CONNECTIONS` is a JSON array of objects keyed by `fullName`, not owner/repo: select the entry whose `fullName` is "{{input:github_repository}}" and use its `tokenEnv` value.
2. Compute the cutoff:
`cutoff=$(date -u -d '21 days ago' +%Y-%m-%dT%H:%M:%SZ)`
3. List candidates:
`gh pr list -R {{input:github_repository}} --state open --limit 500 --json number,title,author,url,createdAt,isDraft`
and select PRs with `createdAt < cutoff` (creation date; include drafts).
4. **Mandatory reopen check: execute it, never infer it.** `gh pr view` / `gh pr list` do not expose reopen events, so for **every** candidate from step 3 (no exceptions, no sampling, no reuse of results from earlier runs or memory) run this command and record its raw output:
`gh api "repos/{{input:github_repository}}/issues/<number>/events" --paginate --jq '[.[] | select(.event == "reopened")] | last | .created_at'`
(GraphQL `timelineItems(itemTypes: REOPENED_EVENT)` is an acceptable alternative.)
- `--jq` runs once per page, so the command may print more than one line. The reopen timestamp is the **latest non-`null`** timestamp across all printed lines. It is `null` (never reopened) only if every printed line is `null`.
- `gh` prints a null result as an empty line rather than the text `null`. If the command exits 0 but prints only empty lines (or nothing), confirm with GraphQL `timelineItems(itemTypes: REOPENED_EVENT)`: empty `nodes` means never reopened (`null`); otherwise the reopen timestamp is the latest `createdAt` in `nodes`. Ignore `totalCount` there, because it counts all timeline items, not just reopens.
- If the command errors, exits non-zero, hits a rate limit or prints unparseable output, or the GraphQL confirmation fails, the reopen check **failed** for that PR: do not close it. Mark it "Skipped (reopen check failed)" with the error.
5. For each candidate compute **staleness date = the later of `createdAt` and the reopen timestamp from step 4** (just `createdAt` when the reopen timestamp is null).
- If staleness date β₯ cutoff (reopened within the last 21 days): do not close. List it under "Skipped (reopened within the last 3 weeks)" with the exact reopen timestamp.
- Only PRs whose staleness date < cutoff are eligible to close.
6. Before closing, gather assessment inputs for each eligible PR:
`gh pr view <number> -R {{input:github_repository}} --json number,title,author,url,createdAt,updatedAt,isDraft,reviewDecision,labels,additions,deletions,comments,reviews,body`
(body truncated to about 400 characters is enough).
7. **Re-check immediately before closing.** For each eligible PR, re-run the exact step 4 check right before closing it and re-apply step 5 to that fresh result.
- If it now shows a reopen timestamp β₯ cutoff, skip it (list under "Skipped (reopened within the last 3 weeks)").
- If the re-check fails for any reason, skip it as "Skipped (reopen check failed)".
- Only if the fresh check succeeds and staleness date < cutoff, run:
`gh pr close <number> -R {{input:github_repository}} --comment "Closing: this PR has been open for more than 3 weeks (automated stale-PR cleanup). Reopen if it is still relevant."`
8. Re-run the list to verify there are zero remaining open PRs whose staleness date is older than the cutoff (all remaining old-created PRs must be ones skipped in steps 4β7).
Hard limits: do not close PRs newer than 21 days, do not close any PR whose reopen check was not successfully executed, do not merge anything, and do not touch other repositories.
### Part 2: Assess importance
For each closed PR assign one of: π΄ High, π Medium, π‘ Medium-Low, π’ Low. Base it on the PR body, size, review activity, labels and recency of updates.
- π΄ High: anything touching security, cross-tenant data access, billing/entitlements, auth, or data loss.
- π Medium: real product bugs or architecture.
- π’ Low: cosmetic changes, planning docs, tiny hardening, or PRs already labelled with one of {{input:low_priority_labels}}.
- π‘ Medium-Low: the rest. A PR that is not High, Medium or Low, typically a modest feature or improvement without review activity. This is agent judgment, like the other levels (see Changes from the source).
Write a one-sentence assessment per PR and, at the end, name the PRs (if any) that should be reopened or re-verified.
Also list every PR that was **skipped** (not closed): under "Skipped (reopened within the last 3 weeks)" with its exact reopen timestamp, and under "Skipped (reopen check failed)" with the error.
### Part 3: Report (always, even if zero PRs were closed)
A. **Type message** (the automation's own Type space/run thread): the cutoff date, count closed, count skipped (reopened recently), count skipped (reopen check failed), then a Markdown table with columns:
| PR | Title | Author | Opened | Reopened | Importance | Assessment |
|---|---|---|---|---|---|---|
- PR is a link.
- "Reopened" must show the exact timestamp returned by the step 4/7 check (latest non-null value). "β" is allowed **only** when the check ran successfully and returned null (never reopened).
- Then include the "Skipped (reopened within the last 3 weeks)" and "Skipped (reopen check failed)" lists.
- End with a "Bottom line" sentence naming reopen candidates.
B. **Slack** (channel "#{{input:slack_channel_name}}", {{input:slack_channel_id}}), posted as a two-part message with the Slack integration (`call_tool`, service `slack`):
1. Call `slack_post_message` with `channel_id` "{{input:slack_channel_id}}" and a SHORT snippet only (3β6 lines), for example:
"π§Ή Stale PR cleanup β closed N PRs older than 3 weeks (cutoff YYYY-MM-DD). Skipped M reopened PRs. π΄ X high Β· π Y medium Β· π‘ W medium-low Β· π’ Z low. Reopen candidates: #a, #b. Full table in thread π"
2. Call `slack_post_message` again with `channel_id` "{{input:slack_channel_id}}" **and** `thread_ts` set to the `ts` from the first message, posting the FULL Markdown assessment table (including the skipped lists) as a threaded reply.
If zero PRs were closed and zero were skipped, still post a one-line summary to both Type and Slack ("nothing to close today").
## State and failure behavior
- No persistent state, checkpoints or effect keys. Every run recomputes the cutoff and re-reads the repository; results from earlier runs or memory must not be reused (step 4).
- Duplicate handling: a PR closed on an earlier run is no longer open, so it is not a candidate again. Re-running the same day re-evaluates whatever is still open.
- A failed or unparseable reopen check (initial or re-check), including a failed GraphQL confirmation, skips that PR and reports it with the error; it is never closed on an inferred result.
- A PR reopened between the first check and closing is caught by the step 7 re-check and skipped.
- If a close command fails, the PR stays open and must not be reported as closed; step 8 surfaces any remaining eligible PR.
- Reporting always happens, including the zero-result case.
- The source defines no retry policy beyond this; the Slack thread reply depends on the `ts` returned by the first post.
## Integration data and capabilities
**GitHub (gh CLI in the managed sandbox, connection {{input:github_connection}})**
- Auth: `TYPE_GITHUB_CONNECTIONS` (JSON array; entries keyed by `fullName`, each with a `tokenEnv` naming the environment variable that holds the token). Exported as `GH_TOKEN`. Requires full automation access.
- Reads: `gh pr list --state open --limit 500 --json number,title,author,url,createdAt,isDraft`; `gh api repos/<repo>/issues/<number>/events --paginate` (filters `event == "reopened"`, reads `created_at`); GraphQL `timelineItems(itemTypes: REOPENED_EVENT)` (`nodes`, each `createdAt`) to confirm empty results; `gh pr view --json number,title,author,url,createdAt,updatedAt,isDraft,reviewDecision,labels,additions,deletions,comments,reviews,body`.
- Writes: `gh pr close <number> --comment "<fixed comment>"` only.
- Limits: the list is capped at 500 open PRs; issue events are paginated (`--paginate`, `--jq` runs per page). Rate-limit errors count as a failed reopen check.
**Slack (connection {{input:slack_connection}})**
- Writes: `slack_post_message` with `channel_id` (top-level snippet), then `slack_post_message` with `channel_id` and `thread_ts` (threaded full table). Reads only the `ts` of the first post.
- The threaded reply is the Markdown table sent as message text, as in the source. Slack does not render Markdown tables, so it appears as pipe-separated rows.
**Type**
- The full report is posted as the run's message in the automation's own Type space/thread.
## Reference implementation
Original provider: GitHub via `gh` in a Bash shell with `jq` and GNU `date`; Slack via Type's `slack_post_message`. Placeholders: {{input:github_repository}}, {{input:slack_channel_id}}, {{input:slack_channel_name}}, {{input:low_priority_labels}}. **Reuse when these assumptions hold; adapt when they do not, preserving the behavior above.**
Agent instructions: the source's instructions with inputs filled in and the changes listed under Changes from the source. Save them as the task's `instructions`.
```text
Close all open pull requests in the GitHub repository {{input:github_repository}} that were created more than 3 weeks (21 days) ago AND have not been reopened within the last 21 days, then report on them.
## Part 1 β Close stale PRs
1. In the managed sandbox, read TYPE_GITHUB_CONNECTIONS and use the tokenEnv for "{{input:github_repository}}" (do not print token values). Use it as GH_TOKEN for the gh CLI. TYPE_GITHUB_CONNECTIONS is a JSON array of objects keyed by fullName (not owner/repo); select the entry with fullName "{{input:github_repository}}" and use its tokenEnv value as GH_TOKEN.
2. Compute the cutoff: `cutoff=$(date -u -d '21 days ago' +%Y-%m-%dT%H:%M:%SZ)`.
3. List candidates: `gh pr list -R {{input:github_repository}} --state open --limit 500 --json number,title,author,url,createdAt,isDraft` and select PRs with createdAt < cutoff (creation date; include drafts).
4. **Mandatory reopen check β execute it, never infer it.** `gh pr view` / `gh pr list` do NOT expose reopen events, so for EVERY candidate from step 3 (no exceptions, no sampling, no reuse of results from earlier runs or memory) run this command and record its raw output:
`gh api "repos/{{input:github_repository}}/issues/<number>/events" --paginate --jq '[.[] | select(.event == "reopened")] | last | .created_at'`
(GraphQL `timelineItems(itemTypes: REOPENED_EVENT)` is an acceptable alternative.)
- `--jq` runs once per page, so the command may print more than one line. The reopen timestamp is the LATEST non-`null` timestamp across all printed lines. It is `null` (never reopened) only if every printed line is `null`.
- gh prints a null result as an empty line, not "null". If the command exits 0 but prints only empty lines (or nothing), confirm with GraphQL `timelineItems(itemTypes: REOPENED_EVENT)`: empty `nodes` means never reopened (null); otherwise the reopen timestamp is the latest `createdAt` in `nodes`. Ignore `totalCount` there, because it counts all timeline items, not just reopens.
- If the command errors, exits non-zero, hits a rate limit, or prints unparseable output, or the GraphQL confirmation fails, the reopen check FAILED for that PR: do NOT close it. Mark it "Skipped (reopen check failed)" with the error.
5. Compute for each candidate: **staleness date = the LATER of createdAt and the reopen timestamp from step 4** (just createdAt when the reopen timestamp is null).
- If staleness date β₯ cutoff (reopened within the last 21 days): do NOT close. List it under "Skipped (reopened within the last 3 weeks)" with the exact reopen timestamp.
- Only PRs whose staleness date < cutoff are eligible to close.
6. Before closing, gather assessment inputs for each eligible PR with `gh pr view <number> -R {{input:github_repository}} --json number,title,author,url,createdAt,updatedAt,isDraft,reviewDecision,labels,additions,deletions,comments,reviews,body` (body truncated to ~400 chars is enough).
7. **Re-check immediately before closing.** For each eligible PR, re-run the exact step 4 command right before closing it and re-apply step 5 to that fresh result. If it now shows a reopen timestamp β₯ cutoff, skip it (list under "Skipped (reopened within the last 3 weeks)"). If the re-check fails for any reason, skip it as "Skipped (reopen check failed)". Only if the fresh check succeeds and staleness date < cutoff, run:
`gh pr close <number> -R {{input:github_repository}} --comment "Closing: this PR has been open for more than 3 weeks (automated stale-PR cleanup). Reopen if it is still relevant."`
8. Re-run the list to verify zero remaining open PRs whose staleness date is older than the cutoff (all remaining old-created PRs must be ones skipped in steps 4β7). Do not close PRs newer than 21 days, do not close any PR whose reopen check was not successfully executed, do not merge anything, and do not touch other repositories.
## Part 2 β Assess importance
For each closed PR assign one of: π΄ High, π Medium, π‘ Medium-Low, π’ Low. Base it on the PR body, size, review activity, labels, and recency of updates. Treat as High anything touching security, cross-tenant data access, billing/entitlements, auth, or data loss; Medium for real product bugs/architecture; Low for cosmetic, planning docs, tiny hardening, or PRs already labeled with one of {{input:low_priority_labels}}; Medium-Low for the rest: a PR that is not High, Medium or Low, typically a modest feature or improvement without review activity. Write a one-sentence assessment per PR and, at the end, name the PRs (if any) that should be reopened or re-verified.
Also list every PR that was **skipped** (not closed): under "Skipped (reopened within the last 3 weeks)" with its exact reopen timestamp, and under "Skipped (reopen check failed)" with the error.
## Part 3 β Report (do this even if zero PRs were closed)
A. Post in Type (this Space) a message with the cutoff date, count closed, count skipped (reopened recently), count skipped (reopen check failed), then a Markdown table with columns: PR (as a link), Title, Author, Opened, Reopened, Importance, Assessment. The "Reopened" column must show the exact timestamp returned by the step 4/7 command (latest non-null value). "β" is allowed ONLY when the command ran successfully and returned null (never reopened). Then include the "Skipped (reopened within the last 3 weeks)" and "Skipped (reopen check failed)" lists. End with a "Bottom line" sentence naming reopen candidates.
B. Post to Slack channel "{{input:slack_channel_name}}" ({{input:slack_channel_id}}) as a two-part message using call_tool with service "slack":
1. First call `slack_post_message` with channel_id "{{input:slack_channel_id}}" and a SHORT snippet only (3β6 lines): e.g. "π§Ή Stale PR cleanup β closed N PRs older than 3 weeks (cutoff YYYY-MM-DD). Skipped M reopened PRs. π΄ X high Β· π Y medium Β· π‘ W medium-low Β· π’ Z low. Reopen candidates: #a, #b. Full table in thread π"
2. Then call `slack_post_message` again with channel_id "{{input:slack_channel_id}}" AND thread_ts set to the ts from the first message, posting the FULL Markdown assessment table (including the skipped lists) as a threaded reply.
If zero PRs were closed and zero skipped, still post a one-line summary to both Type and Slack ("nothing to close today").
```
Token setup (Workflow step 1):
```bash
tokenEnv=$(printf '%s' "$TYPE_GITHUB_CONNECTIONS" \
| jq -er 'first(.[] | select(.fullName == "{{input:github_repository}}") | .tokenEnv)')
export GH_TOKEN=$(printenv "$tokenEnv")
```
Cutoff, candidates and reopen check (steps 2β4):
```bash
cutoff=$(date -u -d '21 days ago' +%Y-%m-%dT%H:%M:%SZ)
gh pr list -R {{input:github_repository}} --state open --limit 500 \
--json number,title,author,url,createdAt,isDraft
# for EVERY candidate with createdAt < cutoff:
gh api "repos/{{input:github_repository}}/issues/<number>/events" --paginate \
--jq '[.[] | select(.event == "reopened")] | last | .created_at'
```
Confirming an empty result (step 4). Illustrative helper, not in the source; the source only names the GraphQL field and the `nodes` check:
```bash
repo='{{input:github_repository}}'
gh api graphql -F owner="${repo%%/*}" -F name="${repo#*/}" -F number=<number> -f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
timelineItems(itemTypes: [REOPENED_EVENT], last: 100) {
nodes { ... on ReopenedEvent { createdAt } }
}
}
}
}' --jq '.data.repository.pullRequest.timelineItems.nodes'
# [] means never reopened; otherwise use the latest createdAt. Any error means the check failed.
```
Close (step 7, only after a successful fresh re-check):
```bash
gh pr close <number> -R {{input:github_repository}} \
--comment "Closing: this PR has been open for more than 3 weeks (automated stale-PR cleanup). Reopen if it is still relevant."
```
## Verification
Source-derived examples (from the source rules; no run data):
- PR created 30 days ago, never reopened (every printed line `null`, or empty output with GraphQL `nodes` empty) β closed; "Reopened" shows "β".
- PR created 40 days ago, reopened 5 days ago β staleness date is 5 days ago β₯ cutoff β listed under "Skipped (reopened within the last 3 weeks)" with the exact reopen timestamp.
- PR created 30 days ago, reopened 25 days ago β staleness date 25 days ago < cutoff β closed; "Reopened" shows that timestamp.
- PR created 10 days ago β not a candidate.
- Reopen check returns a rate-limit error β "Skipped (reopen check failed)" with the error; not closed.
- Empty output and the GraphQL confirmation also fails β "Skipped (reopen check failed)"; not closed.
- Closed PR touching authentication β π΄ High and named as a reopen candidate if still relevant.
- Closed PR labelled with one of {{input:low_priority_labels}} β π’ Low.
- Closed modest feature PR with no review activity that is not a bug, architecture, security or cosmetic change β π‘ Medium-Low.
- Zero closed and zero skipped β one-line "nothing to close today" in Type and Slack.
Proposed acceptance checks for reconstruction (not completed tests):
- Use a test repository with draft and non-draft PRs on both sides of the 21-day boundary; confirm drafts are included and only PRs with staleness date < cutoff close.
- Reopen a candidate between the first check and closing; confirm the re-check skips it.
- A PR whose events span more than one page with a reopen on the second page: confirm the latest non-null timestamp is used.
- A never-reopened PR whose check prints only empty lines: confirm the GraphQL confirmation runs and the PR is treated as never reopened.
- Confirm the Slack summary shows all four counts, the reply lands in the thread of the first message, and the Type message contains the full table, skipped lists and "Bottom line".
- Confirm Slack receives only the messages above per run (two, or one in the nothing-to-close case), with no extra copy of the final Type message.
- Run twice on the same day; confirm the second run does not report already-closed PRs as closed.
## Changes from the source
1. π‘ Medium-Low now has a rule. The source lists the level without criteria; the rule follows the author's later code-first version of this automation, where Medium-Low is what remains after High, Medium and Low (moderate-size work without reviews). The rating stays agent judgment.
2. The Slack summary example includes the Medium-Low count, as the author's later version does.
3. Empty reopen-check output is confirmed with the GraphQL query instead of being treated as an immediate failure. This reconciles the source instructions with the source's own saved working notes, which record that `gh` prints a null result as an empty line.