Compare commits

...
Author SHA1 Message Date
Claude b8ebb08354 simplify issue triage: single type system, selective comments, remove scripts
- Replace boolean fields (is_support_question, is_feature_suggestion) with
  a single issue_type: Bug, Support, Feature, Task
- Map "Support" to existing "Troubleshooting" GitHub issue type
- Comments are now selective: only posted when Claude has something specific
  (duplicate found, missing repro steps, feature→discussion, code pointer)
- Reduce from 10 to 5 max turns (enough for focused searches)
- Remove support label (replaced by issue type)
- Delete one-time scripts (issue-backfill-triage.sh, issue-cleanup-labels.sh)
  — user has copy-paste commands to run locally

https://claude.ai/code/session_012t1J7hF5Th74qF9Fcn8h3h
2026-03-18 10:40:52 +00:00
Claude fd79aa0bda refactor: overhaul issue triage — remove stale bot, bounties, add code-aware responses
- Delete issue-stale.yml: closing issues without reason isn't helpful
- Remove bounty label references (no longer doing bounties)
- Redesign issue-triage.yml:
  - Use sonnet (not haiku) with 10 turns and full repo checkout
  - Claude searches the actual codebase (Glob/Grep/Read) to find
    relevant code before responding
  - Posts substantive, helpful comments with code pointers instead
    of just labels
  - Drop auto-priority and auto-size labels (Claude lacks context
    for these — maintainers should set them)
  - Security: read-only tools only (no Bash/Edit/Write), no
    contents:write permission — safe against prompt injection on
    a public repo
- Update backfill script to match new label schema
- Add bounty + stale labels to cleanup script deletion list

https://claude.ai/code/session_012t1J7hF5Th74qF9Fcn8h3h
2026-03-18 09:38:10 +00:00
Claude ea30dbcd99 feat: comprehensive issue management with auto-triage, cleanup, and stale bot
Auto-triage workflow (issue-triage.yml):
- Classifies new issues using Claude Haiku (single turn, low cost)
- Sets GitHub issue types (Bug/Feature/Task) via GraphQL API
- Applies scope, priority, and size labels automatically
- Detects potential duplicates by comparing against open issues
- Guides support questions to Discord/Discussions
- Flags feature suggestions for discussion conversion
- Requests reproduction steps for unconfirmed bugs

Stale issue workflow (issue-stale.yml):
- Marks issues stale after 90 days of inactivity
- Auto-closes after 14 more days
- Exempts critical, high priority, and bounty issues

Issue template updates:
- Remove all "type: *" labels from templates (replaced by issue types)
- Auto-triage handles classification for all templates uniformly

One-time scripts (scripts/):
- issue-cleanup-labels.sh: deletes obsolete type labels and typos
- issue-backfill-triage.sh: triages existing 154 unlabeled issues

https://claude.ai/code/session_012t1J7hF5Th74qF9Fcn8h3h
2026-03-18 09:24:18 +00:00
Claude 6414d6d41d feat: add auto-triage and stale issue management workflows
- Add issue-triage.yml: auto-labels new issues using Claude Haiku
  (classifies type, scope, priority, size, and good-first-issue)
- Add issue-stale.yml: marks inactive issues as stale after 90 days,
  auto-closes after 14 more days (exempts critical/high/bounty)
- Fix feature request template to auto-apply 'type: feature' label

Addresses the problem of 74% of open issues being completely unlabeled.

https://claude.ai/code/session_012t1J7hF5Th74qF9Fcn8h3h
2026-03-18 09:15:19 +00:00
4 changed files with 266 additions and 3 deletions
@@ -2,7 +2,7 @@
name: Add new feature / behavior
about: Describe the desired product increment
title: 'Ex: Add custom field from Companies / People table options menu'
labels: ''
labels: []
assignees: ''
---
+1 -1
View File
@@ -2,7 +2,7 @@
name: Report a bug
about: Report a bug or a functional regression
title: 'Ex: In DarkMode, a blank square appears in bottom right corner while scrolling'
labels: ['type: bug']
labels: []
assignees: ''
---
@@ -2,7 +2,7 @@
name: Request technical chore
about: Request technical work that does not provide any product increment (aka refactoring)
title: ''
labels: ['type: chore']
labels: []
assignees: ''
---
+263
View File
@@ -0,0 +1,263 @@
name: Auto-Triage Issues
on:
issues:
types: [opened]
jobs:
triage:
# Skip if issue mentions @claude (handled by claude.yml)
if: |
!contains(github.event.issue.title, '@claude') &&
!contains(github.event.issue.body || '', '@claude')
runs-on: ubuntu-latest
permissions:
issues: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Fetch repo context for triage
id: context
uses: actions/github-script@v7
with:
script: |
// Fetch recent open issues for duplicate detection
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 50,
sort: 'created',
direction: 'desc',
});
const issueList = issues.data
.filter(i => i.number !== context.payload.issue.number && !i.pull_request)
.map(i => {
const body = (i.body || '').substring(0, 200).replace(/\n/g, ' ');
return `#${i.number}: ${i.title} — ${body}`;
})
.join('\n');
core.setOutput('open_issues', issueList);
// Fetch available issue types via GraphQL
try {
const result = await github.graphql(`
{
repository(owner: "${context.repo.owner}", name: "${context.repo.repo}") {
issueTypes(first: 20) {
nodes {
id
name
description
}
}
}
}
`, {
headers: { 'GraphQL-Features': 'issue_types' }
});
const types = result.repository.issueTypes.nodes;
core.setOutput('issue_types', JSON.stringify(types));
} catch (e) {
console.log('Could not fetch issue types:', e.message);
core.setOutput('issue_types', '[]');
}
- name: Triage issue with Claude
id: triage
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
direct_prompt: |
You are an issue triager for Twenty, an open-source CRM (https://github.com/twentyhq/twenty).
You have read-only access to the codebase. Your primary job is to CLASSIFY issues accurately.
You may also search the code to find relevant context when it helps.
## Issue to analyze
Issue #${{ github.event.issue.number }}
Title: ${{ github.event.issue.title }}
Body:
${{ github.event.issue.body }}
Available issue types in this repo:
${{ steps.context.outputs.issue_types }}
Recent open issues (for duplicate check):
${{ steps.context.outputs.open_issues }}
## Classification
Determine the following:
**issue_type** — one of:
- "Bug": something is broken or not working as expected, WITH evidence (error messages, screenshots, clear reproduction steps)
- "Support": help request, troubleshooting, "how do I...", or a report that LOOKS like a bug but lacks evidence or reproduction steps
- "Feature": a new capability, enhancement, or product suggestion
- "Task": refactoring, cleanup, tech debt, documentation, or internal work
- null: if truly unclear
**scope** — one of: "front" (React/UI), "backend" (NestJS/API/DB), "back+front" (both), "infra" (CI/CD/deploy), "self-host" (Docker/self-hosted), or null
**good_first_issue** — true only if the fix is clearly straightforward, well-scoped, and good for newcomers
**needs_reproduction** — true for bugs that lack clear steps to reproduce
**duplicate_of** — an existing issue number ONLY if you are confident this is clearly about the same problem. Do not set for merely related issues.
**comment** — a markdown string or null. Default to null. Only set this when you have something genuinely specific and useful to say:
- You found a likely duplicate (explain which issue and why you think so)
- A bug is missing reproduction steps (ask for specifics relevant to this issue)
- A feature is significant enough to suggest moving to a GitHub Discussion
- You searched the codebase and found a confident, specific code pointer relevant to the issue (file path + brief explanation)
Do NOT post generic comments. Most issues just need labels, not a bot comment.
## How to search the codebase
You can use Glob, Grep, and Read to search for relevant code when it helps classification or when you want to provide a specific code pointer in the comment. For example:
- Grep for error messages or component names mentioned in the issue
- Search for the feature area being discussed
Keep searches focused — you have limited turns.
## Output
After your analysis, output ONLY a JSON object with no markdown fencing:
{
"issue_type": "<Bug|Support|Feature|Task|null>",
"scope": "<front|backend|back+front|infra|self-host|null>",
"good_first_issue": <boolean>,
"needs_reproduction": <boolean>,
"duplicate_of": <number or null>,
"comment": "<markdown string or null>"
}
claude_args: '--max-turns 5 --model sonnet --allowedTools "Glob,Grep,Read"'
- name: Apply triage results
if: steps.triage.outputs.result
uses: actions/github-script@v7
with:
script: |
let triage;
try {
const raw = `${{ steps.triage.outputs.result }}`;
const jsonMatch = raw.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
console.log('No JSON found in Claude response:', raw);
return;
}
triage = JSON.parse(jsonMatch[0]);
} catch (e) {
console.log('Failed to parse triage response:', e.message);
return;
}
const issueNumber = context.payload.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// 1. Set issue type via GraphQL
if (triage.issue_type) {
try {
const issueTypes = JSON.parse(`${{ steps.context.outputs.issue_types }}`);
// Map "Support" to the "Troubleshooting" GitHub issue type
const typeName = triage.issue_type === 'Support' ? 'Troubleshooting' : triage.issue_type;
const matchedType = issueTypes.find(t => t.name === typeName);
if (matchedType) {
const issueData = await github.graphql(`
{
repository(owner: "${owner}", name: "${repo}") {
issue(number: ${issueNumber}) {
id
}
}
}
`);
const issueNodeId = issueData.repository.issue.id;
await github.graphql(`
mutation {
updateIssue(input: {
id: "${issueNodeId}",
issueTypeId: "${matchedType.id}"
}) {
issue { number }
}
}
`, {
headers: { 'GraphQL-Features': 'issue_types' }
});
console.log(`Set issue type to: ${typeName}`);
}
} catch (e) {
console.log(`Could not set issue type: ${e.message}`);
}
}
// 2. Build label list
const labels = [];
const validScopes = ['front', 'backend', 'back+front', 'infra', 'self-host'];
if (triage.scope && validScopes.includes(triage.scope)) {
labels.push(triage.scope === 'self-host' ? 'scope: self-host' : `scope: ${triage.scope}`);
}
if (triage.good_first_issue === true) {
labels.push('good first issue');
}
if (triage.needs_reproduction === true) {
labels.push('needs: reproduction');
}
// 3. Ensure labels exist and apply them
if (labels.length > 0) {
const existingLabels = await github.rest.issues.listLabelsForRepo({
owner, repo, per_page: 100,
});
const existingLabelNames = new Set(existingLabels.data.map(l => l.name));
const labelColors = {
'needs: reproduction': 'fbca04',
'scope: self-host': 'bfd4f2',
};
for (const label of labels) {
if (!existingLabelNames.has(label)) {
try {
await github.rest.issues.createLabel({
owner, repo,
name: label,
color: labelColors[label] || 'ededed',
});
} catch (e) {
console.log(`Could not create label "${label}": ${e.message}`);
}
}
}
await github.rest.issues.addLabels({
owner, repo,
issue_number: issueNumber,
labels,
});
console.log(`Applied labels: ${labels.join(', ')}`);
}
// 4. Post comment only if Claude had something specific to say
if (triage.comment) {
const commentParts = [triage.comment];
if (triage.duplicate_of && Number.isInteger(triage.duplicate_of)) {
commentParts.push(`\n> **Possible duplicate** of #${triage.duplicate_of} — please check if this is the same issue.`);
}
await github.rest.issues.createComment({
owner, repo,
issue_number: issueNumber,
body: commentParts.join('\n'),
});
}