diff --git a/openpeon/files.macos/config.json b/openpeon/files.macos/config.json
index d607421..de0a431 100644
--- a/openpeon/files.macos/config.json
+++ b/openpeon/files.macos/config.json
@@ -10,4 +10,4 @@
"input.required": true,
"resource.limit": true
}
-}
+}
\ No newline at end of file
diff --git a/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md b/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md
index 55b129b..509e548 100644
--- a/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md
+++ b/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md
@@ -1,3 +1,8 @@
+---
+name: attio-frontend-rules
+description: Styling conventions and component guidelines for the Attio frontend codebase. Covers styled-components patterns, transient props, data attributes, spacing, color tokens, and design system usage. Use when modifying frontend UI code in the Attio monorepo.
+---
+
# Attio Frontend Rules
Guidelines and conventions for working on the Attio frontend codebase. Use whenever modifying the frontend.
@@ -52,6 +57,26 @@ export function Stack({..., className}: {..., className: string | undefined}) {
If the same re-styling is applied multiple times, it should become its own reusable component (or component variant).
+### Layout.Stack defaults
+
+`Layout.Stack` defaults `align` to `"center"` (i.e. `align-items: center`). **Always explicitly set `align="flex-start"`** when you need left/top alignment — don't assume it will be the default.
+
+```tsx
+// Good — explicit alignment
+
+ Title
+ Description
+
+
+// Bad — text will be centered, not left-aligned
+
+ Title
+ Description
+
+```
+
+Other useful `Layout.Stack` props: `direction`, `justify`, `gap`, `flex`, `shrink`, `minWidth`, `width`, `height`, and all spacing props (`p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`, `m`, `mx`, `my`, etc.). **Always prefer these props over writing custom styled divs with `display: flex`.**
+
### Avoid layout assumptions
Components should not generally include external layout styles such as `width`, `z-index`, `margin` or `flex`. These properties should instead be set by the parent component using a `styled(MyComponent)` override.
diff --git a/pi/files/agent/skills/linear/SKILL.md b/pi/files/agent/skills/linear/SKILL.md
new file mode 100644
index 0000000..620601d
--- /dev/null
+++ b/pi/files/agent/skills/linear/SKILL.md
@@ -0,0 +1,105 @@
+---
+name: linear
+description: Access Linear issue tracker - search, view, create, update issues, list teams/projects, and manage comments. Use when the user asks about Linear issues, tasks, tickets, or project management in Linear.
+---
+
+# Linear
+
+Manage Linear issues, projects, and teams via the Linear SDK.
+
+## Setup
+
+Run once before first use:
+
+```bash
+cd {baseDir} && npm install
+```
+
+Requires a `LINEAR_API_KEY` environment variable. Generate one at: https://linear.app/settings/api (Personal API keys).
+
+Set it in your shell profile or pi settings:
+
+```bash
+export LINEAR_API_KEY=lin_api_...
+```
+
+## Current User
+
+```bash
+node {baseDir}/linear-me.js # Show authenticated user
+node {baseDir}/linear-me.js --issues # Show user + their active issues
+```
+
+## Search Issues
+
+```bash
+node {baseDir}/linear-search.js "query" # Text search
+node {baseDir}/linear-search.js "query" -n 20 # More results
+node {baseDir}/linear-search.js "query" --team ENG # Filter by team
+node {baseDir}/linear-search.js "query" --state "In Progress" # Filter by state
+```
+
+## List Issues (with filters)
+
+```bash
+node {baseDir}/linear-issues.js # All recent issues
+node {baseDir}/linear-issues.js --team ENG # By team
+node {baseDir}/linear-issues.js --state "In Progress" # By state
+node {baseDir}/linear-issues.js --assignee me # My issues
+node {baseDir}/linear-issues.js --assignee "John" # By assignee name
+node {baseDir}/linear-issues.js --label "Bug" # By label
+node {baseDir}/linear-issues.js --project "Q1 Goals" # By project
+node {baseDir}/linear-issues.js --team ENG --state Todo -n 50 # Combined filters
+```
+
+## View Issue Details
+
+```bash
+node {baseDir}/linear-issue.js ATT-1234 # Full issue details
+node {baseDir}/linear-issue.js ATT-1234 --comments # Include comments
+```
+
+## Create Issue
+
+```bash
+node {baseDir}/linear-create.js --team ENG --title "Fix login bug"
+node {baseDir}/linear-create.js --team ENG --title "New feature" --description "Details here" --state Todo --priority 2 --assignee me --label "Feature"
+node {baseDir}/linear-create.js --team ENG --title "Sub-task" --parent ATT-100
+```
+
+Priority values: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
+
+## Update Issue
+
+```bash
+node {baseDir}/linear-update.js ATT-1234 --state "In Progress"
+node {baseDir}/linear-update.js ATT-1234 --assignee me --priority 2
+node {baseDir}/linear-update.js ATT-1234 --title "New title" --description "Updated desc"
+```
+
+## Add Comment
+
+```bash
+node {baseDir}/linear-comment.js ATT-1234 "This is done in PR #567"
+```
+
+## List Teams
+
+```bash
+node {baseDir}/linear-teams.js
+```
+
+## List Projects
+
+```bash
+node {baseDir}/linear-projects.js # All projects
+node {baseDir}/linear-projects.js --team ENG # By team
+```
+
+## Tips
+
+- Use `--assignee me` to filter by the authenticated user
+- Issue identifiers follow the pattern `TEAM-NUMBER` (e.g. `ATT-1234`, `ENG-567`)
+- Descriptions support markdown formatting
+- State names are case-insensitive (e.g. "todo", "Todo", "TODO" all work)
+- When creating issues, the team key is required; use `linear-teams.js` to find available teams
diff --git a/pi/files/agent/skills/linear/lib.js b/pi/files/agent/skills/linear/lib.js
new file mode 100755
index 0000000..46cb062
--- /dev/null
+++ b/pi/files/agent/skills/linear/lib.js
@@ -0,0 +1,23 @@
+import { LinearClient } from "@linear/sdk";
+
+export function getClient() {
+ const apiKey = process.env.LINEAR_API_KEY;
+ if (!apiKey) {
+ console.error("Error: LINEAR_API_KEY environment variable is required.");
+ console.error(
+ "Generate one at: https://linear.app/settings/api (Personal API keys)"
+ );
+ process.exit(1);
+ }
+ return new LinearClient({ apiKey });
+}
+
+export function formatDate(date) {
+ if (!date) return "";
+ return new Date(date).toISOString().split("T")[0];
+}
+
+export function truncate(str, len = 120) {
+ if (!str) return "";
+ return str.length > len ? str.slice(0, len) + "…" : str;
+}
diff --git a/pi/files/agent/skills/linear/linear-comment.js b/pi/files/agent/skills/linear/linear-comment.js
new file mode 100755
index 0000000..d677f33
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-comment.js
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+// Add a comment to a Linear issue
+// Usage: linear-comment.js
+
+import { getClient } from "./lib.js";
+
+const args = process.argv.slice(2);
+const identifier = args[0];
+const body = args.slice(1).join(" ");
+
+if (!identifier || !body) {
+ console.log("Usage: linear-comment.js ");
+ console.log("\nExamples:");
+ console.log(' linear-comment.js ATT-1234 "This is fixed in the latest PR"');
+ process.exit(1);
+}
+
+const client = getClient();
+
+const results = await client.searchIssues(identifier, { first: 1 });
+const issue = results.nodes[0];
+if (!issue) {
+ console.error(`Issue '${identifier}' not found.`);
+ process.exit(1);
+}
+
+await client.createComment({ issueId: issue.id, body });
+console.log(`Comment added to ${issue.identifier}.`);
diff --git a/pi/files/agent/skills/linear/linear-create.js b/pi/files/agent/skills/linear/linear-create.js
new file mode 100755
index 0000000..92e9d7b
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-create.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+
+// Create a new Linear issue
+// Usage: linear-create.js --team --title [--description ] [--state ] [--priority <0-4>] [--assignee ] [--label ] [--parent ]
+
+import { getClient } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+function extractArg(flag) {
+ const idx = args.indexOf(flag);
+ if (idx !== -1 && args[idx + 1]) {
+ const val = args[idx + 1];
+ args.splice(idx, 2);
+ return val;
+ }
+ return null;
+}
+
+const teamKey = extractArg("--team");
+const title = extractArg("--title");
+const description = extractArg("--description");
+const stateName = extractArg("--state");
+const priority = extractArg("--priority");
+const assigneeName = extractArg("--assignee");
+const labelName = extractArg("--label");
+const parentId = extractArg("--parent");
+
+if (!teamKey || !title) {
+ console.log("Usage: linear-create.js --team --title [options]");
+ console.log("\nRequired:");
+ console.log(" --team Team key (e.g. ENG)");
+ console.log(' --title Issue title');
+ console.log("\nOptional:");
+ console.log(" --description Issue description (markdown)");
+ console.log(" --state Initial state (e.g. 'Todo')");
+ console.log(" --priority <0-4> Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low");
+ console.log(" --assignee Assignee name or 'me'");
+ console.log(" --label Label name");
+ console.log(" --parent Parent issue identifier (e.g. ATT-100)");
+ process.exit(1);
+}
+
+const client = getClient();
+
+// Resolve team
+const teams = await client.teams({ filter: { key: { eq: teamKey.toUpperCase() } } });
+const team = teams.nodes[0];
+if (!team) {
+ console.error(`Team '${teamKey}' not found.`);
+ process.exit(1);
+}
+
+const input = {
+ teamId: team.id,
+ title,
+};
+
+if (description) input.description = description;
+if (priority) input.priority = parseInt(priority, 10);
+
+// Resolve state
+if (stateName) {
+ const states = await team.states();
+ const state = states.nodes.find(
+ (s) => s.name.toLowerCase() === stateName.toLowerCase()
+ );
+ if (state) input.stateId = state.id;
+ else console.warn(`Warning: State '${stateName}' not found, using default.`);
+}
+
+// Resolve assignee
+if (assigneeName) {
+ if (assigneeName.toLowerCase() === "me") {
+ const me = await client.viewer;
+ input.assigneeId = me.id;
+ } else {
+ const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } });
+ if (users.nodes[0]) input.assigneeId = users.nodes[0].id;
+ else console.warn(`Warning: User '${assigneeName}' not found.`);
+ }
+}
+
+// Resolve label
+if (labelName) {
+ const labels = await client.issueLabels({ filter: { name: { eqIgnoreCase: labelName } } });
+ if (labels.nodes[0]) input.labelIds = [labels.nodes[0].id];
+ else console.warn(`Warning: Label '${labelName}' not found.`);
+}
+
+// Resolve parent
+if (parentId) {
+ const parentSearch = await client.searchIssues(parentId, { first: 1 });
+ if (parentSearch.nodes[0]) input.parentId = parentSearch.nodes[0].id;
+ else console.warn(`Warning: Parent '${parentId}' not found.`);
+}
+
+const result = await client.createIssue(input);
+const issue = await result.issue;
+
+console.log(`Created: ${issue.identifier} - ${issue.title}`);
+console.log(`URL: ${issue.url}`);
diff --git a/pi/files/agent/skills/linear/linear-issue.js b/pi/files/agent/skills/linear/linear-issue.js
new file mode 100755
index 0000000..f9fa5d7
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-issue.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+
+// Get details for a specific Linear issue
+// Usage: linear-issue.js [--comments]
+
+import { getClient, formatDate } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+const showComments = args.includes("--comments");
+const filtered = args.filter((a) => a !== "--comments");
+
+const identifier = filtered[0];
+
+if (!identifier) {
+ console.log("Usage: linear-issue.js [--comments]");
+ console.log("\nExamples:");
+ console.log(" linear-issue.js ATT-1234");
+ console.log(" linear-issue.js ATT-1234 --comments");
+ process.exit(1);
+}
+
+const client = getClient();
+
+// Parse team key and issue number from identifier (e.g. "SIP-1205")
+const parts = identifier.match(/^([A-Za-z]+)-(\d+)$/);
+if (!parts) {
+ console.error(`Invalid identifier format: ${identifier}. Expected format: TEAM-123`);
+ process.exit(1);
+}
+
+const teamKey = parts[1].toUpperCase();
+const issueNumber = parseInt(parts[2], 10);
+
+// Find the issue by team key + number
+const issues = await client.issues({
+ filter: {
+ team: { key: { eq: teamKey } },
+ number: { eq: issueNumber },
+ },
+ first: 1,
+});
+
+const issue = issues.nodes[0];
+if (!issue) {
+ console.error(`Issue ${identifier} not found.`);
+ process.exit(1);
+}
+
+const state = await issue.state;
+const team = await issue.team;
+const assignee = await issue.assignee;
+const labels = await issue.labels();
+const parent = await issue.parent;
+const project = await issue.project;
+const cycle = await issue.cycle;
+
+console.log(`=== ${issue.identifier}: ${issue.title} ===`);
+console.log(`URL: ${issue.url}`);
+console.log(`State: ${state?.name || "Unknown"}`);
+console.log(`Priority: ${issue.priorityLabel}`);
+console.log(`Team: ${team?.key || "?"}`);
+console.log(`Assignee: ${assignee?.name || "Unassigned"}`);
+if (project) console.log(`Project: ${project.name}`);
+if (cycle) console.log(`Cycle: ${cycle.name || cycle.number}`);
+if (parent) console.log(`Parent: ${parent.identifier} - ${parent.title}`);
+if (labels.nodes.length > 0) {
+ console.log(`Labels: ${labels.nodes.map((l) => l.name).join(", ")}`);
+}
+console.log(`Created: ${formatDate(issue.createdAt)}`);
+console.log(`Updated: ${formatDate(issue.updatedAt)}`);
+if (issue.dueDate) console.log(`Due: ${issue.dueDate}`);
+console.log(`\nDescription:\n${issue.description || "(empty)"}`);
+
+if (showComments) {
+ const comments = await issue.comments();
+ if (comments.nodes.length > 0) {
+ console.log(`\n--- Comments (${comments.nodes.length}) ---`);
+ for (const comment of comments.nodes) {
+ const author = await comment.user;
+ console.log(`\n[${formatDate(comment.createdAt)}] ${author?.name || "Unknown"}:`);
+ console.log(comment.body);
+ }
+ } else {
+ console.log("\nNo comments.");
+ }
+}
diff --git a/pi/files/agent/skills/linear/linear-issues.js b/pi/files/agent/skills/linear/linear-issues.js
new file mode 100755
index 0000000..06a3761
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-issues.js
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+
+// List Linear issues with filters
+// Usage: linear-issues.js [--team ] [--state ] [--assignee ] [--label ] [--project ] [-n ]
+
+import { getClient, formatDate, truncate } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+function extractArg(flag) {
+ const idx = args.indexOf(flag);
+ if (idx !== -1 && args[idx + 1]) {
+ const val = args[idx + 1];
+ args.splice(idx, 2);
+ return val;
+ }
+ return null;
+}
+
+const numResults = parseInt(extractArg("-n") || "25", 10);
+const teamKey = extractArg("--team");
+const stateName = extractArg("--state");
+const assigneeName = extractArg("--assignee");
+const labelName = extractArg("--label");
+const projectName = extractArg("--project");
+
+if (args.includes("--help") || args.includes("-h")) {
+ console.log("Usage: linear-issues.js [options]");
+ console.log("\nOptions:");
+ console.log(" --team Filter by team key (e.g. ENG)");
+ console.log(" --state Filter by state (e.g. 'In Progress', 'Todo')");
+ console.log(" --assignee Filter by assignee name or 'me'");
+ console.log(" --label Filter by label name");
+ console.log(" --project Filter by project name");
+ console.log(" -n Number of results (default: 25)");
+ process.exit(0);
+}
+
+const client = getClient();
+
+// Build filter
+const filter = {};
+
+if (teamKey) {
+ filter.team = { key: { eq: teamKey.toUpperCase() } };
+}
+
+if (stateName) {
+ filter.state = { name: { eqIgnoreCase: stateName } };
+}
+
+if (assigneeName) {
+ if (assigneeName.toLowerCase() === "me") {
+ const me = await client.viewer;
+ filter.assignee = { id: { eq: me.id } };
+ } else {
+ filter.assignee = { name: { containsIgnoreCase: assigneeName } };
+ }
+}
+
+if (labelName) {
+ filter.labels = { name: { eqIgnoreCase: labelName } };
+}
+
+if (projectName) {
+ filter.project = { name: { containsIgnoreCase: projectName } };
+}
+
+const issues = await client.issues({
+ filter,
+ first: numResults,
+ orderBy: "updatedAt",
+});
+
+if (issues.nodes.length === 0) {
+ console.log("No issues found matching filters.");
+ process.exit(0);
+}
+
+for (const issue of issues.nodes) {
+ const state = await issue.state;
+ const team = await issue.team;
+ const assignee = await issue.assignee;
+
+ console.log(
+ `${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${(assignee?.name || "Unassigned").padEnd(20)} ${truncate(issue.title, 80)}`
+ );
+}
+
+console.log(`\n${issues.nodes.length} issue(s) shown.`);
diff --git a/pi/files/agent/skills/linear/linear-me.js b/pi/files/agent/skills/linear/linear-me.js
new file mode 100755
index 0000000..cbe2e20
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-me.js
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+
+// Show current authenticated user and their assigned issues
+// Usage: linear-me.js [--issues]
+
+import { getClient, truncate } from "./lib.js";
+
+const showIssues = process.argv.includes("--issues");
+
+const client = getClient();
+
+const me = await client.viewer;
+console.log(`User: ${me.name}`);
+console.log(`Email: ${me.email}`);
+console.log(`ID: ${me.id}`);
+
+if (showIssues) {
+ const issues = await me.assignedIssues({
+ first: 25,
+ filter: {
+ state: { type: { nin: ["completed", "canceled"] } },
+ },
+ orderBy: "updatedAt",
+ });
+
+ console.log(`\n--- Active Assigned Issues (${issues.nodes.length}) ---`);
+ for (const issue of issues.nodes) {
+ const state = await issue.state;
+ console.log(
+ `${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${truncate(issue.title, 80)}`
+ );
+ }
+}
diff --git a/pi/files/agent/skills/linear/linear-projects.js b/pi/files/agent/skills/linear/linear-projects.js
new file mode 100755
index 0000000..99813e8
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-projects.js
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+
+// List Linear projects
+// Usage: linear-projects.js [--team ] [-n ]
+
+import { getClient, formatDate } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+function extractArg(flag) {
+ const idx = args.indexOf(flag);
+ if (idx !== -1 && args[idx + 1]) {
+ const val = args[idx + 1];
+ args.splice(idx, 2);
+ return val;
+ }
+ return null;
+}
+
+const numResults = parseInt(extractArg("-n") || "25", 10);
+const teamKey = extractArg("--team");
+
+const client = getClient();
+
+const filter = {};
+if (teamKey) {
+ filter.accessibleTeams = { key: { eq: teamKey.toUpperCase() } };
+}
+
+const projects = await client.projects({ filter, first: numResults });
+
+if (projects.nodes.length === 0) {
+ console.log("No projects found.");
+ process.exit(0);
+}
+
+for (const project of projects.nodes) {
+ const lead = await project.lead;
+ console.log(`--- ${project.name} ---`);
+ console.log(`State: ${project.state} | Progress: ${Math.round(project.progress * 100)}%`);
+ if (lead) console.log(`Lead: ${lead.name}`);
+ if (project.targetDate) console.log(`Target: ${project.targetDate}`);
+ console.log(`URL: ${project.url}`);
+ console.log("");
+}
diff --git a/pi/files/agent/skills/linear/linear-search.js b/pi/files/agent/skills/linear/linear-search.js
new file mode 100755
index 0000000..2fa5aa6
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-search.js
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+
+// Search Linear issues by text query
+// Usage: linear-search.js [-n ] [--team ] [--state ]
+
+import { getClient, formatDate, truncate } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+let numResults = 10;
+const nIdx = args.indexOf("-n");
+if (nIdx !== -1 && args[nIdx + 1]) {
+ numResults = parseInt(args[nIdx + 1], 10);
+ args.splice(nIdx, 2);
+}
+
+let teamFilter = null;
+const teamIdx = args.indexOf("--team");
+if (teamIdx !== -1 && args[teamIdx + 1]) {
+ teamFilter = args[teamIdx + 1];
+ args.splice(teamIdx, 2);
+}
+
+let stateFilter = null;
+const stateIdx = args.indexOf("--state");
+if (stateIdx !== -1 && args[stateIdx + 1]) {
+ stateFilter = args[stateIdx + 1];
+ args.splice(stateIdx, 2);
+}
+
+const query = args.join(" ");
+
+if (!query) {
+ console.log("Usage: linear-search.js [-n ] [--team ] [--state ]");
+ console.log("\nOptions:");
+ console.log(" -n Number of results (default: 10)");
+ console.log(" --team Filter by team key (e.g. ENG)");
+ console.log(" --state Filter by state name (e.g. 'In Progress')");
+ process.exit(1);
+}
+
+const client = getClient();
+
+const results = await client.searchIssues(query, { first: numResults });
+
+for (const issue of results.nodes) {
+ const state = await issue.state;
+ const team = await issue.team;
+ const assignee = await issue.assignee;
+
+ if (teamFilter && team?.key?.toLowerCase() !== teamFilter.toLowerCase()) continue;
+ if (stateFilter && state?.name?.toLowerCase() !== stateFilter.toLowerCase()) continue;
+
+ console.log(`--- ${issue.identifier} ---`);
+ console.log(`Title: ${issue.title}`);
+ console.log(`State: ${state?.name || "Unknown"}`);
+ console.log(`Priority: ${issue.priorityLabel}`);
+ console.log(`Team: ${team?.key || "?"} | Assignee: ${assignee?.name || "Unassigned"}`);
+ console.log(`Created: ${formatDate(issue.createdAt)} | Updated: ${formatDate(issue.updatedAt)}`);
+ if (issue.description) console.log(`Description: ${truncate(issue.description, 200)}`);
+ console.log(`URL: ${issue.url}`);
+ console.log("");
+}
+
+if (results.nodes.length === 0) {
+ console.log("No results found.");
+}
diff --git a/pi/files/agent/skills/linear/linear-teams.js b/pi/files/agent/skills/linear/linear-teams.js
new file mode 100755
index 0000000..e6e888a
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-teams.js
@@ -0,0 +1,15 @@
+#!/usr/bin/env node
+
+// List all Linear teams
+// Usage: linear-teams.js
+
+import { getClient } from "./lib.js";
+
+const client = getClient();
+
+const teams = await client.teams();
+
+console.log("Teams:");
+for (const team of teams.nodes) {
+ console.log(` ${team.key.padEnd(8)} ${team.name}`);
+}
diff --git a/pi/files/agent/skills/linear/linear-update.js b/pi/files/agent/skills/linear/linear-update.js
new file mode 100755
index 0000000..883e926
--- /dev/null
+++ b/pi/files/agent/skills/linear/linear-update.js
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+
+// Update an existing Linear issue
+// Usage: linear-update.js [--title ] [--state ] [--priority <0-4>] [--assignee ] [--description ]
+
+import { getClient } from "./lib.js";
+
+const args = process.argv.slice(2);
+
+const identifier = args[0];
+if (!identifier || identifier.startsWith("--")) {
+ console.log("Usage: linear-update.js [options]");
+ console.log("\nOptions:");
+ console.log(" --title New title");
+ console.log(" --state New state (e.g. 'In Progress')");
+ console.log(" --priority <0-4> New priority");
+ console.log(" --assignee New assignee");
+ console.log(" --description New description");
+ process.exit(1);
+}
+
+args.shift();
+
+function extractArg(flag) {
+ const idx = args.indexOf(flag);
+ if (idx !== -1 && args[idx + 1]) {
+ const val = args[idx + 1];
+ args.splice(idx, 2);
+ return val;
+ }
+ return null;
+}
+
+const title = extractArg("--title");
+const stateName = extractArg("--state");
+const priority = extractArg("--priority");
+const assigneeName = extractArg("--assignee");
+const description = extractArg("--description");
+
+const client = getClient();
+
+// Find the issue
+const results = await client.searchIssues(identifier, { first: 1 });
+const issue = results.nodes[0];
+if (!issue) {
+ console.error(`Issue '${identifier}' not found.`);
+ process.exit(1);
+}
+
+const input = {};
+
+if (title) input.title = title;
+if (description) input.description = description;
+if (priority) input.priority = parseInt(priority, 10);
+
+// Resolve state
+if (stateName) {
+ const team = await issue.team;
+ const states = await team.states();
+ const state = states.nodes.find(
+ (s) => s.name.toLowerCase() === stateName.toLowerCase()
+ );
+ if (state) input.stateId = state.id;
+ else {
+ console.error(`State '${stateName}' not found. Available states:`);
+ for (const s of states.nodes) console.error(` - ${s.name}`);
+ process.exit(1);
+ }
+}
+
+// Resolve assignee
+if (assigneeName) {
+ if (assigneeName.toLowerCase() === "me") {
+ const me = await client.viewer;
+ input.assigneeId = me.id;
+ } else {
+ const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } });
+ if (users.nodes[0]) input.assigneeId = users.nodes[0].id;
+ else {
+ console.error(`User '${assigneeName}' not found.`);
+ process.exit(1);
+ }
+ }
+}
+
+if (Object.keys(input).length === 0) {
+ console.log("No updates specified. Use --title, --state, --priority, --assignee, or --description.");
+ process.exit(1);
+}
+
+await client.updateIssue(issue.id, input);
+console.log(`Updated ${issue.identifier}: ${issue.title}`);
+console.log(`URL: ${issue.url}`);
diff --git a/pi/files/agent/skills/linear/package-lock.json b/pi/files/agent/skills/linear/package-lock.json
new file mode 100644
index 0000000..ed7445a
--- /dev/null
+++ b/pi/files/agent/skills/linear/package-lock.json
@@ -0,0 +1,107 @@
+{
+ "name": "linear-skill",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "linear-skill",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@linear/sdk": "^37.0.0"
+ }
+ },
+ "node_modules/@graphql-typed-document-node/core": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@linear/sdk": {
+ "version": "37.0.0",
+ "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-37.0.0.tgz",
+ "integrity": "sha512-EAZCXtV414Nwtvrwn7Ucu3E8BbYYKsc3HqZCGf1mHUE7FhZGtfISu295DOVv89WhhXlp2N344EMg3K0nnhLxtA==",
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-typed-document-node/core": "^3.1.0",
+ "graphql": "^15.4.0",
+ "isomorphic-unfetch": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12.x",
+ "yarn": "1.x"
+ }
+ },
+ "node_modules/graphql": {
+ "version": "15.10.1",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz",
+ "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/isomorphic-unfetch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+ "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.6.1",
+ "unfetch": "^4.2.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==",
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ }
+ }
+}
diff --git a/pi/files/agent/skills/linear/package.json b/pi/files/agent/skills/linear/package.json
new file mode 100644
index 0000000..b488118
--- /dev/null
+++ b/pi/files/agent/skills/linear/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "linear-skill",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Linear API skill for pi - manage issues, projects, and teams",
+ "license": "MIT",
+ "dependencies": {
+ "@linear/sdk": "^37.0.0"
+ }
+}