surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
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
| A.github/workflows/issue-lifecycle-comment.yml |
| Ascripts/issue-lifecycle.ts |
| Ascripts/lifecycle-comment.ts |
| Mscripts/sweep.ts |
| @@ -1,0 +1,27 @@ | ||
| 1 | +name: "Issue Lifecycle Comment" | |
| 2 | + | |
| 3 | +on: | |
| 4 | + issues: | |
| 5 | + types: [labeled] | |
| 6 | + | |
| 7 | +permissions: | |
| 8 | + issues: write | |
| 9 | + | |
| 10 | +jobs: | |
| 11 | + comment: | |
| 12 | + runs-on: ubuntu-latest | |
| 13 | + steps: | |
| 14 | + - name: Checkout repository | |
| 15 | + uses: actions/checkout@v4 | |
| 16 | + | |
| 17 | + - name: Setup Bun | |
| 18 | + uses: oven-sh/setup-bun@v2 | |
| 19 | + with: | |
| 20 | + bun-version: latest | |
| 21 | + | |
| 22 | + - name: Post lifecycle comment | |
| 23 | + run: bun run scripts/lifecycle-comment.ts | |
| 24 | + env: | |
| 25 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 26 | + LABEL: ${{ github.event.label.name }} | |
| 27 | + ISSUE_NUMBER: ${{ github.event.issue.number }} |
| @@ -1,0 +1,38 @@ | ||
| 1 | +// Single source of truth for issue lifecycle labels, timeouts, and messages. | |
| 2 | + | |
| 3 | +export const lifecycle = [ | |
| 4 | + { | |
| 5 | + label: "invalid", | |
| 6 | + days: 3, | |
| 7 | + reason: "this doesn't appear to be about Claude Code", | |
| 8 | + nudge: "This doesn't appear to be about [Claude Code](https://github.com/anthropics/claude-code). For general Anthropic support, visit [support.anthropic.com](https://support.anthropic.com).", | |
| 9 | + }, | |
| 10 | + { | |
| 11 | + label: "needs-repro", | |
| 12 | + days: 7, | |
| 13 | + reason: "we still need reproduction steps to investigate", | |
| 14 | + nudge: "We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected?", | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + label: "needs-info", | |
| 18 | + days: 7, | |
| 19 | + reason: "we still need a bit more information to move forward", | |
| 20 | + nudge: "We need more information to continue investigating. Can you make sure to include your Claude Code version (`claude --version`), OS, and any error messages or logs?", | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + label: "stale", | |
| 24 | + days: 14, | |
| 25 | + reason: "inactive for too long", | |
| 26 | + nudge: "This issue has been automatically marked as stale due to inactivity.", | |
| 27 | + }, | |
| 28 | + { | |
| 29 | + label: "autoclose", | |
| 30 | + days: 14, | |
| 31 | + reason: "inactive for too long", | |
| 32 | + nudge: "This issue has been marked for automatic closure.", | |
| 33 | + }, | |
| 34 | +] as const; | |
| 35 | + | |
| 36 | +export type LifecycleLabel = (typeof lifecycle)[number]["label"]; | |
| 37 | + | |
| 38 | +export const STALE_UPVOTE_THRESHOLD = 10; |
| @@ -1,0 +1,53 @@ | ||
| 1 | +#!/usr/bin/env bun | |
| 2 | + | |
| 3 | +// Posts a comment when a lifecycle label is applied to an issue, | |
| 4 | +// giving the author a heads-up and a chance to respond before auto-close. | |
| 5 | + | |
| 6 | +import { lifecycle } from "./issue-lifecycle.ts"; | |
| 7 | + | |
| 8 | +const DRY_RUN = process.argv.includes("--dry-run"); | |
| 9 | +const token = process.env.GITHUB_TOKEN; | |
| 10 | +const repo = process.env.GITHUB_REPOSITORY; // owner/repo | |
| 11 | +const label = process.env.LABEL; | |
| 12 | +const issueNumber = process.env.ISSUE_NUMBER; | |
| 13 | + | |
| 14 | +if (!DRY_RUN && !token) throw new Error("GITHUB_TOKEN required"); | |
| 15 | +if (!repo) throw new Error("GITHUB_REPOSITORY required"); | |
| 16 | +if (!label) throw new Error("LABEL required"); | |
| 17 | +if (!issueNumber) throw new Error("ISSUE_NUMBER required"); | |
| 18 | + | |
| 19 | +const entry = lifecycle.find((l) => l.label === label); | |
| 20 | +if (!entry) { | |
| 21 | + console.log(`No lifecycle entry for label "${label}", skipping`); | |
| 22 | + process.exit(0); | |
| 23 | +} | |
| 24 | + | |
| 25 | +const body = `${entry.nudge} This issue will be closed automatically if there's no activity within ${entry.days} days.`; | |
| 26 | + | |
| 27 | +// -- | |
| 28 | + | |
| 29 | +if (DRY_RUN) { | |
| 30 | + console.log(`Would comment on #${issueNumber} for label "${label}":\n\n${body}`); | |
| 31 | + process.exit(0); | |
| 32 | +} | |
| 33 | + | |
| 34 | +const response = await fetch( | |
| 35 | + `https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`, | |
| 36 | + { | |
| 37 | + method: "POST", | |
| 38 | + headers: { | |
| 39 | + Authorization: `Bearer ${token}`, | |
| 40 | + Accept: "application/vnd.github.v3+json", | |
| 41 | + "Content-Type": "application/json", | |
| 42 | + "User-Agent": "lifecycle-comment", | |
| 43 | + }, | |
| 44 | + body: JSON.stringify({ body }), | |
| 45 | + } | |
| 46 | +); | |
| 47 | + | |
| 48 | +if (!response.ok) { | |
| 49 | + const text = await response.text(); | |
| 50 | + throw new Error(`GitHub API ${response.status}: ${text}`); | |
| 51 | +} | |
| 52 | + | |
| 53 | +console.log(`Commented on #${issueNumber} for label "${label}"`); |
| @@ -1,23 +1,15 @@ | ||
| 1 | 1 | #!/usr/bin/env bun |
| 2 | 2 | |
| 3 | +import { lifecycle, STALE_UPVOTE_THRESHOLD } from "./issue-lifecycle.ts"; | |
| 4 | + | |
| 3 | 5 | // -- |
| 4 | 6 | |
| 5 | 7 | const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose"; |
| 6 | 8 | const DRY_RUN = process.argv.includes("--dry-run"); |
| 7 | -const STALE_DAYS = 14; | |
| 8 | -const STALE_UPVOTE_THRESHOLD = 10; | |
| 9 | 9 | |
| 10 | 10 | const CLOSE_MESSAGE = (reason: string) => |
| 11 | 11 | `Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`; |
| 12 | 12 | |
| 13 | -const lifecycle = [ | |
| 14 | - { label: "invalid", days: 3, reason: "this doesn't appear to be about Claude Code" }, | |
| 15 | - { label: "needs-repro", days: 7, reason: "we still need reproduction steps to investigate" }, | |
| 16 | - { label: "needs-info", days: 7, reason: "we still need a bit more information to move forward" }, | |
| 17 | - { label: "stale", days: 14, reason: "inactive for too long" }, | |
| 18 | - { label: "autoclose", days: 14, reason: "inactive for too long" }, | |
| 19 | -]; | |
| 20 | - | |
| 21 | 13 | // -- |
| 22 | 14 | |
| 23 | 15 | async function githubRequest<T>( |
| @@ -51,12 +43,13 @@ | ||
| 51 | 43 | // -- |
| 52 | 44 | |
| 53 | 45 | async function markStale(owner: string, repo: string) { |
| 46 | + const staleDays = lifecycle.find((l) => l.label === "stale")!.days; | |
| 54 | 47 | const cutoff = new Date(); |
| 55 | - cutoff.setDate(cutoff.getDate() - STALE_DAYS); | |
| 48 | + cutoff.setDate(cutoff.getDate() - staleDays); | |
| 56 | 49 | |
| 57 | 50 | let labeled = 0; |
| 58 | 51 | |
| 59 | - console.log(`\n=== marking stale (${STALE_DAYS}d inactive) ===`); | |
| 52 | + console.log(`\n=== marking stale (${staleDays}d inactive) ===`); | |
| 60 | 53 | |
| 61 | 54 | for (let page = 1; page <= 10; page++) { |
| 62 | 55 | const issues = await githubRequest<any[]>( |