Skip to content
Back to skills

Keel Stale Prs

ASecurity

Find open PRs that have gone quiet or drifted off the base branch; triage, comment, and optionally refresh from the configured base branch — respecting the merge window.

  • 11 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 5, 2026
ai-agentsgobashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add berkayturanci/voicebridge --skill keel-stale-prs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Keel Stale Prs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Keel Stale Prs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/berkayturanci-keel-stale-prs-voicebridge/badge)](https://www.skillsdirectory.com/skills/berkayturanci-keel-stale-prs-voicebridge)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: keel-stale-prs
description: Find open PRs that have gone quiet or drifted off the base branch; triage, comment, and optionally refresh from the configured base branch — respecting the merge window.
---

# keel-stale-prs

Use this skill when the user asks to run the keel command `stale-prs` (e.g. `keel stale-prs ...`, `stale-prs <args>`, or `/keel:stale-prs`). It reads every project value from `.keel/project.yaml` via the `keel` CLI.

# /keel:stale-prs

## Live progress — stamp this run (required)

So this run shows live on `keel-visual`'s board, record it with `keel activity` **as you
go**. This command's phases are: `orient` → `list` → `classify` → `triage` → `post` → `rebase` → `summary`. Pick one stable `--run-id` for the whole run
(e.g. `stale-prs-<issue-or-pr>`):

- **Right now, before the work below**, stamp the first phase:
  `keel activity .keel/project.yaml --root . --write --command stale-prs --run-id "$RUN" --phase orient`
- Re-run with the next `--phase` (`list`, …) **as you advance** through the flow.
- At the end: `keel activity .keel/project.yaml --root . --run-id "$RUN" --done`

Treat this like any other contractual step — do not skip it. The one allowed exception is a
core too old to ship `keel activity` (keel < 1.6.0): then skip it silently and never block
the command.

## Command step evidence

Every numbered step in this command is contractual. Complete the step, record the
evidence it asks for, or explicitly mark it `N/A — <reason>` before moving on. If a step
has an external side effect such as a GitHub comment, issue, review, report, branch, or
PR, the side effect must be posted or written through the selected transport and cited in
the final summary. Never silently skip a step because the runtime, agent, or prompt feels
obvious.

Project-neutral stale-PR sweep. Triages open PRs that have either gone quiet or drifted out
of sync with `base_branch`. All project values come from `.keel/project.yaml` via the `keel`
CLI — the base branch, the merge window, and the CI workflows are never hardcoded here.

The command is **read-only on PR code by default**: it never modifies a PR's diff, never
closes a PR, never re-triggers CI manually, and never merges. Without `--rebase` the only
write is one triage comment per stale PR. It is safe to re-run on a schedule — the same-day
idempotency check (Step 4) is load-bearing.

## Step 0 — orient + parse arguments

```bash
keel validate .keel/project.yaml --root .
keel plan     .keel/project.yaml --root .     # read base_branch, ci_workflows, merge window
keel plan     .keel/project.yaml --root . --command stale-prs --live --json
keel window   .keel/project.yaml              # is the merge window open right now?
```

The live plan is the operator-consent preflight. Before posting comments, checking out or
merging branches, pushing refresh commits, using secrets, publishing, or calling
production-adjacent systems, parse `contract.operator_consent`; if
`requires_operator_consent` is true, STOP and ask the operator to rerun with the required
`--approve-scope` values.

Arguments:
- `--days <N>` — staleness threshold in calendar days. Default `3`. Reject `0`, negatives,
  and non-integers.
- `--rebase` — boolean; actually update each drift-bucket non-draft PR off `base_branch` and
  push. Without it the command is comment-only.
- `--merge-develop` — legacy alias for `--rebase`. It MUST behave exactly like `--rebase`
  and merge the configured `base_branch`, not a hardcoded `develop` branch. Reject runs that
  pass both aliases together as repeated refresh intent.
- `--dry-run` — boolean; print intended actions as `would …: …` lines and make **no** API
  call and **no** push. Every state-changing call (comment, checkout, merge, push) is
  redirected to stdout and skipped. May be combined with `--rebase` (prints the would-be
  rebase per PR, posts nothing).
- Reject unknown flags.

All published artifacts (comments, branch names) MUST be written in English; free-form chat
may be in any language.

## Step 1 — list open PRs targeting the base branch

Enumerate every open PR whose base is `base_branch`, paginating until all are collected. For
each, capture: number, title, last-activity timestamp (the activity signal advances on the
last commit OR comment OR review, so a PR with recent reviewer chatter but no new commits is
correctly *not* stale), head ref, draft flag, and author. Compute the cutoff as
`now − <days>` in UTC; keep only PRs whose last activity is strictly older than the cutoff.
PRs inside the window are skipped entirely.

## Step 2 — classify each stale PR

For each candidate, fetch its merge-state and confirm draft status. Assign one bucket, in
**priority order drift > review-stalled > abandoned** (a PR matching multiple goes to the
highest):

1. **Drift** — behind `base_branch` or merge-conflicted. A refresh is the unblocking action.
2. **Review-stalled** — mergeable but with no reviewer activity inside the window. Waiting on
   humans.
3. **Likely-abandoned** — a draft whose age from creation exceeds `2 × <days>`. **Informational
   only** — this command never closes abandoned PRs.

A PR both behind and a stale draft → drift; a clean stale draft → abandoned. PRs whose
merge-state cannot be classified and that do not match the abandoned criteria are dropped
with a one-line stdout note and no comment.

## Step 3 — build the per-PR triage comment

Build a comment body whose **literal first line** is a codename `STALE-PRS-<DATE>-<UTC_TIMESTAMP>`
(`<DATE>` in the project timezone). **Codename pin (load-bearing):** no blank line above it,
no leading whitespace, no quoting, no Markdown prefix or surrounding formatting — Step 4's
same-day dedupe finds prior runs by the `STALE-PRS-<DATE>-` prefix, and any deviation makes it
miss the prior comment and post a duplicate.

The body carries: a mandatory `@author` mention (how the author gets notified), the bucket,
last-activity timestamp + age in whole UTC days, merge state, the most-recent review/comment
link (or "none in window"), and a **suggested action** per bucket:
- **Drift** — fetch + merge `base_branch` into the head branch, resolve conflicts, push; or
  re-run with `--rebase` to let the sweep attempt the auto-merge.
- **Review-stalled** — ping the most-recent requested reviewer (fall back to "ping a
  reviewer" if none is parseable) or request a fresh one.
- **Likely-abandoned** — note the inactivity; informational, the PR will not be auto-closed.

If `--rebase` is set and the Step 5 auto-merge hits a conflict, append a final line to the
same body before posting: `Merge conflict detected — manual resolution needed on <head>.`

## Step 4 — post the triage comment (same-day idempotent)

For each PR with a built body, read its existing comments and look for one whose first line
starts with `STALE-PRS-<DATE>-` for today's `<DATE>`:
- **Prior same-day comment exists** → skip the post; log `already commented today on #N`. This
  is the idempotency guarantee that makes scheduled re-runs safe — do not weaken it.
- **No prior same-day comment** and not `--dry-run` → post the comment.
- **`--dry-run`** → print `would comment on #N` followed by the indented body; no API call.

## Step 5 — `--rebase` action (only when the flag is set)

Skip entirely when `--rebase` is unset. Otherwise, for each **drift-bucket non-draft** PR
(drafts are skipped to avoid pushing work the author may be rebasing), refresh the branch:

1. Fetch the head ref; check it out onto a throwaway local branch.
2. Merge `base_branch` into it with a no-fast-forward merge commit.
3. **Conflict handling — straightforward only:**
   - Merge clean → push to the head ref. Record `merge-pushed`. The push organically retriggers
     CI — that is the *only* intended CI side-effect (no manual re-run / dispatch).
   - Any conflict whatsoever → **abort and skip** (`git merge --abort`). Do NOT auto-resolve;
     arbitrary three-way conflicts are not safely automatable. Record `conflict-skipped` and
     append the conflict note to the triage comment (or a follow-up if today's comment already
     posted).
4. Local cleanup: return to the prior branch and delete the throwaway branch.

`--dry-run` prints `would rebase <head> for #N` per drift-bucket non-draft PR and performs no
fetch/checkout/merge/push. **Per-PR failures** (push auth error, network drop, merge-driver
crash) MUST NOT abort the sweep — record `error-skipped — <reason>` and continue.

Respect the **merge window**: this command never merges a PR into `base_branch`. Route a clean,
review-ready PR to `/keel:ship` (window + lock + review) or `/keel:review-cycle` rather than
merging here.

## Step 6 — session summary

Print a summary table: PR · title · bucket · action, where action is one of `commented`,
`already-commented-today`, `merge-pushed`, `conflict-skipped`, `dry-run-noted`,
`error-skipped — <reason>`. If no PR is stale at the threshold, print a single line
`No stale PRs at threshold <days> days.`

## Stop conditions / invariants

- **Never close a stale PR** — the abandoned bucket is informational; closing is a human call.
- **Never re-trigger CI manually** — the only CI side-effect is the natural retrigger from a
  push after a successful `--rebase`.
- **Never auto-resolve arbitrary conflicts** — Step 5 aborts and records `conflict-skipped`.
- **Per-PR errors never abort the sweep** — each is recorded and the loop continues.
- **Same-day idempotency** — never post a duplicate triage comment on the same date.
- **No silent dry-run mutations** — every would-be write is printed and skipped.
- **Never modify a PR's tree** beyond the merge commit that brings in `base_branch`.
- Fail-soft per PR; deterministic ordering.

<!-- keel-generated: surface=skills command=stale-prs keel_version=1.6.5 source_sha256=c38026730f14278d6bb7c41bc9ee38d2b1ed30b9c9ca370c079291613c0dd67a generated_sha256=b0c21fc3ea56bc136082333fac40526e0f2071e9a2b9bfaacb4e6bc7cc7689af -->

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.