name: AI Triage on: issues: types: [opened] pull_request_target: types: [opened] permissions: issues: write pull-requests: write jobs: triage: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v6 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 SF 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 SF 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.`; if (!process.env.ANTHROPIC_API_KEY) { core.warning('Skipping AI triage because ANTHROPIC_API_KEY is not configured.'); return; } let result; try { 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: 1024, messages: [{ role: 'user', content: prompt }] }), signal: AbortSignal.timeout(20000) }); if (!response.ok) { const err = await response.text(); core.warning(`Skipping AI triage after 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.warning(`Skipping AI triage because the model response was not parseable JSON: ${text}`); return; } result = JSON.parse(jsonMatch[0]); } catch (e) { core.warning(`Skipping AI triage after unexpected failure: ${e.message}`); return; } 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/sf-build/SF/security/advisories/new) instead.** See [CONTRIBUTING.md](https://github.com/sf-build/SF/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/sf-build/SF/blob/main/VISION.md) and [CONTRIBUTING.md](https://github.com/sf-build/SF/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}`); }