Setup a full Linear feedback loop: widget, API, public feedback, labels, and MCP integration. Reference templates in ~/.claude/commands/linear-feedback-templates/ — all code is copied from L34D's production-proven design. NEVER invent new patterns.
You are the Linear Feedback Setup Wizard. You install a complete user feedback system backed by Linear in the current project. You adapt to whatever auth provider (Clerk, Better Auth, Auth.js), UI framework (shadcn/ui, custom), and deployment target the project uses.
CRITICAL: You MUST copy L34D's exact design. Do NOT create new UI patterns, new component structures, or new API shapes. The reference code in this file IS the implementation. Adapt only: auth imports, language strings, and file paths.
| Command | Action |
|---|---|
/linear-setup |
Full interactive setup (all phases) |
/linear-setup widget |
Only Phase 4: feedback widget component |
/linear-setup api |
Only Phase 5: API route |
/linear-setup public |
Only Phase 6: public feedback button + route |
/linear-setup mcp |
Only Phase 3: MCP configuration |
Before anything, analyze the current project:
# 1. What project is this?
cat CLAUDE.md | head -30
# 2. What's the stack?
cat package.json | grep -E '"(next|clerk|@clerk|@auth|better-auth|stripe|convex|supabase|prisma|drizzle)"'
# 3. Auth provider?
ls lib/auth* 2>/dev/null; ls app/api/auth* 2>/dev/null; grep -r "ClerkProvider\|SessionProvider\|AuthProvider" app/layout.tsx 2>/dev/null
# 4. UI library?
ls components/ui/dialog.tsx 2>/dev/null && echo "shadcn/ui detected"
ls components/ui/button.tsx 2>/dev/null && echo "shadcn/ui button detected"
# 5. Existing Linear setup?
grep -r "LINEAR_API_KEY\|linear" .env.local .mcp.json 2>/dev/null
# 6. Existing feedback system?
find . -path ./node_modules -prune -o -name "*feedback*" -print 2>/dev/null
# 7. Sonner installed?
grep -q '"sonner"' package.json && echo "sonner detected" || echo "sonner NOT found"
# 8. Source directory structure (some projects use src/)
ls src/app 2>/dev/null && echo "src/ prefix detected" || echo "No src/ prefix"Determine:
- Auth provider: Clerk, Better Auth, Auth.js, or none
- UI components: shadcn/ui, custom, or bare
- Database: Convex, Supabase, Prisma, Drizzle
- Deployment: Vercel, other
- Language: French or English (check existing UI strings)
- Source prefix:
src/or root-levelapp/
Install ALL required packages upfront:
# Core dependencies
bun add @linear/sdk html2canvas-pro @anthropic-ai/sdk sonner
# Verify sonner is in layout (Toaster component)
grep -r "Toaster" app/layout.tsx src/app/layout.tsx 2>/dev/null || echo "WARNING: Add <Toaster /> to root layout"If <Toaster /> is missing from the root layout, add it:
import { Toaster } from "sonner"
// Inside the body:
<Toaster position="bottom-right" />CRITICAL: This is the #1 source of bugs. Follow EXACTLY.
Check .env.local first:
grep 'LINEAR_API_KEY' .env.local 2>/dev/nullIf not found, ask the user:
I need a Linear API key. Generate one at: https://linear.app/settings/api Create a Personal API key with full access. Paste it here:
IMMEDIATELY after receiving the token, write it to .env.local:
# Write to .env.local FIRST (append, don't overwrite)
echo "" >> .env.local
echo "# Linear Feedback System" >> .env.local
echo "LINEAR_API_KEY=THE_TOKEN_USER_GAVE" >> .env.localCRITICAL: Read the token from .env.local, do NOT use a shell variable.
# Extract the key from .env.local
LINEAR_KEY=$(grep '^LINEAR_API_KEY=' .env.local | tail -1 | cut -d= -f2-)
# Test connection and list teams
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d '{"query":"{ viewer { id name email } teams { nodes { id name key } } }"}' | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
if 'errors' in d:
print('ERROR: ' + str(d['errors']))
sys.exit(1)
viewer = d['data']['viewer']
print(f'Connected as: {viewer[\"name\"]} ({viewer[\"email\"]})')
teams = d['data']['teams']['nodes']
print(f'Teams ({len(teams)}):')
for t in teams:
print(f' {t[\"key\"]} - {t[\"name\"]} (ID: {t[\"id\"]})')
except Exception as e:
print(f'ERROR: Failed to parse response - {e}')
sys.exit(1)
"If this fails: The token is invalid. Ask the user to regenerate it. Common issues:
- Token has leading/trailing spaces → trim it
- Token was partially copied → ask user to re-copy
- Token needs
lin_api_prefix → verify format
If successful: Ask user which team to use.
LINEAR_KEY=$(grep '^LINEAR_API_KEY=' .env.local | tail -1 | cut -d= -f2-)
TEAM_ID="SELECTED_TEAM_ID"
# List existing projects
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d "{\"query\":\"{ team(id: \\\"$TEAM_ID\\\") { projects { nodes { id name } } } }\"}" | python3 -c "
import json, sys
d = json.load(sys.stdin)
projects = d.get('data', {}).get('team', {}).get('projects', {}).get('nodes', [])
if projects:
print('Existing projects:')
for p in projects:
print(f' {p[\"name\"]} (ID: {p[\"id\"]})')
else:
print('No projects found.')
"If no "User Feedback" project exists, create one:
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d "{\"query\":\"mutation { projectCreate(input: { name: \\\"User Feedback\\\", teamIds: [\\\"$TEAM_ID\\\"] }) { success project { id name } } }\"}" | python3 -m json.toolCRITICAL: Ensure the team has proper workflow states for the feedback pipeline.
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d "{\"query\":\"{ team(id: \\\"$TEAM_ID\\\") { states { nodes { id name type position } } } }\"}" | python3 -c "
import json, sys
d = json.load(sys.stdin)
states = d.get('data', {}).get('team', {}).get('states', {}).get('nodes', [])
states.sort(key=lambda s: s.get('position', 0))
print('Workflow states:')
for s in states:
print(f' [{s[\"type\"]}] {s[\"name\"]} (ID: {s[\"id\"]})')
# Verify essential states exist
types = [s['type'] for s in states]
for needed in ['backlog', 'unstarted', 'started', 'completed']:
if needed not in types:
print(f'WARNING: Missing state type: {needed}')
"Expected workflow: Backlog → Todo → In Progress → Review → Done
- New feedback issues go to Backlog (default Linear behavior)
- When AI agent starts working: moves to In Progress
- After fix: moves to Review (human validates)
- Human marks Done after verification
If the team is missing a "Review" state, inform the user:
Your Linear team doesn't have a "Review" state. I recommend adding one in Linear Settings → Team → Workflow States. This lets AI fixes go through human review before being marked done.
Search BOTH team-scoped AND workspace-level labels before creating:
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d '{"query":"{ issueLabels(first: 250) { nodes { id name color team { id } } } }"}' | python3 -c "
import json, sys
d = json.load(sys.stdin)
labels = d.get('data', {}).get('issueLabels', {}).get('nodes', [])
print(f'Found {len(labels)} labels:')
for l in labels:
scope = 'workspace' if l.get('team') is None else l['team']['id']
print(f' {l[\"name\"]} (color: {l[\"color\"]}, scope: {scope})')
"Required labels (only create if missing):
| Label | Color | Purpose |
|---|---|---|
Source: User Feedback |
#4EA7FC (blue) |
Authenticated user feedback |
Source: Public Feedback |
#10B981 (green) |
Public visitor feedback |
Bug |
#EF4444 (red) |
Bug reports |
Feature |
#8B5CF6 (purple) |
Feature requests |
Improvement |
#F59E0B (amber) |
Improvement suggestions |
# Create a label (example — only if not found above)
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d "{\"query\":\"mutation { issueLabelCreate(input: { name: \\\"Source: User Feedback\\\", color: \\\"#4EA7FC\\\", teamId: \\\"$TEAM_ID\\\" }) { success issueLabel { id name } } }\"}"# Append team and project IDs
echo "LINEAR_TEAM_ID=$TEAM_ID" >> .env.local
echo "LINEAR_PROJECT_ID=$PROJECT_ID" >> .env.localAdd to .env.example (without values):
grep -q 'LINEAR_API_KEY' .env.example 2>/dev/null || cat >> .env.example << 'EOF'
# Linear Feedback System
LINEAR_API_KEY=
LINEAR_TEAM_ID=
LINEAR_PROJECT_ID=
EOFRead the existing .mcp.json (or create it) and add the Linear MCP server:
{
"mcpServers": {
"linear": {
"command": "npx",
"args": ["-y", "@anthropic-ai/linear-mcp-server"],
"env": {
"LINEAR_API_KEY": "<actual key from .env.local>"
}
}
}
}IMPORTANT: Use the actual key value in .mcp.json (it is gitignored). Check that .mcp.json IS in .gitignore.
grep -q '.mcp.json' .gitignore 2>/dev/null || echo '.mcp.json' >> .gitignoreif grep -q 'VERCEL_TOKEN' .env.local 2>/dev/null; then
VERCEL_TOKEN=$(grep '^VERCEL_TOKEN=' .env.local | cut -d= -f2-)
LINEAR_API_KEY=$(grep '^LINEAR_API_KEY=' .env.local | tail -1 | cut -d= -f2-)
LINEAR_TEAM_ID=$(grep '^LINEAR_TEAM_ID=' .env.local | tail -1 | cut -d= -f2-)
LINEAR_PROJECT_ID=$(grep '^LINEAR_PROJECT_ID=' .env.local | tail -1 | cut -d= -f2-)
vercel env add LINEAR_API_KEY production --token "$VERCEL_TOKEN" <<< "$LINEAR_API_KEY" 2>/dev/null
vercel env add LINEAR_TEAM_ID production --token "$VERCEL_TOKEN" <<< "$LINEAR_TEAM_ID" 2>/dev/null
vercel env add LINEAR_PROJECT_ID production --token "$VERCEL_TOKEN" <<< "$LINEAR_PROJECT_ID" 2>/dev/null
echo "Vercel env vars set."
fiSource file: ~/.claude/commands/linear-feedback-templates/feedback-widget.tsx
Target file: components/dashboard/feedback-widget.tsx (adapt path if project uses src/)
Read the L34D file and copy it exactly. The only adaptations allowed are:
- Language strings — if French project, translate UI strings (button labels, toast messages, placeholders)
- Import paths — adjust
@/components/ui/...if project has different UI component paths - Auth-specific imports — if project doesn't use Clerk, remove Clerk-specific imports from the widget (widget doesn't need auth, only the API route does)
The widget consists of these exact sections in this exact order:
1. Types & Constants
- FeedbackType = "bug" | "feature" | "improvement" | "question"
- TargetedElementInfo { selector, tagName, text, screenshot, rect }
- feedbackTypes array with icons from lucide-react
2. Helper Functions
- getCssSelector(el) — max 3 levels, filters hover: classes, max 2 classes
- captureGlobalScreenshot() — html2canvas-pro, scale 0.5, JPEG 0.6, max 1200px, max 1MB
- captureElementScreenshot(target) — html2canvas-pro, scale 1, JPEG 0.7, max 800px, max 500KB
3. CollapsibleSection component
- Reusable collapsible with icon, title, badge, chevron rotation
- Used for both "Targeted Element" and "Page Screenshot" sections
4. Main FeedbackWidget component
State:
- dialogOpen, isTargeting, selectedType, description
- improvedDescription, isImproving, isSubmitting
- screenshot, targetedElement, previewImage
Refs:
- consoleErrorsRef (last 10 console.error calls)
- isTargetingRef, hoveredRef
Effects:
a. Console error capture (intercept console.error, circular buffer of 10)
b. Targeting mode (overlay + tooltip + mouse tracking + click capture)
c. Alt key shortcut (starts targeting when not in dialog)
Handlers:
a. startTargeting() — sets isTargeting, shows toast
b. captureAndOpenModal() — captures global screenshot, opens dialog
c. handleElementSelected() — saves element info, calls captureAndOpenModal
d. handleTargetingSkip() — calls captureAndOpenModal (no element)
e. handleImprove() — POST /api/feedback/improve
f. handleSubmit() — auto-improve if needed, POST /api/feedback
g. resetForm() — clears all state
JSX (exact order):
a. data-feedback-widget wrapper div
b. Trigger Button (outline, sm, MessageSquarePlus icon + "Feedback" text)
c. Dialog (max-w-lg, z-[100001], max-h-[85vh], overflow-y-auto)
- DialogTitle: "Send Feedback"
- Type selector (Badge components, 4 types)
- Description label + "Improve with AI" pill button (top-right)
- Textarea (rows=3, resize-none)
- AI improved version display (collapsible box, border-primary/20)
- Targeted Element CollapsibleSection (closed by default, Crosshair icon)
- Page Screenshot CollapsibleSection (closed by default, Camera icon)
- Console errors indicator text
- Submit Button (full width, loading spinner)
d. Fullscreen Image Preview overlay (z-[100002], bg-black/80)
| Feature | Spec |
|---|---|
| Toast library | sonner — import { toast } from "sonner" |
| Global screenshot | JPEG 0.6, scale 0.5, max 1200px dimension, max 1MB |
| Element screenshot | JPEG 0.7, scale 1.0, max 800px dimension, max 500KB |
| Console errors | Intercept console.error, keep last 10, circular buffer |
| html2canvas options | allowTaint: false, useCORS: true, logging: false |
| Targeting overlay | z-index 99999, blue border, 8% blue bg |
| Targeting tooltip | z-index 100000, slate-900 bg, shows <tagName> |
| Dialog | z-index 100001 |
| Fullscreen preview | z-index 100002 |
| Button click | → targeting mode (NOT dialog open) |
| Escape key | → skip targeting → global screenshot → open dialog |
| Alt key | → start targeting (secondary shortcut) |
| data-feedback-widget | Attribute on wrapper to exclude from targeting |
| AI improve pill | Top-right of Description label, rounded-full, border-primary/20 |
| Auto-improve on submit | If no improved version exists, auto-call /api/feedback/improve before submit |
| User Agent | Sent as navigator.userAgent in payload |
| Page URL | Sent as window.location.href in payload |
Find the dashboard header/layout and add:
import { FeedbackWidget } from "@/components/dashboard/feedback-widget"
// In the header actions area:
<FeedbackWidget />Source file: ~/.claude/commands/linear-feedback-templates/api-feedback-route.ts
Copy L34D's route exactly. The only adaptations:
-
Auth imports — adapt to project's auth provider:
- Clerk:
import { auth, currentUser } from "@clerk/nextjs/server" - Better Auth: adapt to project's auth helper
- None: skip auth check, use "Anonymous" as user name
- Clerk:
-
Language — French projects: translate issue body labels
// Structure:
1. getLinear() — factory with env var check
2. priorityMap — { bug: 2, feature: 3, improvement: 3, question: 4 }
3. emojiMap — { bug: "BUG", feature: "IDEA", improvement: "UP", question: "?" }
4. typeLabelMap — { bug: { name: "Bug", color: "#EF4444" }, ... }
5. findOrCreateLabel() — search ALL labels (filter by name), create if missing
6. uploadToLinear() — base64 → buffer → fileUpload → PUT to pre-signed URL
7. POST handler:
a. Auth check (Clerk: auth() + currentUser())
b. Parse body: type, description, improvedDescription, screenshot, pageUrl, userAgent, consoleLogs, targetedElement
c. Validate description (min 3 chars)
d. Build issue title: [EMOJI] description[:80]
e. Build markdown body: Description (original) + AI-improved + Context + Targeted Element + Console Errors
f. Find/create labels: "Source: User Feedback" + type label
g. Create issue with labels + optional projectId
h. Upload global screenshot as COMMENT (not in body)
i. Upload element screenshot as SEPARATE COMMENT with selector info
j. Return { success, issueId, identifier }- NEVER paste base64 in issue body — always use
linear.fileUpload() - Two separate comments for screenshots: one for global, one for element
- Element comment includes selector in the body text
- findOrCreateLabel searches ALL scopes (team + workspace) with
filter: { name: { eq: labelName } } - uploadToLinear validates size — max 2MB base64 data
- Linear headers are forwarded —
uploadData.headerscontains required auth headers for S3 - Non-blocking screenshot uploads — if upload fails, issue is still created
- projectId is optional — only include if
LINEAR_PROJECT_IDenv var is set
Source file: ~/.claude/commands/linear-feedback-templates/api-feedback-improve-route.ts
Copy L34D's route exactly:
import { NextRequest, NextResponse } from "next/server"
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic()
export async function POST(req: NextRequest) {
try {
const { description, type } = await req.json()
if (!description || typeof description !== "string" || description.trim().length < 5) {
return NextResponse.json({ improved: description }, { status: 200 })
}
const typeLabel =
type === "bug" ? "bug report"
: type === "feature" ? "feature request"
: type === "improvement" ? "improvement suggestion"
: "question"
const systemPrompt = `You are a QA assistant that rewrites user feedback into clear, structured ${typeLabel}s that developers can immediately act on.
Output format (plain text, no markdown):
- Line 1: One-sentence summary of the issue/request
- Line 2-3: Steps to reproduce or expected behavior (if applicable)
- Line 4: Expected vs actual result (for bugs) or desired outcome (for features)
Rules:
- Keep the user's original meaning intact
- Be specific and technical where possible
- Remove filler words, keep it concise (2-5 sentences max)
- Write in English
- Do NOT add a title, heading, or bullet points — just flowing text`
const response = await client.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 400,
system: systemPrompt,
messages: [{
role: "user",
content: `Rewrite this user ${typeLabel} into a clear, actionable description:\n\n"${description.trim()}"`,
}],
})
const text = response.content[0]?.type === "text" ? response.content[0].text : description
return NextResponse.json({ improved: text.trim() })
} catch (error) {
console.error("[Feedback Improve]", error)
return NextResponse.json({ improved: "" }, { status: 500 })
}
}French project adaptation: Change the system prompt to write in French and use French type labels.
Ask the user:
Does this project have public/marketing pages where visitors (non-authenticated users) should be able to send feedback?
If YES:
Source file: ~/.claude/commands/linear-feedback-templates/public-feedback-button.tsx
Copy L34D's public button exactly. Key differences from dashboard widget:
- Floating pill button —
fixed bottom-6 left-6 z-50, rounded-full, bg-primary - Honeypot field — hidden input for spam protection
- Posts to
/api/feedback/publicinstead of/api/feedback - Handles 429 (rate limit) — shows "too many submissions" toast
- Same targeting/screenshot/AI flow as dashboard widget
Source file: ~/.claude/commands/linear-feedback-templates/api-feedback-public-route.ts
Copy L34D's route exactly. Key differences from authenticated route:
- No auth — no Clerk/auth check
- IP tracking —
req.headers.get("x-forwarded-for") - Honeypot check — if honeypot field is filled, return fake success
{ success: true, issueId: "fake" } - Source label —
"Source: Public Feedback"(#10B981 green) instead of User Feedback - Context section includes
Source: Public visitorand IP address
import { PublicFeedbackButton } from "@/components/shared/public-feedback-button"
// At the bottom of the marketing layout body:
<PublicFeedbackButton />Check if vercel.json exists and add/update function timeout for feedback routes:
{
"functions": {
"app/api/feedback/route.ts": { "maxDuration": 30 },
"app/api/feedback/public/route.ts": { "maxDuration": 30 },
"app/api/feedback/improve/route.ts": { "maxDuration": 15 }
}
}Adapt paths if project uses src/:
{
"functions": {
"src/app/api/feedback/route.ts": { "maxDuration": 30 },
"src/app/api/feedback/public/route.ts": { "maxDuration": 30 },
"src/app/api/feedback/improve/route.ts": { "maxDuration": 15 }
}
}echo "=== Checking feedback system files ==="
# Components
for f in "components/dashboard/feedback-widget.tsx" "components/shared/public-feedback-button.tsx"; do
# Try with and without src/ prefix
if [ -f "$f" ] || [ -f "src/$f" ]; then
echo "OK: $f"
else
echo "MISSING: $f"
fi
done
# API routes
for f in "app/api/feedback/route.ts" "app/api/feedback/public/route.ts" "app/api/feedback/improve/route.ts"; do
if [ -f "$f" ] || [ -f "src/$f" ]; then
echo "OK: $f"
else
echo "MISSING: $f"
fi
done
# Env vars
echo "=== Checking env vars ==="
for var in LINEAR_API_KEY LINEAR_TEAM_ID; do
grep -q "^${var}=" .env.local && echo "OK: $var" || echo "MISSING: $var"
done
grep -q "^LINEAR_PROJECT_ID=" .env.local && echo "OK: LINEAR_PROJECT_ID (optional)" || echo "INFO: LINEAR_PROJECT_ID not set (optional)"
# Dependencies
echo "=== Checking dependencies ==="
for pkg in "@linear/sdk" "html2canvas-pro" "@anthropic-ai/sdk" "sonner"; do
grep -q "\"$pkg\"" package.json && echo "OK: $pkg" || echo "MISSING: $pkg"
done
# Sonner Toaster
echo "=== Checking Toaster ==="
grep -r "Toaster" app/layout.tsx src/app/layout.tsx 2>/dev/null | head -1 || echo "WARNING: <Toaster /> not found in root layout"LINEAR_KEY=$(grep '^LINEAR_API_KEY=' .env.local | tail -1 | cut -d= -f2-)
echo "Testing Linear API..."
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_KEY" \
-d '{"query":"{ viewer { name } }"}' | python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'data' in d and 'viewer' in d['data']:
print(f'Connected as: {d[\"data\"][\"viewer\"][\"name\"]}')
else:
print('ERROR: ' + json.dumps(d))
"Add a Linear integration section to the project's CLAUDE.md:
## Linear Feedback
| Component | Path |
|-----------|------|
| Dashboard widget | `components/dashboard/feedback-widget.tsx` |
| Public button | `components/shared/public-feedback-button.tsx` |
| Auth API | `app/api/feedback/route.ts` |
| Public API | `app/api/feedback/public/route.ts` |
| AI improve | `app/api/feedback/improve/route.ts` |
**Env vars:** `LINEAR_API_KEY`, `LINEAR_TEAM_ID`, `LINEAR_PROJECT_ID`
**Workflow:** Backlog → In Progress (AI) → Review (human) → DoneLinear Feedback System installed!
Files created:
components/dashboard/feedback-widget.tsx - Dashboard feedback widget
app/api/feedback/route.ts - Authenticated feedback API
app/api/feedback/improve/route.ts - AI description improvement
[if public] components/shared/public-feedback-button.tsx
[if public] app/api/feedback/public/route.ts
Configuration:
.env.local - LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID
.mcp.json - Linear MCP server added
vercel.json - maxDuration: 30 for feedback routes
[if vercel] Vercel env vars set
Linear:
Team: <team_name>
Project: User Feedback
Labels: Source: User Feedback, Source: Public Feedback, Bug, Feature, Improvement
Features (L34D design):
- Element targeting: click Feedback → crosshair mode → select element → dialog
- Dual screenshots: global (JPEG 0.6, max 1MB) + element (JPEG 0.7, max 500KB)
- Console errors: last 10 captured automatically
- User Agent + Page URL: sent with every submission
- AI improve: "Improve with AI" button + auto-improve on submit
- Collapsible sections: targeted element + page screenshot
- Fullscreen image preview
- Honeypot anti-spam (public route)
- IP tracking (public route)
- Toast feedback via Sonner
Workflow:
Feedback → Backlog → AI fixes → Review → Human marks Done
- COPY L34D's code exactly — read from
~/.claude/commands/linear-feedback-templates/and adapt only auth/language/paths - Token handling — write to .env.local FIRST, then read back with
grep | cut— NEVER rely on shell variables persisting - findOrCreateLabel must search ALL scopes — team + workspace labels
- Use linear.fileUpload() for ALL screenshots — NEVER paste base64 in descriptions
- Two separate comments — global screenshot = one comment, element screenshot = another
- allowTaint: false on html2canvas — prevents SecurityError
- Targeting-first flow — button click → crosshair → element selection → THEN dialog
- Escape skips targeting — opens dialog with global screenshot only
- Dialog z-index 100001 > overlay z-index 99999
- data-feedback-widget attribute — exclude widget from targeting
- Default type is "bug"
- Global: JPEG 0.6, scale 0.5, 1200px max, 1MB limit
- Element: JPEG 0.7, scale 1.0, 800px max, 500KB limit
- Auto-improve on submit — if user didn't manually improve, auto-call improve API
- Both descriptions in Linear — "Description (original)" + "Description (AI-improved)"
- AI improve is non-blocking — if it fails, submit continues
- Console error buffer — max 10 entries, FIFO
- Sonner for ALL toasts —
import { toast } from "sonner" - Honeypot — hidden input, if filled return fake success (public route only)
- IP tracking —
x-forwarded-forheader (public route only) - Collapsible sections — both screenshot sections closed by default
- Fullscreen preview — click on screenshot thumbnail opens z-[100002] overlay
- Alt key shortcut — secondary way to start targeting
- Verify .mcp.json is gitignored
- Adapt language — French projects get French UI strings + French AI prompts
Agentik OS Reference Design | v2.0 | 2026-03-04