From 4c081fa55609c7521126ee5e15c770b6c3f2b60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Thu, 19 Mar 2026 17:32:54 -0600 Subject: [PATCH] feat: AI-powered issue and PR triage via Claude Haiku (#1510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a GitHub Actions workflow that automatically triages new issues and PRs using Claude Haiku 4.5. Classifies with type and priority labels, and flags items that violate VISION.md or CONTRIBUTING.md guidelines with a `needs-review` label and explanatory comment. No auto-closing — maintainer makes all final decisions. Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/ai-triage.yml | 191 ++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 .github/workflows/ai-triage.yml diff --git a/.github/workflows/ai-triage.yml b/.github/workflows/ai-triage.yml new file mode 100644 index 000000000..e97b417e3 --- /dev/null +++ b/.github/workflows/ai-triage.yml @@ -0,0 +1,191 @@ +name: AI Triage + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + VISION.md + CONTRIBUTING.md + sparse-checkout-cone-mode: false + + - name: Triage with Claude + uses: actions/github-script@v7 + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + script: | + const fs = require('fs'); + const vision = fs.readFileSync('VISION.md', 'utf8'); + const contributing = fs.readFileSync('CONTRIBUTING.md', 'utf8'); + + const isIssue = !!context.payload.issue; + const item = context.payload.issue || context.payload.pull_request; + const number = item.number; + const title = item.title; + const body = item.body || ''; + const type = isIssue ? 'issue' : 'pull_request'; + const existingLabels = (item.labels || []).map(l => l.name); + + const prompt = `You are a GitHub issue/PR triage bot for the GSD-2 project. Your job is to: + 1. Classify the ${type} with appropriate labels + 2. Detect if it violates project guidelines + + + ${vision} + + + + ${contributing} + + + <${type}> + Title: ${title} + Body: ${body} + Existing labels: ${existingLabels.join(', ') || 'none'} + + + Available labels for classification (pick 1-3 that fit): + - bug: Something isn't working + - enhancement: New feature or request + - documentation: Improvements or additions to documentation + - performance: Performance improvement + - refactor: Code restructuring without behavior change + - tech-debt: Technical debt reduction + - question: Further information is requested + - good first issue: Good for newcomers + - needs-info: Waiting for reporter information + + Available priority labels (pick exactly 1 if you can assess priority): + - High Priority + - Medium Priority + - Low Priority + + Respond in this exact JSON format: + { + "labels": ["label1", "label2"], + "aligned": true, + "violation_type": null, + "explanation": null + } + + Set "aligned" to false if the ${type} clearly violates project guidelines. Violation types: + - "enterprise-patterns": Enterprise patterns (DI containers, abstract factories, etc.) + - "framework-swap": Rewriting working code in a different framework without measurable improvement + - "cosmetic-refactor": Pure formatting/renaming churn with no user value + - "complexity-without-value": Adds abstraction/indirection without user-visible improvement + - "heavy-orchestration": Duplicates what agent infrastructure already provides + - "missing-info": Issue is too vague to act on (no repro steps, no context) + - "off-topic": Not related to GSD-2 at all + - "security-in-public": Appears to report a security vulnerability publicly + + If aligned is false, provide a brief, polite explanation (2-3 sentences) of why this was flagged. + + Be generous in your assessment — only flag clear violations. Ambiguous cases should be marked as aligned. + Do NOT flag issues/PRs that are legitimately reporting bugs or requesting features, even if they could be better written.`; + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': process.env.ANTHROPIC_API_KEY, + 'content-type': 'application/json', + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: 'claude-haiku-4-5-20251001', + max_tokens: 300, + messages: [{ role: 'user', content: prompt }] + }) + }); + + if (!response.ok) { + const err = await response.text(); + core.setFailed(`Anthropic API error: ${response.status} ${err}`); + return; + } + + const data = await response.json(); + const text = data.content[0].text; + + // Extract JSON from response (handle markdown code blocks) + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + core.setFailed(`Could not parse Claude response: ${text}`); + return; + } + + const result = JSON.parse(jsonMatch[0]); + core.info(`Triage result: ${JSON.stringify(result, null, 2)}`); + + // Apply labels + const labelsToAdd = result.labels.filter(l => !existingLabels.includes(l)); + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + labels: labelsToAdd + }); + core.info(`Added labels: ${labelsToAdd.join(', ')}`); + } + + // Flag misaligned issues/PRs + if (!result.aligned) { + // Add needs-review label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + labels: ['needs-review'] + }); + + const violationNames = { + 'enterprise-patterns': 'Enterprise patterns', + 'framework-swap': 'Framework swap', + 'cosmetic-refactor': 'Cosmetic refactor', + 'complexity-without-value': 'Complexity without user value', + 'heavy-orchestration': 'Heavy orchestration layer', + 'missing-info': 'Missing information', + 'off-topic': 'Off-topic', + 'security-in-public': 'Security report in public' + }; + + const securityNote = result.violation_type === 'security-in-public' + ? `\n\n**If this is a security vulnerability, please delete this ${type} and use [GitHub\'s private vulnerability reporting](https://github.com/gsd-build/GSD-2/security/advisories/new) instead.** See [CONTRIBUTING.md](https://github.com/gsd-build/GSD-2/blob/main/CONTRIBUTING.md#security) for details.` + : ''; + + const comment = `👋 Thanks for opening this ${type}! + + This was automatically flagged for maintainer review. + + **Flag:** ${violationNames[result.violation_type] || result.violation_type} + + ${result.explanation} + + Please review our [VISION.md](https://github.com/gsd-build/GSD-2/blob/main/VISION.md) and [CONTRIBUTING.md](https://github.com/gsd-build/GSD-2/blob/main/CONTRIBUTING.md) for project guidelines.${securityNote} + + A maintainer will review this shortly. If you believe this was flagged in error, no action is needed — we'll take a look. + + --- + *This is an automated triage. The maintainers make all final decisions.*`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: comment + }); + core.info(`Flagged as misaligned: ${result.violation_type}`); + }