spacehub

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

Add daily sweep to enforce issue lifecycle label timeoutsBrowse files
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.
Chris Lloyd authored on Feb 12, 2026 parent 0a0135f · commit a17040212cb5
2 changed files+137 -0compared with 0a0135f
A.github/workflows/sweep.yml
Ascripts/sweep.ts
@@ -1,0 +1,31 @@
1+name: "Daily Issue Sweep"
2+
3+on:
4+ schedule:
5+ - cron: "0 10 * * *" # 2am Pacific
6+ workflow_dispatch:
7+
8+permissions:
9+ issues: write
10+
11+concurrency:
12+ group: daily-issue-sweep
13+
14+jobs:
15+ sweep:
16+ runs-on: ubuntu-latest
17+ steps:
18+ - name: Checkout repository
19+ uses: actions/checkout@v4
20+
21+ - name: Setup Bun
22+ uses: oven-sh/setup-bun@v2
23+ with:
24+ bun-version: latest
25+
26+ - name: Enforce lifecycle timeouts
27+ run: bun run scripts/sweep.ts
28+ env:
29+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+ GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
31+ GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
@@ -1,0 +1,106 @@
1+#!/usr/bin/env bun
2+
3+// --
4+
5+const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
6+const DRY_RUN = process.argv.includes("--dry-run");
7+
8+const lifecycle = [
9+ { label: "needs-repro", days: 7 },
10+ { label: "needs-info", days: 7 },
11+ { label: "needs-votes", days: 30 },
12+ { label: "stale", days: 30 },
13+];
14+
15+const closeMessages: Record<string, string> = {
16+ "needs-repro": `Closing — we weren't able to get the reproduction steps needed to investigate.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with steps to reproduce.`,
17+ "needs-info": `Closing — we didn't receive the information needed to move forward.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with the requested details.`,
18+ "needs-votes": `Closing this feature request — it didn't get enough community support to prioritize.\n\nIf you'd still like to see this, please [open a new feature request](${NEW_ISSUE}) with more context about the use case.`,
19+ stale: `Closing due to inactivity.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with up-to-date information.`,
20+};
21+
22+// --
23+
24+async function githubRequest<T>(
25+ endpoint: string,
26+ method = "GET",
27+ body?: unknown
28+): Promise<T> {
29+ const token = process.env.GITHUB_TOKEN;
30+ if (!token) throw new Error("GITHUB_TOKEN required");
31+
32+ const response = await fetch(`https://api.github.com${endpoint}`, {
33+ method,
34+ headers: {
35+ Authorization: `Bearer ${token}`,
36+ Accept: "application/vnd.github.v3+json",
37+ "User-Agent": "sweep",
38+ ...(body && { "Content-Type": "application/json" }),
39+ },
40+ ...(body && { body: JSON.stringify(body) }),
41+ });
42+
43+ if (!response.ok) {
44+ if (response.status === 404) return {} as T;
45+ const text = await response.text();
46+ throw new Error(`GitHub API ${response.status}: ${text}`);
47+ }
48+
49+ return response.json();
50+}
51+
52+// --
53+
54+async function main() {
55+ const owner = process.env.GITHUB_REPOSITORY_OWNER;
56+ const repo = process.env.GITHUB_REPOSITORY_NAME;
57+ if (!owner || !repo)
58+ throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
59+
60+ if (DRY_RUN) console.log("DRY RUN — no issues will be closed\n");
61+
62+ let closed = 0;
63+
64+ for (const { label, days } of lifecycle) {
65+ const cutoff = new Date();
66+ cutoff.setDate(cutoff.getDate() - days);
67+ console.log(`\n=== ${label} (${days}d timeout) ===`);
68+
69+ for (let page = 1; page <= 10; page++) {
70+ const issues = await githubRequest<any[]>(
71+ `/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
72+ );
73+ if (issues.length === 0) break;
74+
75+ for (const issue of issues) {
76+ if (issue.pull_request) continue;
77+ const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
78+
79+ const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
80+
81+ const labeledAt = events
82+ .filter((e) => e.event === "labeled" && e.label?.name === label)
83+ .map((e) => new Date(e.created_at))
84+ .pop();
85+
86+ if (!labeledAt || labeledAt > cutoff) continue;
87+
88+ if (DRY_RUN) {
89+ const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
90+ console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
91+ } else {
92+ await githubRequest(`${base}/comments`, "POST", { body: closeMessages[label] });
93+ await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
94+ console.log(`#${issue.number}: closed (${label})`);
95+ }
96+ closed++;
97+ }
98+ }
99+ }
100+
101+ console.log(`\nDone: ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
102+}
103+
104+main().catch(console.error);
105+
106+export {};