plugins/autopilot/skills/linear-integration/SKILL.md
Linear API patterns and examples for autopilot. Includes authentication, webhooks, issue CRUD, state transitions, file attachments, and comment handling.
npx skillsauth add madappgang/magus linear-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
plugin: autopilot updated: 2026-01-20
Version: 0.1.0 Purpose: Patterns for Linear API integration in autopilot workflows Status: Phase 1
Use this skill when you need to:
This skill provides patterns for:
Personal API Key (MVP):
import { LinearClient } from '@linear/sdk';
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
Verification:
async function verifyConnection(): Promise<boolean> {
try {
const me = await linear.viewer;
console.log(`Connected as: ${me.name}`);
return true;
} catch (error) {
console.error('Linear connection failed:', error);
return false;
}
}
Bun HTTP Server:
import { serve } from 'bun';
import { createHmac } from 'crypto';
interface LinearWebhookPayload {
action: 'created' | 'updated' | 'deleted';
type: 'Issue' | 'Comment' | 'Label';
data: {
id: string;
title?: string;
description?: string;
state: { id: string; name: string };
labels: Array<{ id: string; name: string }>;
};
}
serve({
port: process.env.AUTOPILOT_WEBHOOK_PORT || 3001,
async fetch(req: Request): Promise<Response> {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
// Verify signature
const signature = req.headers.get('Linear-Signature');
const body = await req.text();
if (!verifySignature(body, signature)) {
return new Response('Unauthorized', { status: 401 });
}
const payload: LinearWebhookPayload = JSON.parse(body);
// Route to handler
await routeWebhook(payload);
return new Response('OK', { status: 200 });
}
});
function verifySignature(body: string, signature: string | null): boolean {
if (!signature) return false;
const hmac = createHmac('sha256', process.env.LINEAR_WEBHOOK_SECRET!);
const expectedSignature = hmac.update(body).digest('hex');
return signature === expectedSignature;
}
Create Issue:
async function createIssue(
teamId: string,
title: string,
description: string,
labels: string[]
): Promise<string> {
// Note: Linear SDK uses linear.createIssue() method
const result = await linear.createIssue({
teamId,
title,
description,
labelIds: await resolveLabelIds(labels),
assigneeId: process.env.AUTOPILOT_BOT_USER_ID,
priority: 2,
});
const issue = await result.issue;
return issue!.id;
}
Query Issues:
async function getAutopilotTasks(teamId: string) {
const issues = await linear.issues({
filter: {
team: { id: { eq: teamId } },
assignee: { id: { eq: process.env.AUTOPILOT_BOT_USER_ID } },
state: { name: { in: ['Todo', 'In Progress'] } },
},
});
return issues.nodes;
}
Transition State:
async function transitionState(
issueId: string,
newStateName: string
): Promise<void> {
// Get workflow states for the issue's team
const issue = await linear.issue(issueId);
const team = await issue.team;
const states = await team.states();
const targetState = states.nodes.find(s => s.name === newStateName);
if (!targetState) {
throw new Error(`State "${newStateName}" not found`);
}
// Note: Linear SDK uses linear.updateIssue() method
await linear.updateIssue(issueId, {
stateId: targetState.id,
});
}
Upload and Attach:
async function attachFile(
issueId: string,
filePath: string,
fileName: string
): Promise<void> {
// Request upload URL
const uploadPayload = await linear.fileUpload(
getMimeType(filePath),
fileName,
getFileSize(filePath)
);
// Upload to storage
const fileContent = await Bun.file(filePath).arrayBuffer();
await fetch(uploadPayload.uploadUrl, {
method: 'PUT',
body: fileContent,
headers: { 'Content-Type': getMimeType(filePath) },
});
// Attach to issue
await linear.attachmentCreate({
issueId,
url: uploadPayload.assetUrl,
title: fileName,
});
}
Add Comment:
async function addComment(
issueId: string,
body: string
): Promise<void> {
// Note: Linear SDK uses linear.createComment() method
await linear.createComment({
issueId,
body,
});
}
// Create issue
const issueId = await createIssue(
teamId,
"Add user profile page",
"Implement user profile with avatar upload",
["frontend", "feature"]
);
// Transition to In Progress
await transitionState(issueId, "In Progress");
// ... work happens ...
// Attach proof artifacts
await attachFile(issueId, "screenshot.png", "Desktop Screenshot");
// Add completion comment
await addComment(issueId, "Implementation complete. See attached proof.");
// Transition to In Review
await transitionState(issueId, "In Review");
const tasks = await getAutopilotTasks(teamId);
console.log(`Autopilot queue: ${tasks.length} tasks`);
for (const task of tasks) {
console.log(`- ${task.identifier}: ${task.title} (${task.state.name})`);
}
testing
A test skill for validation testing. Use when testing skill parsing and validation logic.
tools
--- name: bad-skill description: This skill has invalid YAML in frontmatter allowed-tools: [invalid, array, syntax prerequisites: not-an-array --- # Bad Skill This skill has malformed frontmatter that should fail parsing. The YAML has: - Unclosed array bracket - Wrong type for prerequisites (should be array, not string)
development
Sync model aliases from the curated Firebase database. Fetches default model assignments, short aliases, team compositions, and known model metadata from the claudish API. Run this to get fresh model recommendations.
tools
Release one or more Magus plugins to the distribution repos (magus, magus-alpha, magus-marketing). Handles version inference from git history, marketplace.json updates, tagging, and force-push to lean dist repos. Use whenever the user says "release kanban", "release the dev plugin", "cut a new version of gtd", "bump kanban to 1.7", or hands you a batch like "release kanban and gtd". Also use for multi-plugin releases and for checking what a release would contain before committing.