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'}
+ ${type}>
+
+ 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}`);
+ }