| name | issue-triage |
| description | Triage open GitHub issues on sibson/redbeat — work out whether a report still applies, prove it with a committed regression test when possible, ask the reporter a specific question when not, and cluster duplicates. Use this whenever the user mentions triage, the issue backlog, stale issues, "does this still apply", "is this still a bug", cleaning up old issues, or invokes /issue-triage. Also use it when asked to investigate a single issue number in depth before deciding what to do with it. |
redbeat issue triage
redbeat's backlog runs back to 2017. Most of it is not "unknown" so much as
"nobody has spent the twenty minutes". That is the job here: spend the twenty
minutes and leave behind evidence, so the maintainer's decision is a one-line
read rather than a fresh investigation.
The thing that makes this worth automating is that redbeat is unusually cheap to
reproduce against. tests/basecase.py gives you a RedBeatCase backed by
fakeredis, so confirming a scheduling bug takes a file and no infrastructure.
Where a test is possible, a test beats any amount of prose.
Two rules that matter more than the rest
Only report what you actually ran. If you say a bug reproduces, a test must
have executed and failed, and you quote its real output. A plausible-sounding
reproduction that was never run is worse than saying nothing — it puts a false
fact in a permanent public record, and the maintainer has no way to tell it from
a real one without redoing your work, which defeats the point of the exercise.
The same goes for "already fixed": name the commit, don't infer it from vibes.
Read the comments before you write one. Re-runs are normal — the backlog
gets swept more than once, and the same issue will come up again. If the last
comment on the issue is a previous triage comment, posting a near-identical one
turns this from a useful tool into a notification spammer that reporters mute.
Check first, and if the state hasn't changed, say so in your report to the
maintainer rather than on the issue.
Both rules exist because the audience for a triage comment is a human who was
mildly annoyed to get the notification. Earn it.
Invocation
-
/issue-triage 307 — one issue, full depth.
-
/issue-triage — a batch of the least-recently-updated open issues not yet
triaged (default 5), followed by the overdue report. "Not yet triaged" means
carrying none of bug, confirmed, has-repro, needs-info,
probably-fixed, duplicate, documentation, question,
needs-integration-test. enhancement and help wanted predate this process
and say nothing about whether an issue has been looked at — an issue carrying
only those is still untriaged, and several of the oldest ones are exactly that.
The batch is selected by querying the API and sorting by updated_at
ascending. It is never read off the filesystem. Files under evals/fixtures/
are frozen copies used to grade this skill, so the issues they name are the
ones most likely to be sitting in your context — which makes them a tempting
and completely wrong answer to "which issues need triage". If a run is
supplying a fixture in place of a live issue, that substitutes the content
of one issue you were already asked about; it never tells you which issues to
pick. Selecting the fixture set is the tell that the selection rule was
skipped.
-
--dry-run on either — do the whole investigation and print the report and the
exact comment you would post, but take no write action. Use this whenever
you're unsure, and note that the evals in evals/ run this way.
What you may and may not do
You may comment, apply labels from the set below, create a branch, and open a PR.
You may not close an issue. Never call issue_write with state or
state_reason — not for duplicates, not for obsolete reports, not for anything.
Deciding a report is dead is the maintainer's call, and the whole value of this
skill rests on that boundary being reliable. Never push to main.
You may not review, approve, or comment on someone else's open PR. When one
addresses the issue you're triaging, your assessment of it goes in the issue
comment and the run report — the reporter and the maintainer are the audience,
and a contributor whose two-year-old PR suddenly gets a critique from a triage
bot is not.
Labels, and only these: bug, confirmed, has-repro, needs-info,
probably-fixed, duplicate, documentation, question,
needs-integration-test, enhancement, help wanted. If none fits, that is a
finding to report, not a licence to invent one. All eleven exist on the repo; you
cannot create labels and have no need to.
Every comment ends with the attribution footer:
---
_Generated by [Claude Code](https://claude.ai/code)_
Exactly that URL. Anything you write here — comment, PR body, PR title, commit
message — is public and permanent, so it carries no claude.ai/code/session_...
link and no Claude-Session: trailer. Those resolve for nobody but the person
who ran it, and they are noise in a thread the reporter has to read. The bare
https://claude.ai/code above is the whole attribution.
Procedure
1. Read. issue_read with get, then get_comments. Pull out the reported
redbeat, celery, python and redis versions, the repro if there is one, and
whether a maintainer already replied. Apply the idempotency check now: if the
most recent comment is a prior triage comment, do not re-post — either advance
the state because something changed, or report it as unchanged.
2. Locate. Map the symptom onto real code and cite file:line. Most of it
lands in redbeat/schedulers.py; JSON round-tripping is redbeat/decoder.py,
rrule is redbeat/schedules.py. If you cannot find the code the report is about,
that is itself the finding — say so rather than guessing.
3. Check whether it was already fixed. Old issues are frequently dead. In
rough order of speed:
git log -S'<symbol>' --oneline -- redbeat/ # when did this change?
git log -L<start>,<end>:redbeat/schedulers.py # history of these lines
grep -n '#<NNN>' CHANGES.txt # changelog references
plus search_issues for a merged PR mentioning the number. A fix you can name
beats a suspicion you can't. An open PR is a different finding and belongs in
step 6 — don't let one you spot here stand in for that search.
Check for a shallow clone first (.git/shallow, or a suspiciously short
git log). In one, git log -S blames the graft boundary commit for everything
older, which reads exactly like "changed recently" and will walk you into naming
the wrong commit. When history is truncated, get it from the API instead —
search_commits and the closed issue or PR will date a fix properly where local
history can't. Failing that, cite CHANGES.txt: an honest CHANGES.txt:67 is
worth more than a confident sha that means nothing.
4. Check version relevance. The tree supports python >= 3.9 and celery >= 5.
A traceback from celery 4.3 on redbeat 0.13 against a function that has since
been rewritten is not current evidence. Say that plainly instead of treating the
old trace as live — but don't leap from "old" to "invalid": the underlying bug
often survives the rewrite. Check the current code before deciding.
5. Look for duplicates. Search open and closed issues for the same symptom.
Canonical = oldest with the best repro.
Search the narrowest distinctive identifier on its own before adding
qualifying terms. GitHub search ANDs the terms, so every word you add can only
shrink the result set, and the duplicate you want is often the thread that
described the same bug in different words. due_at finds #210; due_at remaining_estimate does not, because the 2021 report never used the second
word. Add terms only to cut a result set that came back too large — never as
your opening move.
Recurring themes, as a starting point for the search and nothing more —
verify against the actual threads before relying on any of it, because
similar-sounding reports routinely have different causes: due_at /
remaining_estimate arithmetic; lock ownership, extension and re-election;
lock release on shutdown; entries disappearing from the schedule. A shared
error message is not a shared cause — LockNotOwnedError alone spans a
documented design exit, a stale lock after restart, and a null lock after a
dropped connection.
6. Check for an open PR that already addresses it. Step 3 asks whether a
fix landed; this asks whether one is sitting in review. They are different
states with different maintainer actions — "someone should look at this" versus
"review #NNN" — and an issue whose fix has been waiting two years is the most
actionable thing this skill can surface.
search_pull_requests repo:sibson/redbeat is:open <NNN>
list_pull_requests state=open # under a dozen; skimming is cheap
search_issues is not a substitute: the clause in step 3 finds PRs only
incidentally, and a contributor who titled theirs fix #285 or
Fix/due at calculation 307 is found by the number, not by the words in your
symptom. Search the bare number first, then skim the open list — several of
these PRs name their issue only in the title or a one-line body, and #286
(fix #284 fix #199) addresses two issues at once, so a per-issue search that
came back empty is not proof.
Judge it, don't just link it. Does the diff address the mechanism you traced in
step 2, or something adjacent? Does it contradict documented design — #287
re-acquires the lock inside tick, where docs/design.rst:55 says a node that
has lost the lock exits instead? Is it stale, does it carry a test? A link plus
one sentence of assessment is the deliverable; approving or merging is not yours
to do.
What this changes is the action, not the classification: an issue with a PR
open against it is still confirmed, or still a duplicate. See "When a PR is
already open" in references/decision-table.md — most importantly, do not open
a second PR that duplicates work someone is already waiting on review for.
7. Classify. bug, enhancement, question, or documentation. Before treating
anything as a defect, check docs/ — particularly docs/design.rst — for
whether it is a deliberate choice. Several of redbeat's sharpest edges are
documented design — beat exiting when it loses the lock, rather than
re-acquiring, is the one that catches people.
Finding the design note settles less than it appears to, so don't stop there.
The documented intent usually covers what happens, not how well: exiting on
lock loss can be deliberate while dying with an unhandled traceback instead of a
clean logged exit is still a defect, and "we chose not to re-acquire" invites the
question of whether that choice holds for a single-beat deployment. Say which
part is by design and which part is still open, rather than closing the whole
report because the headline behaviour is documented.
If the docs justify the behaviour but only somewhere the affected user would
never look, name that gap too — a design note in docs/design.rst doesn't help
someone reading the setting's reference entry in docs/config.rst.
Once you have found the design note, it constrains what you may write next: do
not commit a test asserting that the documented behaviour is a defect. Writing
@unittest.expectedFailure around "beat exits when it loses the lock" encodes
a claim the docs contradict, and it outlives the run — the next person reads a
failing test as an agreed bug. If the residual complaint is the quality of the
documented behaviour (a bare traceback where a logged exit belongs), test that
specific gap and say so, or leave it to prose. The verdict has to match the
analysis: recognising the behaviour as designed and then filing it bug +
confirmed with a failing test is the contradiction this step exists to
prevent.
8. Try to reproduce. See references/repro-harness.md for how to write a
redbeat test — the conventions are unusual and you will get them wrong from
instinct. Bugs that are pure scheduling arithmetic are almost always testable by
injecting nowfun. Bugs that need a real Redis, a cluster, a sentinel, a DST
transition or a suspended laptop are not, and forcing a fake test for them
produces something that passes for the wrong reason. Knowing which is which is
most of the skill.
9. Act. Pick exactly one outcome from references/decision-table.md and
follow its template. Read that file before writing any comment.
10. Leave the tree as you found it. Delete the scratch files this run
created, then check git status. If it is not clean, report exactly what is
there — never delete a file you did not create to make the output look tidy. An
untracked file you did not write belongs to someone: an interrupted earlier run,
or the maintainer's work in progress. Removing it to report a clean tree is
destroying someone else's data to improve your own status line, and it is
unrecoverable in a way nothing else in this skill is. "Working tree clean except
tests/foo.py, which predates this run and I left alone" is the correct
outcome, not a failure.
The overdue report
A bare /issue-triage ends with a list of issues that have gone quiet. Two ways
in, and the second matters more than the first:
- It carries
needs-info, the newest comment is a triage comment, and that
comment is more than 30 days old.
- The newest comment is an unanswered question from a maintainer, more than 30
days old, whatever the labels say. Most of this backlog predates any
labelling scheme, so the first rule alone sees nothing: #270 has had
"does this still occur with 2.3.2?" sitting unanswered since early 2025 and
carries no label at all. An overdue check that only finds issues the process
already touched will report an empty list over a backlog full of dead threads.
Both are readable from the API, so there's no state file to drift out of sync.
List each with number, title, days since the question, and one line on what a
close would be based on. Recommend; don't close. The maintainer decides.
Report format
End every run with this, whether dry-run or not:
## Triage: #NNN <title>
**Outcome**: <A-G> — <one line>
**Evidence**: <commit / file:line / test result — the actual thing>
**Open PR**: <#NNN — one line on whether it addresses this, or "none found">
**Labels**: <applied or proposed>
**Actions taken**: <comment posted, PR #NNN opened, or "none (dry-run)">
Open PR is never blank. "none found" is a real result and says you looked;
an absent line reads as a step skipped.
For a batch, one block per issue, then the overdue section. Keep it terse — this
is a worklist, not an essay.