The sweep script was closing issues based solely on when a lifecycle label
was applied, ignoring any human comments posted after the label. This caused
active issues (like #11792) to be closed even when users responded to the
stale warning.
Three changes:
1. Teach the triage bot about `stale` and `autoclose` labels so it removes
them when a human comments on the issue.
2. Add a safety net in `closeExpired()` that checks for non-bot comments
posted after the lifecycle label was applied — if any exist, skip closing.
3. Extend the 10-upvote protection (which previously only applied to
enhancements) to all issue types, in both `markStale()` and
`closeExpired()`.
Fixes#16497
## Test plan
Trace through scenarios manually:
- Issue with stale label + human comment after → triage removes label;
sweep skips even if triage hasn't run yet (safety net)
- Issue with stale label + no human comment → closes as before
- Issue with 10+ upvotes of any type → never marked stale or closed
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When lifecycle labels (needs-info, needs-repro, invalid, stale, autoclose)
are applied to an issue, the author currently only sees a label change with
no explanation. They then get a closing comment days later without ever
being nudged to respond.
Add a GitHub Actions workflow that triggers on issues.labeled and runs a
new lifecycle-comment.ts script to post a comment explaining what's needed
and how long before auto-close.
Extract lifecycle config (labels, timeouts, close reasons, nudge messages)
into a shared issue-lifecycle.ts so the sweep script and comment script
stay in sync. Previously the timeouts were duplicated between the sweep
script and the comment messages.
- needs-info: asks for version, OS, error messages
- needs-repro: asks for steps to trigger the issue
- invalid: links to the Claude Code repo and Anthropic support
- stale/autoclose: explains inactivity auto-close
The script no-ops for non-lifecycle labels, so the workflow fires on every
label event and lets the script decide — single source of truth.
## Test plan
Dry-run all labels locally:
GITHUB_REPOSITORY=anthropics/claude-code LABEL=needs-info ISSUE_NUMBER=12345 bun run scripts/lifecycle-comment.ts --dry-run
GITHUB_REPOSITORY=anthropics/claude-code LABEL=needs-repro ISSUE_NUMBER=12345 bun run scripts/lifecycle-comment.ts --dry-run
GITHUB_REPOSITORY=anthropics/claude-code LABEL=invalid ISSUE_NUMBER=12345 bun run scripts/lifecycle-comment.ts --dry-run
GITHUB_REPOSITORY=anthropics/claude-code LABEL=stale ISSUE_NUMBER=12345 bun run scripts/lifecycle-comment.ts --dry-run
GITHUB_REPOSITORY=anthropics/claude-code LABEL=autoclose ISSUE_NUMBER=12345 bun run scripts/lifecycle-comment.ts --dry-run
Verified sweep.ts still works:
GITHUB_TOKEN=$(gh auth token) GITHUB_REPOSITORY_OWNER=anthropics GITHUB_REPOSITORY_NAME=claude-code bun run scripts/sweep.ts --dry-run
The sweep job (https://github.com/anthropics/claude-code/actions/runs/21983111029/job/63510453226)
was silently failing when closeExpired tried to comment on a locked issue,
causing a 403 from the GitHub API.
Two issues:
1. closeExpired didn't skip locked issues like markStale already does.
Adding the same `if (issue.locked) continue` guard fixes this.
2. The error was swallowed by `main().catch(console.error)` which logs
to stderr but exits 0, so CI reported success despite the crash.
Replaced the main() wrapper with top-level await so unhandled errors
properly crash the process with a non-zero exit code.
## Test plan
YOLO
* Unify issue lifecycle labeling and sweep into a single system
Consolidate issue triage, stale detection, and lifecycle enforcement into
two components: a Claude-powered triage workflow and a unified sweep script.
Triage workflow changes:
- Add issue_comment trigger so Claude can re-evaluate lifecycle labels when
someone responds to a needs-repro/needs-info issue
- Add concurrency group per issue with cancel-in-progress to avoid pile-up
- Filter out bot comments to prevent sweep/dedupe triggering re-triage
- Hardcode allowed label whitelist to prevent label sprawl (was discovering
labels via gh label list, leading to junk variants like 'needs repro' vs
'needs-repro')
- Replace MCP GitHub server with gh CLI — simpler, no Docker dependency,
chaining is caught by the action so permissions are equivalent
- Add lifecycle labels (needs-repro, needs-info) for bugs missing info
- Add invalid label for off-topic issues (Claude API, billing, etc.)
- Add anti-patterns to prevent false positives (don't require specific
format, model behavior issues don't need traditional repro, etc.)
Sweep script changes:
- Absorb stale issue detection (was separate stale-issue-manager workflow)
- Mark issues as stale after 14 days of inactivity
- Skip assigned issues (team is working on it internally)
- Skip enhancements with 10+ thumbs up (community wants it)
- Add invalid label with 3-day timeout
- Add autoclose label support to drain 200+ legacy issues
- Drop needs-votes (stale handles inactive enhancements)
- Unify close messages into a single template with per-label reasons
- Run 2x daily instead of once
Delete stale-issue-manager.yml — its logic is now in sweep.ts.
## Test plan
Dry-run sweep locally:
GITHUB_TOKEN=$(gh auth token) GITHUB_REPOSITORY_OWNER=anthropics GITHUB_REPOSITORY_NAME=claude-code bun run scripts/sweep.ts --dry-run
Triage workflow will be tested by opening a test issue after merge.
* Update .github/workflows/claude-issue-triage.yml
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
---------
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
Introduce a simple, mechanical daily sweep that closes issues with
lifecycle labels past their timeout:
- needs-repro: 7 days
- needs-info: 7 days
- needs-votes: 30 days
- stale: 30 days
The sweep checks when the label was last applied via the events API,
and closes the issue if the timeout has elapsed. No AI, no comment
checking — if the label is still there past its timeout, close it.
Removing a label (by a triager, slash command, or future AI retriage)
is what prevents closure.
Each close message directs the reporter to open a new issue rather
than engaging with the closed one.
The script supports --dry-run for local testing:
GITHUB_TOKEN=$(gh auth token) \
GITHUB_REPOSITORY_OWNER=anthropics \
GITHUB_REPOSITORY_NAME=claude-code \
bun run scripts/sweep.ts --dry-run
## Test plan
Ran --dry-run against anthropics/claude-code. Correctly identified 3
issues past their timeouts (1 needs-repro at 12d, 2 needs-info at
14d and 26d). No false positives.