Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions plugins/linear-triage/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "linear-triage",
"version": "0.1.0",
"description": "Claude Code plugin that polls a Linear team's triage queue for issues labeled 'Claude Code', then implements each one end-to-end (branch, PR, tests, status updates).",
"author": {
"name": "Shannon Hu",
"email": "shannon.hu@linear.app"
},
"homepage": "https://github.com/linear/linear-solutions/tree/main/plugins/linear-triage",
"license": "MIT",
"keywords": ["linear", "triage", "automation", "agent"],
"skills": [
"skills/linear-triage-setup",
"skills/linear-triage-poller",
"skills/linear-issue-worker"
],
"requirements": {
"mcp": ["claude_ai_Linear"],
"cli": ["gh", "git"]
}
}
4 changes: 4 additions & 0 deletions plugins/linear-triage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
node_modules/
*.log
config.json
111 changes: 111 additions & 0 deletions plugins/linear-triage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Linear Triage Plugin for Claude Code

Polls a Linear team's triage queue for issues labeled **"Claude Code"** and implements each one end-to-end: reads the issue, plans, creates a worktree on Linear's `gitBranchName`, implements, opens a draft PR, runs tests, and marks the PR ready for review. Every stage transition is posted as a comment on the Linear issue, so the issue stays the single source of truth.

Built on the [Symphony](https://github.com/openai/symphony) pattern: tracker-as-queue + isolated worktree per issue + agent-per-issue.

See [`docs/architecture.md`](docs/architecture.md) for a diagram of how the three skills coordinate with the Linear MCP server.

## Install

```bash
# 1. Clone the linear-solutions monorepo
git clone https://github.com/linear/linear-solutions ~/Claude/linear-solutions

# 2. Copy the example config
cp ~/Claude/linear-solutions/plugins/linear-triage/config.example.json ~/.claude/linear-triage-config.json
$EDITOR ~/.claude/linear-triage-config.json # set repo, repoUrl, team

# 3. Launch Claude Code with the plugin loaded
claude --plugin-dir ~/Claude/linear-solutions/plugins/linear-triage
```

> The `--plugin-dir` flag loads a local plugin for the session. To load it every time, add an alias (e.g. `alias claude-triage='claude --plugin-dir ~/Claude/linear-solutions/plugins/linear-triage'`) or pass multiple `--plugin-dir` flags to combine plugins.

## Prerequisites

- `gh` CLI authenticated (`gh auth status`)
- `git` available on PATH
- Claude Linear MCP connected (the `mcp__claude_ai_Linear__*` tools must be available)
- A target git repo cloned locally (the plugin will clone for you if missing)

## Setup

In Claude Code, run:

```
/linear-triage-setup
```

This:

1. Verifies prerequisites
2. Creates the **"Claude Code"** label in Linear (if missing)
3. Registers a `RemoteTrigger` cron `*/15 * * * *` that runs `/linear-triage-poller`

## Daily use

Tag any Linear triage issue with the **"Claude Code"** label. Within 15 minutes the poller picks it up, claims it (moves to "In Progress"), and spawns a worker that delivers a draft PR.

To trigger immediately: `/linear-triage-poller`

## Configuration

`~/.claude/linear-triage-config.json`:

| Key | Purpose |
|---|---|
| `label` | Linear label that opts an issue into automation |
| `team` | Linear team key to scope the poller to (omit for all teams) |
| `repo` | Local path where worktrees are created |
| `repoUrl` | Remote URL — used if `repo` doesn't exist locally |
| `testCommand` | Run inside the worktree after implementation |
| `statusMap.inProgress` | Linear state name to claim issues into |
| `statusMap.inReview` | Linear state name to set when PR is ready |
| `maxConcurrentIssues` | Cap on workers spawned per poll |

## Permissions (required for background runs)

The poller fires via `RemoteTrigger` with no human in the loop, so every tool the workers call must be pre-approved — otherwise the background agent stalls on permission prompts that nobody answers.

The plugin ships a recommended allowlist at `plugins/linear-triage/settings.json` covering the Linear MCP tools, broad `Bash` access, and file tools. Bash is allowed broadly because the worker has to run arbitrary shell commands to explore the repo, implement fixes, and run tests — narrow patterns can't cover an autonomous coding agent. This is the same trust posture as running Claude Code interactively; only enable this for repos you'd be comfortable letting Claude Code modify on its own.

Merge it into your settings before kicking off background runs:

```bash
# Option A — copy into project settings (scoped to one repo)
cp ~/Claude/linear-solutions/plugins/linear-triage/settings.json <your-repo>/.claude/settings.local.json

# Option B — merge into user settings (applies everywhere)
$EDITOR ~/.claude/settings.json # add the entries from the plugin's settings.json under permissions.allow
```

If your `testCommand` isn't one of the common runners listed (npm/pnpm/yarn/pytest/cargo/go), add it to the allowlist. After editing, restart your Claude Code session.

## Troubleshooting

**Poller isn't firing every 15 minutes**
Check that the `RemoteTrigger` was registered: ask Claude Code to "list my remote triggers" or re-run `/linear-triage-setup`. Triggers only fire while you have a Claude Code session that can receive them — confirm your session is running.

**"Claude Code" label not found / can't claim issues**
Re-run `/linear-triage-setup` to recreate the label. Verify your Linear MCP connection has write scope (`save_issue`, `create_issue_label`).

**`mcp__claude_ai_Linear__*` tools unavailable**
Connect the Linear MCP server at [claude.ai/settings/connectors](https://claude.ai/settings/connectors) and restart Claude Code. The plugin cannot read or update issues without it.

**Worker fails on `gh pr create`**
Run `gh auth status` and `gh auth refresh -s repo` to ensure the CLI has repo scope. The plugin opens PRs as the authenticated `gh` user.

**Tests fail and the issue is labeled `needs-human`**
Expected behavior — inspect the draft PR, push a fix manually, then remove the label. The poller will not retry an issue that's no longer in triage.

## Safety

- Workers only operate inside `repo/.worktrees/<issue-id>/`
- Each issue gets a fresh worktree on Linear's `gitBranchName` so the PR auto-links
- PRs always open as **draft**; tests must pass before they're marked ready
- Failing tests label the issue `needs-human` and stop — no merge attempt

## License

MIT
13 changes: 13 additions & 0 deletions plugins/linear-triage/config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"label": "Claude Code",
"team": "YOUR_TEAM_KEY",
"branchPrefix": "claude",
"repo": "/absolute/path/to/your/repo",
"repoUrl": "https://github.com/YourOrg/your-repo",
"testCommand": "npm test",
"statusMap": {
"inProgress": "In Progress",
"inReview": "In Review"
},
"maxConcurrentIssues": 3
}
80 changes: 80 additions & 0 deletions plugins/linear-triage/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Linear Triage Plugin — Architecture

How the three skills coordinate with the Linear MCP server and local `gh`/`git`.

```
┌─────────────────────────┐
│ You (Linear user) │
│ add "Claude Code" label│
│ to a triage issue │
└────────────┬────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ LINEAR (cloud) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Triage queue │───▶│ "Claude Code"│ │ Issue comments & │ │
│ │ │ │ labeled issue│ │ state transitions │ │
│ └──────────────┘ └──────────────┘ └──────────▲───────────┘ │
└─────────────────▲──────────────▲────────────────────│────────────────┘
│ │ │
list_issues│ save_issue │ save_comment │
get_issue │ get_user │ create_label │
│ │ │
┌─────┴──────────────┴────────────────────┴──────┐
│ Linear MCP Server │
│ (mcp__claude_ai_Linear__*) │
└─────▲──────────────▲────────────────────▲──────┘
│ │ │
─ ─ ─ ─ ─ ─ ─ ─┼─ ─ ─ ─ ─ ─ ─ ┼─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼─ ─ ─ ─
│ │ │
┌──────────┴────┐ ┌──────┴───────┐ ┌────────┴─────────┐
│ SKILL 1 │ │ SKILL 2 │ │ SKILL 3 │
│ triage-setup │ │ triage-poller│ │ issue-worker │
│ (one-time) │ │ (every 15m) │ │ (per issue) │
├───────────────┤ ├──────────────┤ ├──────────────────┤
│• check gh/git │ │• list_issues │ │• get_issue │
│• create label │ │ (label=CC, │ │• save_comment │
│• register │ │ state= │ │ (stage updates) │
│ RemoteTrigger│ │ triage) │ │• save_issue │
│ cron */15 │ │• save_issue │ │ (state→InReview)│
│ │ │ →InProgress │ │• get_user │
│ │ │• save_comment│ │ (assignee @) │
│ │ │ "picked up" │ │ │
│ │ │• spawn worker│──▶│ (one agent per │
│ │ │ per issue │ │ issue, parallel)│
└───────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ ▲ │
│ │ cron fires │ git + gh
│ │ │ (local)
▼ │ ▼
┌──────────────────────────────┐ ┌──────────────────────┐
│ Claude Code RemoteTrigger │ │ Local repo worktree │
│ */15 * * * * │ │ .worktrees/<id>/ │
│ → /linear-triage-poller │ │ branch=gitBranchName│
└──────────────────────────────┘ │ → gh pr create │
│ --draft │
│ → npm test │
│ → gh pr ready │
└──────────┬───────────┘
┌───────────────┐
│ GitHub PR │
│ (auto-linked │
│ to issue) │
└───────────────┘
```

## Reading it left-to-right

- **Skill 1 (`linear-triage-setup`)** — runs once. Talks to Linear MCP to create the label, talks to the Claude Code runtime to register the cron.
- **Skill 2 (`linear-triage-poller`)** — runs on the cron. Queries Linear for labeled triage issues, claims each one (state change + intake comment), then **spawns Skill 3 as a sub-agent per issue** (parallel).
- **Skill 3 (`linear-issue-worker`)** — runs once per issue. Reads the full issue from Linear, does all the local `git`/`gh`/test work in a worktree, and posts status comments back to Linear at every stage. Final step flips the issue to "In Review" and pings the assignee.

## Key invariants

- **Linear is the queue and the status board.** All three skills share the Linear MCP server: it's how the plugin reads work (labeled triage issues) and reports progress (comments, state transitions).
- **`gh` and `git` only appear in Skill 3.** Setup and the poller never touch code or GitHub — they only orchestrate.
- **One worker per issue, isolated in a worktree.** Concurrent issues never collide because each gets its own `.worktrees/<issue-id>/` directory on Linear's `gitBranchName`.
- **The cron is the only thing that makes it "autonomous".** Without `RemoteTrigger`, the plugin is still usable — you'd just invoke `/linear-triage-poller` manually.
23 changes: 23 additions & 0 deletions plugins/linear-triage/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"permissions": {
"allow": [
"mcp__claude_ai_Linear__get_issue",
"mcp__claude_ai_Linear__list_issues",
"mcp__claude_ai_Linear__save_issue",
"mcp__claude_ai_Linear__save_comment",
"mcp__claude_ai_Linear__create_issue_label",
"mcp__claude_ai_Linear__list_issue_labels",
"mcp__claude_ai_Linear__list_issue_statuses",
"mcp__claude_ai_Linear__list_teams",
"mcp__claude_ai_Linear__get_user",
"Bash",
"Task",
"Agent",
"Read",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare Bash in the allow-list grants unattended arbitrary command execution

(Critical) settings.json line 14 allows Bash unconditionally. When the user merges this file into .claude/settings.local.json (the documented setup path), every bash command the worker or poller constructs — including the git clone [repoUrl] and [testCommand] expansions that interpolate values from the Linear issue description — runs without a permission prompt. A malicious or misconfigured Linear issue could inject shell commands through gitBranchName, repoUrl, or testCommand. The worker shells out with these values verbatim (Stage 3 git worktree add, Stage 6 [testCommand] 2>&1). The minimum fix is to restrict the Bash allow pattern to specific safe commands (e.g., Bash(git *), Bash(gh *), Bash(npm test)) rather than a wildcard.

"Edit",
"Write",
"Glob",
"Grep"
]
}
}
Loading