{"id":45408769,"url":"https://github.com/calltelemetry/openclaw-linear-plugin","last_synced_at":"2026-05-10T05:28:58.475Z","repository":{"id":338830201,"uuid":"1159370151","full_name":"calltelemetry/openclaw-linear-plugin","owner":"calltelemetry","description":"Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage","archived":false,"fork":false,"pushed_at":"2026-02-22T21:36:13.000Z","size":3596,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-23T01:21:52.945Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/calltelemetry.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-16T16:43:19.000Z","updated_at":"2026-02-22T21:36:17.000Z","dependencies_parsed_at":"2026-02-22T23:02:29.805Z","dependency_job_id":null,"html_url":"https://github.com/calltelemetry/openclaw-linear-plugin","commit_stats":null,"previous_names":["calltelemetry/openclaw-linear-plugin"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/calltelemetry/openclaw-linear-plugin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calltelemetry%2Fopenclaw-linear-plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calltelemetry%2Fopenclaw-linear-plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calltelemetry%2Fopenclaw-linear-plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calltelemetry%2Fopenclaw-linear-plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/calltelemetry","download_url":"https://codeload.github.com/calltelemetry/openclaw-linear-plugin/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calltelemetry%2Fopenclaw-linear-plugin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29960260,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T01:47:18.291Z","status":"online","status_checked_at":"2026-03-01T02:00:07.437Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-21T22:12:05.844Z","updated_at":"2026-05-10T05:28:58.450Z","avatar_url":"https://github.com/calltelemetry.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/logo.jpeg\" alt=\"OpenClaw Linear Plugin\" width=\"720\" /\u003e\n\u003c/p\u003e\n\n# @calltelemetry/openclaw-linear\n\n[![CI](https://github.com/calltelemetry/openclaw-linear-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/calltelemetry/openclaw-linear-plugin/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/calltelemetry/openclaw-linear-plugin/graph/badge.svg)](https://codecov.io/gh/calltelemetry/openclaw-linear-plugin)\n[![npm](https://img.shields.io/npm/v/@calltelemetry/openclaw-linear)](https://www.npmjs.com/package/@calltelemetry/openclaw-linear)\n[![OpenClaw](https://img.shields.io/badge/OpenClaw-v2026.5.9--beta.1-blue)](https://github.com/openclaw/openclaw/releases/tag/v2026.5.9-beta.1)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n\nConnect Linear to AI agents. Issues get triaged, implemented, and audited — automatically.\n\n\u003e **Real human here.** I'm actively building this and beta testing it on real projects.\n\u003e Looking for feedback, bug reports, and fellow mad scientists.\n\u003e [Open an issue](https://github.com/calltelemetry/openclaw-linear-plugin/issues) — feedback and bug reports welcome.\n\n### Project Status\n\n- [x] Cloudflare tunnel setup (webhook ingress, no inbound ports)\n- [x] Linear webhook sync (Comment + Issue events)\n- [x] Linear OAuth app webhook (AgentSessionEvent created/prompted)\n- [x] Linear API integration (issues, comments, labels, state transitions)\n- [x] Agent routing (`@mentions`, natural language intent classifier)\n- [x] Intent gate + scope enforcement (all 4 webhook paths, 3-layer defense)\n- [x] Auto-triage (story points, labels, priority — read-only)\n- [x] Complexity-tier dispatch (small → Haiku, medium → Sonnet, high → Opus)\n- [x] Isolated git worktrees per dispatch\n- [x] Worker → Auditor pipeline (hard-enforced, not LLM-mediated)\n- [x] Audit rework loop (gaps fed back, automatic retry)\n- [x] Watchdog timeout + escalation\n- [x] Webhook deduplication (60s sliding window + `activeRuns` race guard on all paths)\n- [x] Webhook auto-provisioning (`webhooks setup` CLI, `doctor --fix`)\n- [x] Multi-repo worktree support\n- [x] Project planner (interview → user stories → sub-issues → DAG dispatch)\n- [x] Cross-model plan review (Claude ↔ Codex ↔ Gemini)\n- [x] Issue closure with summary report\n- [x] Sub-issue decomposition (orchestrator-level only)\n- [x] `spawn_agent` / `ask_agent` sub-agent tools\n- [x] CI + coverage badges (1170+ tests, Codecov integration)\n- [x] Setup wizard (`openclaw openclaw-linear setup`) + `doctor --fix` auto-repair\n- [x] Project context auto-detection (repo, framework, build/test commands → worker/audit prompts)\n- [x] Per-backend CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`) with `[Codex]`/`[Claude]`/`[Gemini]`-prefixed Linear session activity streaming\n- [x] Immediate thought emission on comment receipt and tool dispatch (visible before long-running tasks complete)\n- [x] Proactive OAuth token refresh timer (runs on startup, then every 6h)\n- [ ] **Worktree → PR merge** — `createPullRequest()` exists but is not wired into the pipeline. After audit pass, commits sit on a `codex/{identifier}` branch. You create the PR manually.\n- [ ] **Sub-agent worktree sharing** — Sub-agents spawned via `spawn_agent`/`ask_agent` do not inherit the parent worktree. They run in their own session without code access.\n- [ ] **Parallel worktree conflict resolution** — DAG dispatch runs up to 3 issues concurrently in separate worktrees, but there's no merge conflict detection across them.\n\n---\n\n## Why This Exists\n\nLinear is a great project tracker. But it doesn't orchestrate AI agents — it just gives you issues, comments, and sessions. Without something bridging that gap, every stage of an AI-driven workflow requires a human in the loop: copy the issue context, start an agent, wait, read the output, decide what's next, start another agent, paste in the feedback, repeat. That's not autonomous — that's babysitting.\n\nThis plugin makes the full lifecycle hands-off:\n\n```\n  You create an issue\n       │\n       ▼\n  Agent triages it ──── estimate, labels, priority\n       │\n       ▼\n  You assign it\n       │\n       ▼\n  Plugin dispatches ─── picks model tier, creates worktree\n       │\n       ▼\n  Worker implements ─── code, tests, commits\n       │\n       ▼\n  Auditor verifies ─── independent, hard-enforced\n       │\n   ┌───┴───┐\n   ▼       ▼\n  Done    Rework ────── gaps fed back, retry automatic\n```\n\nYou work in Linear. The agents handle the rest.\n\n**What Linear can't do on its own — and what this plugin handles:**\n\n| Gap | What the plugin does |\n|---|---|\n| **No agent orchestration** | Assesses complexity, picks the right model tier, creates isolated worktrees, runs workers, triggers audits, processes verdicts — all from a single issue assignment |\n| **No independent verification** | Hard-enforces a worker → auditor boundary in plugin code. The worker cannot mark its own work done. The audit is not optional and not LLM-mediated. |\n| **No failure recovery** | Watchdog kills hung agents after configurable silence. Feeds audit failures back as rework context. Escalates when retries are exhausted. |\n| **No multi-agent routing** | Routes `@mentions` and natural language (\"hey kaylee look at this\") to specific agents. Intent classifier handles plan requests, questions, close commands, and work requests. |\n| **No project-scale planning** | Planner interviews you, creates issues with user stories and acceptance criteria, runs a cross-model review, then dispatches the full dependency graph — up to 3 issues in parallel. |\n\nThe end result: you work in Linear. You create issues, assign them, comment in plain English. The agents do the rest — or tell you when they can't.\n\n---\n\n## Features\n\n### Core Pipeline\n\n- **Auto-triage** — New issues get story point estimates, labels, and priority within seconds. Read-only mode — no side effects.\n- **Worker → Auditor pipeline** — Assign an issue and a worker implements it in an isolated git worktree. An independent auditor verifies the work. The worker cannot self-certify — the audit is hard-enforced in plugin code.\n- **Complexity-tier dispatch** — The plugin assesses each issue and picks the right model. Simple typo? Haiku. Multi-service refactor? Opus. Saves cost and latency without manual intervention.\n- **Automatic rework** — Failed audits feed gaps back to the worker as context. Retries up to N times before escalating. No human needed until the agents are stuck.\n\n### Planning \u0026 Closure\n\n- **Project planner** — Comment \"plan this project\" and the agent interviews you, builds user stories with acceptance criteria, creates the full issue hierarchy, and dispatches in dependency order — up to 3 issues in parallel.\n- **Cross-model review** — Plans are automatically audited by a different AI model (Claude ↔ Codex ↔ Gemini) before dispatch. Two perspectives, one plan.\n- **Issue closure** — Say \"close this\" or \"mark as done\" and the agent generates a closure report and transitions the issue to completed.\n- **Sub-issue decomposition** — Orchestrators and the planner break complex work into sub-issues via `linear_issues`. Sub-issues inherit team and project from the parent automatically.\n\n### Multi-Agent \u0026 Routing\n\n- **Named agents** — Define agents with different roles and expertise. Route work by `@mention` or natural language (\"hey kaylee look at this\").\n- **Intent classification** — An LLM classifier (~300 tokens, ~2s) runs on every user request across all webhook paths — not just comments. Classifies intent and gates work requests on untriaged issues. Regex fallback if the classifier fails.\n- **Scope enforcement** — Three-layer defense prevents agents from building code on issues that haven't been planned. Intent gate blocks `request_work` pre-dispatch; prompt-level constraints limit CLI tools to planning-only; a `before_tool_call` hook prepends hard constraints to worker prompts.\n- **One-time detour** — `@mention` a different agent in a session and it handles that single interaction. The session stays with the original agent.\n\n### Multi-Backend \u0026 Multi-Repo\n\n- **Three coding backends** — Codex (OpenAI), Claude (Anthropic), Gemini (Google). Configurable globally or per-agent. Each backend registers as a dedicated tool (`cli_codex`, `cli_claude`, `cli_gemini`) with `[Codex]`/`[Claude]`/`[Gemini]`-prefixed activity streaming — thoughts, actions, progress, and results all show the backend label in Linear's session UI so you always know which runner is active. Per-agent overrides let you assign different backends to different team members.\n- **Multi-repo dispatch** — Tag an issue with `\u003c!-- repos: api, frontend --\u003e` and the worker gets isolated worktrees for each repo. One issue, multiple codebases, one agent session.\n\n### Operations\n\n- **Linear Guidance** — Workspace and team-level guidance configured in Linear's admin UI flows into every agent prompt — triage, dispatch, worker, audit. Admins steer agent behavior without touching config files.\n- **Watchdog** — Kills agents that go silent after configurable inactivity. Retries once, then escalates. Covers LLM hangs, API timeouts, and CLI lockups.\n- **Notifications** — Dispatch lifecycle events (started, auditing, done, stuck) to Discord, Slack, Telegram, or Signal. Rich formatting optional.\n- **Webhook deduplication** — Two-tier guard (in-memory set + 60s TTL map) prevents double-processing across Linear's two webhook systems.\n\n---\n\n## Quick Start\n\n\u003e **Tip:** Claude Code is very good at setting this up for you. Install the plugin, install the [Cloudflare CLI](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/#1-download-and-install-cloudflared) and authenticate (`cloudflared tunnel login`), then just ask Claude to configure the rest. Or run the guided setup wizard:\n\u003e\n\u003e ```bash\n\u003e openclaw openclaw-linear setup\n\u003e ```\n\u003e\n\u003e It walks through agent profiles, auth, webhook provisioning, and verification in one interactive flow.\n\n### 1. Install the plugin\n\n```bash\nopenclaw plugins install @calltelemetry/openclaw-linear\n```\n\n### 2. Expose the gateway\n\nLinear delivers webhooks over the public internet, so the gateway needs a public HTTPS URL. See [Tunnel Setup (Cloudflare)](#tunnel-setup-cloudflare) for the recommended approach. Any reverse proxy or tunnel that forwards HTTPS to `localhost:18789` will work.\n\n### 3. Create a Linear OAuth app\n\nGo to **Linear Settings \u003e API \u003e Applications** and create an app:\n\n- Set **Webhook URL** to `https://your-domain.com/linear/webhook`\n- Set **Redirect URI** to `https://your-domain.com/linear/oauth/callback`\n- Enable events: **Agent Sessions**, **Comments**, **Issues**\n- Save your **Client ID** and **Client Secret**\n\n\u003e You also need a **workspace webhook** — run `openclaw openclaw-linear webhooks setup` to auto-provision it, or manually create one in Settings \u003e API \u003e Webhooks pointing to the same URL with **Comment + Issue** events enabled. Both webhooks are required.\n\n### 4. Set credentials\n\n```bash\nexport LINEAR_CLIENT_ID=\"your_client_id\"\nexport LINEAR_CLIENT_SECRET=\"your_client_secret\"\n```\n\nFor systemd services, add these to your unit file:\n\n```ini\n[Service]\nEnvironment=LINEAR_CLIENT_ID=your_client_id\nEnvironment=LINEAR_CLIENT_SECRET=your_client_secret\n```\n\nThen reload: `systemctl --user daemon-reload \u0026\u0026 systemctl --user restart openclaw-gateway`\n\n### 5. Authorize\n\n```bash\nopenclaw openclaw-linear auth\n```\n\nThis opens your browser. Approve the authorization, then restart:\n\n```bash\nsystemctl --user restart openclaw-gateway\n```\n\n### 6. Verify\n\n```bash\nopenclaw openclaw-linear status\n```\n\nYou should see a valid token and connected status. Check the gateway logs for a clean startup:\n\n```\nLinear agent extension registered (agent: default, token: profile, orchestration: enabled)\n```\n\nTest the webhook endpoint:\n\n```bash\ncurl -s -X POST https://your-domain.com/linear/webhook \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"test\",\"action\":\"ping\"}'\n# Returns: \"ok\"\n```\n\nThat's it. Create an issue in Linear and watch the agent respond.\n\n---\n\n## Tunnel Setup (Cloudflare)\n\nLinear delivers webhooks over the public internet. The gateway listens on `localhost:18789` and needs a public HTTPS endpoint. A [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) is the recommended approach — no open ports, no TLS cert management, no static IP required.\n\n```mermaid\nflowchart TB\n    subgraph Internet\n        LW[\"Linear Webhooks\u003cbr/\u003e\u003ci\u003eComment, Issue, AgentSession\u003c/i\u003e\"]\n        LO[\"Linear OAuth\u003cbr/\u003e\u003ci\u003ecallback redirect\u003c/i\u003e\"]\n    end\n\n    subgraph CF[\"Cloudflare Edge\"]\n        TLS[\"TLS termination + DDoS protection\"]\n    end\n\n    subgraph Server[\"Your Server\"]\n        CD[\"cloudflared\u003cbr/\u003e\u003ci\u003eoutbound-only tunnel\u003c/i\u003e\"]\n        GW[\"openclaw-gateway\u003cbr/\u003e\u003ci\u003elocalhost:18789\u003c/i\u003e\"]\n    end\n\n    LW -- \"POST /linear/webhook\" --\u003e TLS\n    LO -- \"GET /linear/oauth/callback\" --\u003e TLS\n    TLS -- \"tunnel\" --\u003e CD\n    CD -- \"HTTP\" --\u003e GW\n```\n\n**How it works:** `cloudflared` opens an outbound connection to Cloudflare's edge and keeps it alive. Cloudflare routes incoming HTTPS requests for your hostname back through the tunnel to `localhost:18789`. No inbound firewall rules needed.\n\n### Install cloudflared\n\n```bash\n# RHEL / Rocky / Alma\nsudo dnf install -y cloudflared\n\n# Debian / Ubuntu\ncurl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg \u003e/dev/null\necho \"deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main\" \\\n  | sudo tee /etc/apt/sources.list.d/cloudflared.list\nsudo apt update \u0026\u0026 sudo apt install -y cloudflared\n\n# macOS\nbrew install cloudflare/cloudflare/cloudflared\n```\n\n### Authenticate with Cloudflare\n\n```bash\ncloudflared tunnel login\n```\n\nThis opens your browser. You must:\n1. Log in to your Cloudflare account\n2. **Select the domain** (zone) for the tunnel (e.g., `yourdomain.com`)\n3. Click **Authorize**\n\nCloudflare writes an origin certificate to `~/.cloudflared/cert.pem`. This cert grants `cloudflared` permission to create tunnels and DNS records under that domain.\n\n\u003e **Prerequisite:** Your domain must already be on Cloudflare (nameservers pointed to Cloudflare). If it's not, add it in the Cloudflare dashboard first.\n\n### Create a tunnel\n\n```bash\ncloudflared tunnel create openclaw-linear\n```\n\nThis outputs a **Tunnel ID** (UUID like `da1f21bf-856e-...`) and writes credentials to `~/.cloudflared/\u003cTUNNEL_ID\u003e.json`.\n\n### DNS — point your hostname to the tunnel\n\n```bash\ncloudflared tunnel route dns openclaw-linear linear.yourdomain.com\n```\n\nThis creates a CNAME record in Cloudflare DNS: `linear.yourdomain.com → \u003cTUNNEL_ID\u003e.cfargotunnel.com`. You can verify it in the Cloudflare dashboard under **DNS \u003e Records**. You can also create this record manually.\n\nThe hostname you choose here is what you'll use for **both** webhook URLs and the OAuth redirect URI in Linear. Make sure they all match.\n\n### Configure the tunnel\n\nCreate `/etc/cloudflared/config.yml` (system-wide) or `~/.cloudflared/config.yml` (user):\n\n```yaml\ntunnel: \u003cTUNNEL_ID\u003e\ncredentials-file: /home/\u003cuser\u003e/.cloudflared/\u003cTUNNEL_ID\u003e.json\n\ningress:\n  - hostname: linear.yourdomain.com\n    service: http://localhost:18789\n  - service: http_status:404    # catch-all, reject unmatched requests\n```\n\nThe `ingress` rule routes all traffic for your hostname to the gateway on localhost. The catch-all `http_status:404` rejects requests for any other hostname.\n\n### Run as a service\n\n```bash\n# Install as system service (recommended for production)\nsudo cloudflared service install\nsudo systemctl enable --now cloudflared\n```\n\nTo test without installing as a service:\n\n```bash\ncloudflared tunnel run openclaw-linear\n```\n\n### Verify end-to-end\n\n```bash\ncurl -s https://linear.yourdomain.com/linear/webhook \\\n  -X POST -H \"Content-Type: application/json\" \\\n  -d '{\"type\":\"test\",\"action\":\"ping\"}'\n# Should return: \"ok\"\n```\n\n\u003e **Tip:** Keep the tunnel running at all times. If `cloudflared` stops, Linear webhook deliveries will fail silently — the gateway won't know about new issues, comments, or agent sessions until the tunnel is restored.\n\n---\n\n## How It Works — Step by Step\n\nA project goes through a complete lifecycle — from planning to implementation to closure. Here's every phase, what triggers it, and what you'll see in Linear.\n\n```mermaid\nflowchart LR\n    P[\"Plan\u003cbr/\u003e\u003ci\u003e(optional)\u003c/i\u003e\"] --\u003e T[\"Triage\u003cbr/\u003e\u003ci\u003e(auto)\u003c/i\u003e\"]\n    T --\u003e D[\"Dispatch\u003cbr/\u003e\u003ci\u003e(assign)\u003c/i\u003e\"]\n    D --\u003e W[\"Worker\u003cbr/\u003e\u003ci\u003e(auto)\u003c/i\u003e\"]\n    W --\u003e A[\"Audit\u003cbr/\u003e\u003ci\u003e(auto)\u003c/i\u003e\"]\n    A --\u003e Done[\"Done ✔\"]\n    A --\u003e R[\"Rework\u003cbr/\u003e\u003ci\u003e(auto retry)\u003c/i\u003e\"]\n    A --\u003e S[\"Escalate ⚠\"]\n    R --\u003e W\n    Done --\u003e CL[\"Close\u003cbr/\u003e\u003ci\u003e(comment or auto)\u003c/i\u003e\"]\n```\n\n### Phase 1: Planning (optional)\n\n**Trigger:** Comment \"let's plan the features\" on a project issue.\n\nFor larger work, the planner breaks a project into issues before any code is written. It enters **interview mode** — asking questions, creating issues with user stories and acceptance criteria, and building a dependency graph in real time.\n\n```mermaid\nsequenceDiagram\n    actor Human\n    participant Linear\n    participant Plugin\n    participant Planner as Planner Agent\n    participant Reviewer as Cross-Model Reviewer\n\n    Human-\u003e\u003eLinear: \"plan this project\"\n    Plugin-\u003e\u003ePlanner: start interview\n    loop Until plan is complete\n        Planner--\u003e\u003eLinear: question\n        Human-\u003e\u003eLinear: reply\n        Planner--\u003e\u003eLinear: create/update issues\n    end\n    Human-\u003e\u003eLinear: \"looks good\"\n    Plugin-\u003e\u003ePlugin: validate DAG + descriptions\n    Plugin-\u003e\u003eReviewer: cross-model audit\n    Reviewer--\u003e\u003ePlugin: recommendations\n    Plugin--\u003e\u003eLinear: summary + ask for approval\n    Human-\u003e\u003eLinear: \"approve plan\"\n    Plugin--\u003e\u003eLinear: dispatch issues in dependency order\n```\n\nThe planner proactively asks for:\n- **User stories** — \"As a [role], I want [feature] so that [benefit]\"\n- **Acceptance criteria** — Given/When/Then format\n- **UAT test scenarios** — How to manually verify the feature\n\n**What you'll see in Linear:**\n\n\u003e I've created 3 issues:\n\u003e - **PROJ-2:** Build search API endpoint (3 pts, blocks PROJ-3)\n\u003e - **PROJ-3:** Search results page (2 pts, blocked by PROJ-2)\n\u003e - **PROJ-4:** Autocomplete suggestions (1 pt, independent)\n\u003e\n\u003e Does that cover it? Should the autocomplete call a separate endpoint or share the search API?\n\nWhen you say \"looks good\", the planner validates the plan (descriptions, estimates, no circular deps) and sends it to a **different AI model** for a cross-model review:\n\n| Your primary model | Auto-reviewer |\n|---|---|\n| Claude / Anthropic | Codex |\n| Codex / OpenAI | Gemini |\n| Gemini / Google | Codex |\n| Other (Kimi, Mistral, etc.) | Gemini |\n\nAfter approval, issues are dispatched automatically in dependency order — up to 3 in parallel.\n\n\u003e `📊 Search Feature: 2/3 complete`\n\n### Phase 2: Triage (automatic)\n\n**Trigger:** A new issue is created (manually or by the planner).\n\nThe agent reads the issue, estimates story points, adds labels, sets priority, and posts an assessment comment — all within seconds. Triage runs in **read-only mode** (no file writes, no code execution) to prevent side effects.\n\n**What you'll see in Linear:**\n\n\u003e **[Mal]** This looks like a medium complexity change — the search API integration touches both the backend GraphQL schema and the frontend query layer. I've estimated 3 points and tagged it `backend` + `frontend`.\n\nThe estimate, labels, and priority are applied silently to the issue fields.\n\n### Phase 3: Dispatch (assign the issue)\n\n**Trigger:** The issue is assigned to the agent (manually or auto-assigned after planning).\n\nThe plugin assesses complexity, picks an appropriate model tier, creates an isolated git worktree, and starts the worker.\n\n**What you'll see in Linear:**\n\n\u003e **Dispatched** as **high** (anthropic/claude-opus-4-6)\n\u003e \u003e Complex multi-service refactor with migration concerns\n\u003e\n\u003e Worktree: `/home/claw/worktrees/ENG-100` (fresh)\n\u003e Branch: `codex/ENG-100`\n\u003e\n\u003e **Status:** Worker is starting now. An independent audit runs automatically after implementation.\n\u003e\n\u003e **While you wait:**\n\u003e - Check progress: `/dispatch status ENG-100`\n\u003e - Cancel: `/dispatch escalate ENG-100 \"reason\"`\n\u003e - All dispatches: `/dispatch list`\n\n**Complexity tiers:**\n\n| Tier | Model | When |\n|---|---|---|\n| Small | claude-haiku-4-5 | Simple config changes, typos, one-file fixes |\n| Medium | claude-sonnet-4-6 | Standard features, multi-file changes |\n| High | claude-opus-4-6 | Complex refactors, architecture changes |\n\n### Phase 4: Implementation (automatic)\n\nThe worker agent reads the issue, plans its approach, writes code, and runs tests — all in the isolated worktree.\n\nIf this is a **retry** after a failed audit, the worker gets the previous audit feedback as context so it knows exactly what to fix.\n\n**Notification:** `ENG-100 working on it (attempt 1)`\n\n### Phase 5: Audit (automatic)\n\nAfter the worker finishes, a separate auditor agent independently verifies the work — checking issue requirements against what was actually implemented, running tests, and reviewing the diff.\n\nThis is **not optional** — the worker cannot mark its own work as done. The audit is triggered by the plugin, not by the AI.\n\n**Notification:** `ENG-100 checking the work...`\n\n### Phase 6: Verdict\n\nThe audit produces one of three outcomes:\n\n#### Pass — Issue is done\n\nThe issue is marked done automatically. A summary is posted.\n\n**What you'll see in Linear:**\n\n\u003e ## Done\n\u003e\n\u003e This issue has been implemented and verified.\n\u003e\n\u003e **What was checked:**\n\u003e - API endpoint returns correct response format\n\u003e - Tests pass for edge cases\n\u003e - Error handling covers timeout scenarios\n\u003e\n\u003e **Test results:** 14 tests passed, 0 failed\n\u003e\n\u003e ---\n\u003e *Completed on attempt 1. Artifacts: `/home/claw/worktrees/ENG-100/.claw/`*\n\n**Notification:** `✅ ENG-100 done! Ready for review.`\n\n#### Fail (retries left) — Automatic rework\n\nThe worker gets the audit feedback and tries again automatically.\n\n**What you'll see in Linear:**\n\n\u003e ## Needs More Work\n\u003e\n\u003e The implementation was checked and some things need to be addressed. Retrying automatically.\n\u003e\n\u003e **Attempt 1 of 3**\n\u003e\n\u003e **What needs fixing:**\n\u003e - Missing input validation on the search endpoint\n\u003e - No test for empty query string\n\u003e\n\u003e **Test results:** 12 passed, 2 failed\n\n**Notification:** `ENG-100 needs more work (attempt 1). Issues: missing validation, no empty query test`\n\n#### Fail (no retries left) — Escalation\n\nAfter all retries are exhausted (default: 3 attempts), the issue is escalated.\n\n**What you'll see in Linear:**\n\n\u003e ## Needs Your Help\n\u003e\n\u003e The automatic retries didn't fix these issues:\n\u003e\n\u003e **What went wrong:**\n\u003e - Search pagination still returns duplicate results\n\u003e - Integration test flaky on CI\n\u003e\n\u003e **Test results:** 10 passed, 4 failed\n\u003e\n\u003e ---\n\u003e *Please review and either:*\n\u003e - *Update the issue description with clearer requirements, then re-assign*\n\u003e - *Fix the issues manually in the worktree at `/home/claw/worktrees/ENG-100`*\n\n**Notification:** `🚨 ENG-100 needs your help — couldn't fix it after 3 tries`\n\n**Options:**\n1. **Clarify the issue** — Add more detail to the description, then re-assign to try again\n2. **Fix it yourself** — The agent's work is in the worktree, ready to edit\n3. **Force retry** — `/dispatch retry ENG-100`\n4. **Check logs** — Worker output in `.claw/worker-*.md`, audit verdicts in `.claw/audit-*.json`\n\n### Phase 7: Closure\n\n**Trigger:** Comment \"close this\", \"mark as done\", or \"this is resolved\" on any issue.\n\nThe plugin generates a closure report and transitions the issue to completed. This is a **static action** — the plugin orchestrates the API calls directly, the agent only writes the report text.\n\n```mermaid\nflowchart LR\n    A[\"'close this'\"] --\u003e B[\"Fetch issue details\"]\n    B --\u003e C[\"Generate closure report\u003cbr/\u003e\u003ci\u003e(read-only agent)\u003c/i\u003e\"]\n    C --\u003e D[\"Transition → completed\"]\n    D --\u003e E[\"Post report to issue\"]\n```\n\n**What you'll see in Linear:**\n\n\u003e ## Closed\n\u003e\n\u003e This issue has been reviewed and closed.\n\u003e\n\u003e **Summary:** The search API endpoint was implemented with pagination, input validation, and error handling. All 14 tests pass. The frontend search page renders results correctly.\n\n### Watchdog \u0026 timeout recovery\n\nEvery running agent has an inactivity watchdog. If the agent goes silent — no text, no tool calls, no thinking — the watchdog kills it.\n\n```\nAgent runs ─────────── output ──→ timer resets (120s default)\n                       output ──→ timer resets\n                       ...\n                       silence ─→ 120s passes ─→ KILL\n                                                   │\n                                          ┌────────┴────────┐\n                                          ▼                 ▼\n                                    Retry (auto)     Already retried?\n                                          │                 │\n                                          ▼                 ▼\n                                    Agent runs again   STUCK → you're notified\n```\n\n**What resets the timer:** any agent output — partial text, tool call start/result, reasoning stream, or error.\n\n**What triggers a kill:** LLM hangs, API timeouts, CLI lockups, rate limiting — anything that causes the agent to stop producing output.\n\n**After a kill:**\n1. First timeout → automatic retry (new attempt, same worktree)\n2. Second timeout → dispatch transitions to `stuck`, Linear comment posted with remediation steps, you get a notification\n\n**The \"Agent Timed Out\" comment includes:**\n- `/dispatch retry ENG-100` command to try again\n- Suggestion to break the issue into smaller pieces\n- How to increase `inactivitySec` in agent profiles\n- Path to `.claw/log.jsonl` for debugging\n\n**Configure per agent** in `~/.openclaw/agent-profiles.json`:\n```json\n{ \"agents\": { \"mal\": { \"watchdog\": { \"inactivitySec\": 180 } } } }\n```\n\n### Audit rework loop\n\nWhen the auditor finds problems, it doesn't just fail — it tells the worker exactly what's wrong, and the worker tries again automatically.\n\n```\nWorker implements ──→ Auditor reviews\n                          │\n                     ┌────┴────┐\n                     ▼         ▼\n                   PASS      FAIL\n                     │         │\n                     ▼         ▼\n                   Done    Gaps extracted\n                              │\n                              ▼\n                     Worker gets gaps as context ──→ \"PREVIOUS AUDIT FAILED:\n                              │                       - Missing input validation\n                              │                       - No test for empty query\"\n                              ▼\n                     Rework attempt (same worktree)\n                              │\n                         ┌────┴────┐\n                         ▼         ▼\n                       PASS    FAIL again?\n                         │         │\n                         ▼         ▼\n                       Done    Retries left?\n                                   │\n                              ┌────┴────┐\n                              ▼         ▼\n                            Retry    STUCK → you're notified\n```\n\n**How gaps flow back:**\n1. Auditor returns a structured verdict: `{ pass: false, gaps: [\"missing validation\", \"no empty query test\"], criteria: [...] }`\n2. Pipeline extracts the `gaps` array\n3. Next worker prompt gets a \"PREVIOUS AUDIT FAILED\" addendum with the gap list\n4. Worker sees exactly what to fix — no guessing\n\n**What you control:**\n- `maxReworkAttempts` (default: `2`) — how many audit failures before escalation\n- After max attempts, issue goes to `stuck` with reason `audit_failed_Nx`\n- You get a Linear comment with what went wrong and a notification\n\n**What the worker sees on rework:**\n```\nPREVIOUS AUDIT FAILED — fix these gaps before proceeding:\n1. Missing input validation on the search endpoint\n2. No test for empty query string\n\nYour previous work is still in the worktree. Fix the issues above and run tests again.\n```\n\n**Artifacts per attempt:** Each rework cycle writes `worker-{N}.md` and `audit-{N}.json` to `.claw/`, so you can see what happened at every attempt.\n\n### Project-level progress\n\nWhen issues are dispatched from a plan, you get project-level progress tracking:\n\n\u003e `📊 Search Feature: 2/3 complete`\n\nWhen everything is done:\n\n\u003e `✅ Search Feature: complete (3/3 issues)`\n\nIf an issue gets stuck, dependent issues are blocked and you're notified.\n\n### What's in the worktree\n\nEvery dispatch creates a `.claw/` folder inside the worktree with everything the agent did:\n\n```\n/home/claw/worktrees/ENG-100/\n├── .claw/\n│   ├── manifest.json       # Issue metadata, tier, status, attempt count\n│   ├── worker-0.md         # What the worker did on attempt 1\n│   ├── worker-1.md         # What the worker did on attempt 2 (if retried)\n│   ├── audit-0.json        # Audit verdict for attempt 1\n│   ├── audit-1.json        # Audit verdict for attempt 2\n│   ├── log.jsonl           # Timeline of every phase with timing\n│   └── summary.md          # Final summary (written on done or stuck)\n├── src/                    # ← your code, modified by the agent\n├── tests/\n└── ...\n```\n\nIf something went wrong, start with `log.jsonl` — it shows every phase, how long it took, and a preview of the output.\n\n---\n\n## Comment Routing — Talk Naturally\n\nYou don't need to memorize magic commands. The bot uses an LLM-based intent classifier to understand what you want from any comment.\n\n```mermaid\nflowchart LR\n    A[\"User comment\"] --\u003e B[\"Intent Classifier\u003cbr/\u003e\u003ci\u003e(small model, ~2s)\u003c/i\u003e\"]\n    B --\u003e C[\"Route to handler\"]\n    B -. \"on failure\" .-\u003e D[\"Regex fallback\"]\n    D --\u003e C\n```\n\n**What the bot understands:**\n\n| What you say | What happens |\n|---|---|\n| \"let's plan the features for this\" | Starts planning interview |\n| \"looks good, ship it\" (during planning) | Runs plan audit + cross-model review |\n| \"nevermind, cancel this\" (during planning) | Exits planning mode |\n| \"hey kaylee can you look at this?\" | Routes to Kaylee (no `@` needed) |\n| \"@mal close this issue\" | Routes to Mal (one-time detour) and closes the issue |\n| \"what can I do here?\" | Default agent responds (not silently dropped) |\n| \"fix the search bug\" | Default agent dispatches work (if issue is In Progress) |\n| \"build me a stock trading app\" (on Backlog issue) | Blocked — agent explains the issue needs scoping first |\n| \"close this\" / \"mark as done\" / \"this is resolved\" | Generates closure report, transitions issue to completed |\n\nIntent classification runs on **all webhook paths** — not just `Comment.create`. `AgentSessionEvent.created`, `AgentSessionEvent.prompted`, and `@mention` fast paths all classify intent and enforce scope.\n\n\u003e **Tip:** Configure `classifierAgentId` to point to a small/fast model agent (like Haiku) for low-latency, low-cost intent classification. The classifier only needs ~300 tokens per call.\n\n### Scope Enforcement — Work Request Gate\n\nWhen a user's intent is classified as `request_work`, the plugin checks the issue's workflow state before dispatching. Issues that haven't reached **In Progress** get a polite rejection instead of an agent run:\n\n```\nThis issue (ENG-123) is in Backlog — it needs planning and scoping before implementation.\n\nTo move forward:\n1. Update the issue description with requirements and acceptance criteria\n2. Move the issue to In Progress\n3. Then ask me to implement it\n\nI can help you scope and plan — just ask questions or discuss the approach.\n```\n\nThis prevents the most common failure mode: an agent receiving a casual comment like \"build X\" on an unscoped issue and immediately spinning up a CLI tool to build something that was never planned.\n\n**What's allowed on untriaged issues:**\n- Questions, discussion, scope refinement\n- Planning (`plan_start`, `plan_continue`, `plan_finalize`)\n- Agent routing (`@mention` or natural language)\n- Issue closure\n- `cli_codex`/`cli_claude`/`cli_gemini` in **planning mode only** — workers can explore code and write plan files but cannot create, modify, or delete source code\n\n**What's blocked on untriaged issues:**\n- `request_work` intent — the gate returns a rejection message before any agent runs\n- Full CLI tool implementation — even if the orchestrator LLM ignores prompt rules, a `before_tool_call` hook prepends hard planning-only constraints to the worker's prompt\n\n### Agent Routing\n\nThe plugin supports a multi-agent team where one agent is the default (`isDefault: true` in agent profiles) and others are routed to on demand. Routing works across all webhook paths:\n\n| Webhook Path | How agent is selected | Scope gate |\n|---|---|---|\n| `Comment.create` | `@mention` → specific agent (with intent gate). No mention → intent classifier may detect agent name (\"hey kaylee\") → `ask_agent` intent. Otherwise → default agent. | `request_work` blocked on untriaged |\n| `AgentSessionEvent.created` | Scans user's message for `@mention` aliases → routes to mentioned agent. No mention → default agent. | `request_work` blocked on untriaged |\n| `AgentSessionEvent.prompted` | Same as `created` — scans follow-up message for `@mention` → one-time detour. No mention → default agent. | `request_work` blocked on untriaged |\n| `Issue.update` (assignment) | Always dispatches to default agent. | Full dispatch pipeline |\n| `Issue.create` (triage) | Always dispatches to default agent. | Read-only triage |\n\n**One-time detour:** When you `@mention` an agent in a session that belongs to a different default agent, the mentioned agent handles that single interaction. The session itself stays owned by whoever created it — subsequent messages without `@mentions` go back to the default. This lets you ask a specific agent for help without permanently switching context.\n\n**Agent profiles** are configured in `~/.openclaw/agent-profiles.json`:\n\n```json\n{\n  \"agents\": {\n    \"mal\": {\n      \"label\": \"Mal\",\n      \"mentionAliases\": [\"mal\"],\n      \"isDefault\": false\n    },\n    \"zoe\": {\n      \"label\": \"Zoe\",\n      \"mentionAliases\": [\"zoe\"],\n      \"isDefault\": true\n    }\n  }\n}\n```\n\nEach agent needs a unique set of `mentionAliases`. The `appAliases` field (e.g. `[\"ctclaw\"]`) is separate — those trigger `AgentSessionEvent` from Linear's own `@app` mention system, not the plugin's routing.\n\n### Deduplication\n\nThe webhook handler prevents double-processing through a two-tier guard system:\n\n1. **`activeRuns` (in-memory Set)** — O(1) check if an agent is already running for an issue. Catches feedback loops where our own API calls (e.g., `createComment`, `createSessionOnIssue`) trigger webhooks back to us.\n\n2. **`wasRecentlyProcessed` (TTL Map, 60s)** — Catches exact-duplicate webhook deliveries. Each event type uses a specific dedup key:\n\n| Event | Dedup Key | Guards (in order) |\n|---|---|---|\n| `AgentSessionEvent.created` | `session:\u003csessionId\u003e` | activeRuns → wasRecentlyProcessed |\n| `AgentSessionEvent.prompted` | `webhook:\u003cwebhookId\u003e` | activeRuns → wasRecentlyProcessed |\n| `Comment.create` | `comment:\u003ccommentId\u003e` | wasRecentlyProcessed → viewerId → activeRuns |\n| `Issue.update` | `\u003ctrigger\u003e:\u003cissueId\u003e:\u003cviewerId\u003e` | activeRuns → no-change → viewerId → wasRecentlyProcessed |\n| `Issue.create` | `issue-create:\u003cissueId\u003e` | wasRecentlyProcessed → activeRuns → planning mode → bot-created |\n| `AppUserNotification` | *(immediate discard)* | — |\n\n`AppUserNotification` events are discarded because they duplicate events already received via the workspace webhook (e.g., `Comment.create` for mentions, `Issue.update` for assignments). Processing both would cause double agent runs.\n\n**Response delivery:** When an agent session exists, responses are delivered via `emitActivity(type: \"response\")` — not `createComment`. This prevents duplicate visible messages on the issue. `createComment` is only used as a fallback when `emitActivity` fails or when no agent session exists.\n\n**Comment echo prevention:** Comments posted outside of sessions use `createCommentWithDedup()`, which pre-registers the comment's ID in `wasRecentlyProcessed` immediately after the API returns. When Linear echoes the `Comment.create` webhook back, it's caught before any processing.\n\n---\n\n## Planning — Validation Details\n\nSee [Phase 1: Planning](#phase-1-planning-optional) for the full walkthrough. This section covers the validation rules that run when you say \"finalize plan\".\n\n### Validation checks\n\n- Every issue has a description (50+ characters) with acceptance criteria\n- Every non-epic issue has an estimate and priority\n- No circular dependencies in the DAG\n\n**If validation fails:**\n\n\u003e ## Plan Audit Failed\n\u003e\n\u003e **Problems:**\n\u003e - PROJ-2: description too short (\u003c 50 chars)\n\u003e - PROJ-3: missing estimate\n\u003e\n\u003e **Warnings:**\n\u003e - PROJ-4: no acceptance criteria found in description\n\u003e\n\u003e Please address these issues, then say \"finalize plan\" again.\n\nFix the issues and try again. Say \"cancel\" or \"stop planning\" to exit without dispatching.\n\n---\n\n## Quick Reference\n\n| What you do in Linear | What happens |\n|---|---|\n| Create a new issue | Agent triages — adds estimate, labels, priority |\n| Assign an issue to the agent | Worker → Audit → Done (or retry, or escalate) |\n| Comment anything on an issue | Intent classifier routes to the right handler |\n| Mention an agent by name (with or without `@`) | That agent responds |\n| Ask a question or request work | Default agent handles it |\n| Say \"close this\" / \"mark as done\" / \"this is resolved\" | Closure report posted, issue moved to completed |\n| Say \"plan this project\" (on a project issue) | Planning interview starts |\n| Reply during planning | Issues created/updated with user stories \u0026 AC |\n| Say \"looks good\" / \"finalize plan\" | Validates → cross-model review → approval |\n| Say \"approve plan\" (after review) | Dispatches all issues in dependency order |\n| Say \"cancel\" / \"abandon planning\" | Exits planning mode |\n| `/dispatch list` | Shows all active dispatches |\n| `/dispatch retry CT-123` | Re-runs a stuck dispatch |\n| `/dispatch status CT-123` | Detailed dispatch info |\n| Add `\u003c!-- repos: api, frontend --\u003e` to issue body | Multi-repo dispatch |\n\n---\n\n## Configuration\n\nAdd settings under the plugin entry in `openclaw.json`:\n\n```json\n{\n  \"plugins\": {\n    \"entries\": {\n      \"openclaw-linear\": {\n        \"config\": {\n          \"defaultAgentId\": \"coder\",\n          \"maxReworkAttempts\": 2,\n          \"enableAudit\": true\n        }\n      }\n    }\n  }\n}\n```\n\n### Plugin Settings\n\n| Key | Type | Default | What it does |\n|---|---|---|---|\n| `defaultAgentId` | string | `\"default\"` | Which agent runs the pipeline |\n| `classifierAgentId` | string | — | Agent for intent classification (use a small/fast model like Haiku) |\n| `plannerReviewModel` | string | auto | Cross-model plan reviewer: `\"claude\"`, `\"codex\"`, or `\"gemini\"`. Auto-detects the complement of your primary model. |\n| `enableAudit` | boolean | `true` | Run auditor after implementation |\n| `enableOrchestration` | boolean | `true` | Allow `spawn_agent` / `ask_agent` tools |\n| `maxReworkAttempts` | number | `2` | Max audit failures before escalation |\n| `codexBaseRepo` | string | `\"/home/claw/ai-workspace\"` | Git repo for worktrees |\n| `worktreeBaseDir` | string | `\"~/.openclaw/worktrees\"` | Where worktrees are created |\n| `repos` | object | — | Multi-repo map (see [Multi-Repo](#multi-repo)) |\n| `projectName` | string | — | Human-readable project name (injected into agent prompts) |\n| `dispatchStatePath` | string | `\"~/.openclaw/linear-dispatch-state.json\"` | Dispatch state file |\n| `planningStatePath` | string | `\"~/.openclaw/linear-planning-state.json\"` | Planning session state file |\n| `promptsPath` | string | — | Custom prompts file path |\n| `notifications` | object | — | Notification targets (see [Notifications](#notifications)) |\n| `inactivitySec` | number | `120` | Kill agent if silent this long |\n| `maxTotalSec` | number | `7200` | Max total agent session time |\n| `toolTimeoutSec` | number | `600` | Max single CLI tool time |\n| `enableGuidance` | boolean | `true` | Inject Linear workspace/team guidance into agent prompts |\n| `teamGuidanceOverrides` | object | — | Per-team guidance toggle. Key = team ID, value = boolean. Unset teams inherit `enableGuidance`. |\n| `claudeApiKey` | string | — | Anthropic API key for Claude CLI (passed as `ANTHROPIC_API_KEY` env var). Required if using Claude backend. |\n\n### Environment Variables\n\n| Variable | Required | What it does |\n|---|---|---|\n| `LINEAR_CLIENT_ID` | Yes | OAuth app client ID |\n| `LINEAR_CLIENT_SECRET` | Yes | OAuth app client secret |\n| `LINEAR_API_KEY` | No | Personal API key (fallback) |\n\n### Agent Profiles\n\nDefine your agents in `~/.openclaw/agent-profiles.json`:\n\n```json\n{\n  \"agents\": {\n    \"coder\": {\n      \"label\": \"Coder\",\n      \"mission\": \"Full-stack engineer. Plans, implements, ships.\",\n      \"isDefault\": true,\n      \"mentionAliases\": [\"coder\"],\n      \"avatarUrl\": \"https://example.com/coder.png\",\n      \"watchdog\": {\n        \"inactivitySec\": 180,\n        \"maxTotalSec\": 7200,\n        \"toolTimeoutSec\": 900\n      }\n    },\n    \"qa\": {\n      \"label\": \"QA\",\n      \"mission\": \"Test engineer. Reviews code, writes tests.\",\n      \"mentionAliases\": [\"qa\", \"tester\"]\n    }\n  }\n}\n```\n\nOne agent must have `\"isDefault\": true` — that's the one that handles triage and the dispatch pipeline.\n\n### Coding Tools\n\nCreate `coding-tools.json` in the plugin root to configure which CLI backend agents use:\n\n\u003e **Warning — Claude Code (Anthropic) and headless/automated usage**\n\u003e\n\u003e Calling Claude Code via CLI in a headless or automated context (which is how this plugin\n\u003e uses it) may violate [Anthropic's Terms of Service](https://www.anthropic.com/terms).\n\u003e The default backend is **Codex CLI** (OpenAI). **Gemini CLI** (Google) is used as the\n\u003e cross-model reviewer. If you choose to use Claude despite this, you do so at your own risk.\n\u003e See [Claude API Key](#claude-api-key) below for opt-in configuration.\n\n```json\n{\n  \"codingTool\": \"codex\",\n  \"agentCodingTools\": {},\n  \"backends\": {\n    \"claude\": { \"aliases\": [\"claude\", \"claude code\", \"anthropic\"] },\n    \"codex\": { \"aliases\": [\"codex\", \"openai\"] },\n    \"gemini\": { \"aliases\": [\"gemini\", \"google\"] }\n  }\n}\n```\n\nEach backend registers as a dedicated tool — `cli_codex`, `cli_claude`, or `cli_gemini` — so agents and Linear's session UI show exactly which backend is running. The agent's prompt references the correct tool name automatically. Resolution order for which tool is exposed: per-agent override (`agentCodingTools`) \u003e global default (`codingTool`) \u003e `\"codex\"`.\n\n#### Claude API Key\n\nIf you opt in to using Claude as a backend (despite the TOS concerns noted above), you can\nprovide an Anthropic API key so the Claude CLI authenticates via API key instead of its\nbuilt-in interactive auth.\n\nSet `claudeApiKey` in the plugin config:\n\n```json\n{\n  \"plugins\": {\n    \"entries\": {\n      \"openclaw-linear\": {\n        \"config\": {\n          \"claudeApiKey\": \"sk-ant-...\"\n        }\n      }\n    }\n  }\n}\n```\n\nThe key is passed to the Claude CLI subprocess as the `ANTHROPIC_API_KEY` environment variable.\nYou can also set `ANTHROPIC_API_KEY` as a process-level environment variable (e.g., in your\nsystemd unit file) as a fallback. The plugin config value takes precedence if both are set.\n\n---\n\n## Notifications\n\nGet notified when dispatches start, pass audit, fail, or get stuck.\n\n### Setup\n\n```json\n{\n  \"notifications\": {\n    \"targets\": [\n      { \"channel\": \"discord\", \"target\": \"1471743433566715974\" },\n      { \"channel\": \"telegram\", \"target\": \"-1003884997363\" },\n      { \"channel\": \"slack\", \"target\": \"C0123456789\", \"accountId\": \"my-acct\" }\n    ],\n    \"events\": {\n      \"auditing\": false\n    },\n    \"richFormat\": true\n  }\n}\n```\n\n- **`targets`** — Where to send notifications (channel name + ID)\n- **`events`** — Toggle specific events off (all on by default)\n- **`richFormat`** — Set to `true` for Discord embeds with colors and Telegram HTML formatting\n\n### Events\n\n| Event | When it fires |\n|---|---|\n| `dispatch` | Issue dispatched to pipeline |\n| `working` | Worker started |\n| `auditing` | Audit in progress |\n| `audit_pass` | Audit passed, issue done |\n| `audit_fail` | Audit failed, worker retrying |\n| `escalation` | Too many failures, needs human |\n| `stuck` | Dispatch stale for 2+ hours |\n| `watchdog_kill` | Agent killed for inactivity |\n\n### Test It\n\n```bash\nopenclaw openclaw-linear notify test              # Test all targets\nopenclaw openclaw-linear notify test --channel discord  # Test one channel\nopenclaw openclaw-linear notify status             # Show config\n```\n\n---\n\n## Prompt Customization\n\nWorker, audit, and rework prompts live in `prompts.yaml`. You can customize them without rebuilding.\n\n### Three Layers\n\nPrompts merge in this order (later layers override earlier ones):\n\n1. **Built-in defaults** — Ship with the plugin, always available\n2. **Your global file** — Set `promptsPath` in config to point to your custom YAML\n3. **Per-project file** — Drop a `prompts.yaml` in the worktree's `.claw/` folder\n\nEach layer only overrides the specific sections you define. Everything else keeps its default.\n\n### Linear Guidance\n\nLinear's [agent guidance system](https://linear.app/docs/agents-in-linear) lets admins configure workspace-wide and team-specific instructions for agents. This plugin automatically extracts that guidance and appends it as supplementary instructions to all agent prompts.\n\nGuidance is configured in Linear at:\n- **Workspace level:** Settings \u003e Agents \u003e Additional guidance (applies across entire org)\n- **Team level:** Team settings \u003e Agents \u003e Additional guidance (takes priority over workspace guidance)\n\nSee [Agents in Linear](https://linear.app/docs/agents-in-linear) for full documentation on how guidance works.\n\nGuidance flows into:\n- **Orchestrator prompts** — AgentSessionEvent and comment handler paths\n- **Worker prompts** — Appended to the task via `{{guidance}}` template variable\n- **Audit prompts** — Appended to the audit task\n- **Triage and closure prompts** — Appended to the triage and close_issue handlers\n\nGuidance is cached per-team (24h TTL) so comment webhooks (which don't carry guidance from Linear) can also benefit.\n\n**Disable guidance globally:**\n```json\n{ \"enableGuidance\": false }\n```\n\n**Disable for a specific team:**\n```json\n{\n  \"enableGuidance\": true,\n  \"teamGuidanceOverrides\": { \"team-id-here\": false }\n}\n```\n\n### Project Context — CLAUDE.md \u0026 AGENTS.md\n\nAgents are instructed to read two files from the repo root before starting work:\n\n- **CLAUDE.md** — Project conventions, tech stack, build/test commands, architecture. This is the same convention used by Claude Code and other AI coding tools.\n- **AGENTS.md** — Behavioral guidelines, code style rules, workflow conventions (branch naming, commit format, etc.).\n\nThese files are the primary way agents learn about your project. Without them, agents will explore the codebase but may miss conventions.\n\nRun `openclaw openclaw-linear doctor` to check if these files exist. The doctor output includes templates to get started.\n\n### Example Custom Prompts\n\n```yaml\nworker:\n  system: \"You are a senior engineer. Write clean, tested code.\"\n  task: |\n    Issue: {{identifier}} — {{title}}\n\n    {{description}}\n\n    Workspace: {{worktreePath}}\n\n    Implement this issue. Write tests. Commit your work.\n\naudit:\n  system: \"You are a strict code auditor.\"\n\nrework:\n  addendum: |\n    PREVIOUS AUDIT FAILED. Fix these gaps:\n    {{gaps}}\n```\n\n### Template Variables\n\n| Variable | What it contains |\n|---|---|\n| `{{identifier}}` | Issue ID (e.g., `API-123`) |\n| `{{title}}` | Issue title |\n| `{{description}}` | Full issue body |\n| `{{worktreePath}}` | Path to the git worktree |\n| `{{tier}}` | Complexity tier (small/medium/high) |\n| `{{attempt}}` | Current attempt number |\n| `{{gaps}}` | Audit gaps from previous attempt |\n| `{{projectName}}` | Project name (planner prompts) |\n| `{{planSnapshot}}` | Current plan structure (planner prompts) |\n| `{{reviewModel}}` | Name of cross-model reviewer (planner review) |\n| `{{crossModelFeedback}}` | Review recommendations (planner review) |\n| `{{guidance}}` | Linear workspace/team guidance (if available, empty string otherwise) |\n| `{{projectContext}}` | Auto-detected project context (repo paths, framework, build commands, test commands) injected into worker and audit prompts. |\n\n### CLI\n\n```bash\nopenclaw openclaw-linear prompts show       # View current prompts\nopenclaw openclaw-linear prompts path       # Show file path\nopenclaw openclaw-linear prompts validate   # Check for errors\n```\n\n---\n\n## Multi-Repo\n\nSometimes a feature touches more than one repo — your API and your frontend, for example. Multi-repo lets the agent work on both at the same time, in separate worktrees.\n\n### Step 1: Tell the plugin where your repos live\n\nAdd a `repos` map to your plugin config in `openclaw.json`. The **key** is a short name you pick, the **value** is the absolute path to that repo on disk:\n\n```json\n{\n  \"plugins\": {\n    \"entries\": {\n      \"openclaw-linear\": {\n        \"config\": {\n          \"repos\": {\n            \"api\": \"/home/claw/repos/api\",\n            \"frontend\": \"/home/claw/repos/frontend\",\n            \"shared\": \"/home/claw/repos/shared-libs\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nRestart the gateway after saving: `systemctl --user restart openclaw-gateway`\n\n### Step 1.5: Sync labels to Linear (optional)\n\nIf you plan to use labels (Method B below) to tag issues, run this to create the `repo:xxx` labels automatically:\n\n```bash\nopenclaw openclaw-linear repos sync\n```\n\nThis reads your `repos` config and creates matching labels (`repo:api`, `repo:frontend`, etc.) in every Linear team. To preview without creating anything:\n\n```bash\nopenclaw openclaw-linear repos check\n```\n\nThe check command also validates your repo paths — it'll warn you if a path doesn't exist, isn't a git repo, or is a **submodule** (which won't work with multi-repo dispatch).\n\n### Step 2: Tag the issue\n\nWhen you write an issue in Linear that needs multiple repos, tell the plugin which ones. Pick **one** of these methods:\n\n#### Method A: HTML comment in the issue body (recommended)\n\nPut this line anywhere in the issue description — it's invisible in Linear's UI:\n\n```\n\u003c!-- repos: api, frontend --\u003e\n```\n\nFull example of what an issue body might look like:\n\n```\nThe search endpoint needs to be added to the API, and the frontend\nneeds a new search page that calls it.\n\n\u003c!-- repos: api, frontend --\u003e\n\nAcceptance criteria:\n- GET /api/search?q=term returns results\n- /search page renders results with pagination\n```\n\n#### Method B: Linear labels\n\nCreate labels in Linear called `repo:api` and `repo:frontend`, then add them to the issue. The part after `repo:` must match the key in your config.\n\n#### Method C: Do nothing (config default)\n\nIf you don't tag the issue at all, the plugin uses your `codexBaseRepo` setting (a single repo). This is how it worked before multi-repo existed — nothing changes for single-repo issues.\n\n### What happens when you dispatch\n\nWhen the agent picks up a multi-repo issue, the dispatch comment tells you:\n\n\u003e **Dispatched** as **high** (anthropic/claude-opus-4-6)\n\u003e\n\u003e Worktrees:\n\u003e - `api` → `/home/claw/worktrees/ENG-100/api`\n\u003e - `frontend` → `/home/claw/worktrees/ENG-100/frontend`\n\u003e\n\u003e Branch: `codex/ENG-100`\n\nThe agent gets access to all the worktrees and can edit files across repos in one session. Each repo gets its own git branch.\n\n### Priority order\n\nIf an issue has both a body marker and labels, the body marker wins. Full order:\n\n1. `\u003c!-- repos: ... --\u003e` in the issue body\n2. `repo:xxx` labels on the issue\n3. `codexBaseRepo` from config (single repo fallback)\n\n### Common mistakes\n\n| Problem | Fix |\n|---|---|\n| Agent only sees one repo | The name in `\u003c!-- repos: api --\u003e` must exactly match a key in your `repos` config. Check spelling. |\n| \"Could not create worktree\" error | The path in your `repos` config doesn't exist, or it's not a git repo. Run `ls /home/claw/repos/api/.git` to check. |\n| Comment marker not detected | Must be `\u003c!-- repos: name1, name2 --\u003e` with the exact format. No extra spaces around `\u003c!--` or `--\u003e`. |\n| Labels not picked up | Labels must be formatted `repo:name` (lowercase, no spaces). The `name` part must match a `repos` config key. |\n\n---\n\n## Dispatch Management\n\n### Slash Commands\n\nType these in any agent session — they run instantly, no AI involved:\n\n| Command | What it does |\n|---|---|\n| `/dispatch list` | Show all active dispatches with age, tier, status |\n| `/dispatch status CT-123` | Detailed info for one dispatch |\n| `/dispatch retry CT-123` | Re-run a stuck dispatch |\n| `/dispatch escalate CT-123 \"needs review\"` | Force a dispatch to stuck status |\n\n### Gateway API\n\nFor programmatic access, the plugin registers these RPC methods:\n\n| Method | What it does |\n|---|---|\n| `dispatch.list` | List dispatches (filterable by status, tier) |\n| `dispatch.get` | Get full dispatch details |\n| `dispatch.retry` | Re-dispatch a stuck issue |\n| `dispatch.escalate` | Force-stuck with a reason |\n| `dispatch.cancel` | Remove an active dispatch |\n| `dispatch.stats` | Counts by status and tier |\n\n---\n\n## Watchdog\n\nIf an agent goes silent (LLM timeout, API hang, CLI lockup), the watchdog handles it automatically:\n\n1. No output for `inactivitySec` → kill and retry once\n2. Second silence → escalate to stuck (you get notified, see [Timeout recovery](#timeout-recovery) above)\n\n| Setting | Default | What it controls |\n|---|---|---|\n| `inactivitySec` | 120s | Kill if no output for this long |\n| `maxTotalSec` | 7200s (2 hrs) | Hard ceiling on total session time |\n| `toolTimeoutSec` | 600s (10 min) | Max time for a single CLI tool call |\n\nConfigure per-agent in `agent-profiles.json` or globally in plugin config.\n\n---\n\n## Agent Tools\n\nEvery agent session gets these registered tools. They're available as native tool calls — no CLI parsing, no shell execution, no flag guessing.\n\n### `cli_codex` / `cli_claude` / `cli_gemini` — Coding backend tools\n\nThree per-backend tools that send tasks to their respective coding CLIs. Each agent sees only the tool matching its configured backend (e.g., an agent configured for `codex` gets `cli_codex`). All activity emissions are prefixed with the backend label (`[Codex]`, `[Claude]`, `[Gemini]`) — thoughts, action starts, progress updates, results, and errors — so the Linear session UI always shows which runner is active. When a CLI tool is dispatched, an immediate thought is emitted with the prompt excerpt before the long-running task begins. The agent writes the prompt; the plugin handles worktree setup, session activity streaming, and output capture.\n\n### `linear_issues` — Native Linear API\n\nAgents call `linear_issues` with typed JSON parameters. The tool wraps the Linear GraphQL API directly and handles all name-to-ID resolution automatically.\n\n| Action | What it does | Key parameters |\n|---|---|---|\n| `read` | Get full issue details (status, labels, comments, relations) | `issueId` |\n| `create` | Create a new issue or sub-issue | `title`, `description`, `teamId` or `parentIssueId` |\n| `update` | Change status, priority, labels, estimate, or title | `issueId` + fields |\n| `comment` | Post a comment on an issue | `issueId`, `body` |\n| `list_states` | Get available workflow states for a team | `teamId` |\n| `list_labels` | Get available labels for a team | `teamId` |\n\n**Sub-issues:** Use `action=\"create\"` with `parentIssueId` to create sub-issues under an existing issue. The new issue inherits `teamId` and `projectId` from its parent automatically. Only orchestrators on triaged issues have `create` access — workers and auditors cannot create issues.\n\n### `spawn_agent` / `ask_agent` — Multi-agent orchestration\n\nDelegate work to other crew agents. `spawn_agent` is fire-and-forget (parallel), `ask_agent` waits for a reply (synchronous). Disabled with `enableOrchestration: false`.\n\nSub-agents run in their own context — they do **not** share the parent's worktree or get CLI tool access. They're useful for reasoning, research, and coordination (e.g., \"ask Inara how to phrase this error message\") but cannot directly modify code. To give a sub-agent code context, include the relevant snippets in the task message.\n\n### `dispatch_history` — Recent dispatch context\n\nReturns recent dispatch activity. Agents use this for situational awareness when working on related issues.\n\n### Access model\n\nTool access varies by context. Orchestrators get the full toolset; workers and auditors are restricted:\n\n| Context | `linear_issues` | `cli_*` | `spawn_agent` / `ask_agent` | Filesystem |\n|---|---|---|---|---|\n| Orchestrator (triaged issue) | Full (read, create, update, comment) | Yes (backend-specific tool) | Yes | Read + write |\n| Orchestrator (untriaged issue) | Read only | Planning only | Yes | Read + write |\n| Worker | None | None | None | Read + write |\n| Auditor | Prompt-constrained (has tool, instructed to verify only) | None | None | Read only (by prompt) |\n| Sub-agent (spawn/ask) | None | None | Yes (can chain) | Inherited from parent |\n\n**Workers** run inside the coding backend (Codex, Claude, Gemini) — they have full filesystem access to the worktree but no Linear tools and no orchestration. Their only job is to write code and return a summary.\n\n**Auditors** have access to `linear_issues` (the tool is registered) but are instructed via prompt to verify only — they return a JSON verdict, not code or issue mutations. Write access is not enforced at the tool level.\n\n**Sub-agents** spawned via `spawn_agent`/`ask_agent` run in their own session with no worktree access and no CLI tools. They're information workers — useful for reasoning and coordination, not code execution.\n\n---\n\n## Linear API \u0026 Hook Architecture\n\nThis section documents every interaction between the plugin and the Linear GraphQL API, the webhook event routing, the hook lifecycle, and the dispatch pipeline internals.\n\n### GraphQL API Layer\n\nAll Linear API calls go through `LinearAgentApi` (`src/api/linear-api.ts`), which wraps `https://api.linear.app/graphql` with automatic token refresh, retry resilience, and 401 recovery.\n\n**Token resolution** (`resolveLinearToken`) checks three sources in priority order:\n\n1. `pluginConfig.accessToken` — static config\n2. Auth profile store (`~/.openclaw/auth-profiles.json`) — OAuth tokens with auto-refresh\n3. `LINEAR_ACCESS_TOKEN` / `LINEAR_API_KEY` environment variable\n\nOAuth tokens get a `Bearer` prefix; personal API keys do not. Tokens are refreshed 60 seconds before expiry via `refreshLinearToken()`, and the refreshed credentials are persisted back to the auth profile store.\n\n**API methods by category:**\n\n| Category | Method | GraphQL Operation | Used By |\n|---|---|---|---|\n| **Issues** | `getIssueDetails(issueId)` | `query Issue` | Triage, audit, close, `linear_issues` tool |\n| | `createIssue(input)` | `mutation IssueCreate` | Planner |\n| | `updateIssue(issueId, input)` | `mutation IssueUpdate` | Triage (labels, estimate, priority) |\n| | `updateIssueExtended(issueId, input)` | `mutation IssueUpdate` | `linear_issues` tool, close handler |\n| | `createIssueRelation(input)` | `mutation IssueRelationCreate` | Planner (dependency DAG) |\n| **Comments** | `createComment(issueId, body, opts)` | `mutation CommentCreate` | All phases (fallback delivery) |\n| | `createReaction(commentId, emoji)` | `mutation ReactionCreate` | Acknowledgment reactions |\n| **Sessions** | `createSessionOnIssue(issueId)` | `mutation AgentSessionCreateOnIssue` | Comment handler, close handler |\n| | `emitActivity(sessionId, content)` | `mutation AgentActivityCreate` | Primary response delivery |\n| | `updateSession(sessionId, input)` | `mutation AgentSessionUpdate` | External URLs, plan text |\n| **Teams** | `getTeamStates(teamId)` | `query TeamStates` | `linear_issues` tool, close handler |\n| | `getTeamLabels(teamId)` | `query TeamLabels` | `linear_issues` tool, triage |\n| | `getTeams()` | `query Teams` | Doctor health check |\n| | `createLabel(teamId, name, opts)` | `mutation IssueLabelCreate` | Triage (auto-create labels) |\n| **Projects** | `getProject(projectId)` | `query Project` | Planner |\n| | `getProjectIssues(projectId)` | `query ProjectIssues` | Planner, DAG dispatch |\n| **Webhooks** | `listWebhooks()` | `query Webhooks` | Doctor, webhook setup CLI |\n| | `createWebhook(input)` | `mutation WebhookCreate` | Webhook setup CLI |\n| | `updateWebhook(id, input)` | `mutation WebhookUpdate` | Webhook management |\n| | `deleteWebhook(id)` | `mutation WebhookDelete` | Webhook cleanup |\n| **Notifications** | `getAppNotifications(count)` | `query Notifications` | Doctor (connectivity check) |\n| **Identity** | `getViewerId()` | `query Viewer` | Self-comment filtering |\n\n### Webhook Event Routing\n\nThe plugin registers an HTTP route at `/linear/webhook` that receives POST payloads from two Linear webhook sources:\n\n1. **Workspace webhook** — `Comment.create`, `Issue.update`, `Issue.create`\n2. **OAuth app webhook** — `AgentSessionEvent.created`, `AgentSessionEvent.prompted`\n\nBoth must point to the same URL. `AgentSessionEvent` payloads carry workspace/team guidance which is extracted, cached per-team, and appended to all agent prompts. Comment webhook paths use the cached guidance since Linear does not include guidance in `Comment.create` payloads. See [Linear Guidance](#linear-guidance).\n\nThe handler dispatches by `type + action`:\n\n```mermaid\nflowchart TD\n    A[\"POST /linear/webhook\"] --\u003e B{\"Event Type\"}\n    B --\u003e C[\"AgentSessionEvent.created\u003cbr/\u003e→ dedup → scan @mentions → intent classify → scope gate → run agent\"]\n    B --\u003e D[\"AgentSessionEvent.prompted\u003cbr/\u003e→ dedup → scan @mentions → intent classify → scope gate → resume agent\"]\n    B --\u003e E[\"Comment.create\u003cbr/\u003e→ filter self → dedup → intent classify → scope gate → route\"]\n    B --\u003e F[\"Issue.update\u003cbr/\u003e→ check assignment → dispatch\"]\n    B --\u003e G[\"Issue.create\u003cbr/\u003e→ triage (estimate, labels, priority)\"]\n    B --\u003e H[\"AppUserNotification\u003cbr/\u003e→ discarded (duplicates workspace events)\"]\n```\n\n### Intent Classification\n\nEvery user request — across all 4 webhook dispatch paths — is classified through a two-tier intent system before the agent runs:\n\n1. **LLM classifier** (~300 tokens, ~2-5s) — a small/fast model parses the message and returns structured JSON with intent + reasoning\n2. **Regex fallback** — if the LLM call fails or times out, static patterns catch common cases\n\n**Where it runs:**\n\n| Webhook Path | Classification Point |\n|---|---|\n| `Comment.create` (no @mention) | Before routing by intent |\n| `Comment.create` (@mention fast path) | After resolving agent, before dispatch |\n| `AgentSessionEvent.created` | After enriching issue, before building prompt |\n| `AgentSessionEvent.prompted` | After enriching issue, before building follow-up prompt |\n\n**Intents:**\n\n| Intent | Trigger | Handler | Blocked on untriaged? |\n|---|---|---|---|\n| `plan_start` | \"let's plan the features\" | Start planner interview session | No |\n| `plan_finalize` | \"looks good, ship it\" | Run plan audit + cross-model review | No |\n| `plan_abandon` | \"cancel planning\" | End planning session | No |\n| `plan_continue` | Any message during active planning | Continue planner conversation | No |\n| `ask_agent` | \"@kaylee\" or \"hey kaylee\" | Route to specific agent by name | No |\n| `request_work` | \"fix the search bug\" | Dispatch to default agent | **Yes** |\n| `question` | \"what's the status?\" | Agent answers without code changes | No |\n| `close_issue` | \"close this\" / \"mark as done\" | Generate closure report + transition state | No |\n| `general` | Noise, automated messages | Silently dropped | No |\n\nThe `request_work` intent is the only one gated by issue state. When the issue is not in **In Progress** (i.e., `stateType !== \"started\"`), the gate returns a rejection message explaining what the user needs to do. All other intents are allowed regardless of issue state — you can always plan, ask questions, discuss scope, and close issues.\n\n### Hook Lifecycle\n\nThe plugin registers completion + lifecycle hooks via `api.on()` in `index.ts`.\nFor completion events it listens to `agent_end`, `task_completed`, and `task_completion`\nto stay compatible across OpenClaw lifecycle event changes.\n\n**Completion hooks (`agent_end` / `task_completed` / `task_completion`)** — Dispatch pipeline state machine. When a sub-agent (worker or auditor) finishes:\n- Looks up the session key in dispatch state to find the active dispatch\n- Validates the attempt number matches (rejects stale events from old retries)\n- If the worker finished → triggers the audit phase (`triggerAudit`)\n- If the auditor finished → processes the verdict (`processVerdict` → pass/fail/stuck)\n\n**`before_agent_start`** — Context injection. For `linear-worker-*` and `linear-audit-*` sessions:\n- Reads dispatch state and finds up to 3 active dispatches\n- Prepends a `\u003cdispatch-history\u003e` block so the agent has situational awareness of concurrent work\n\n**`before_tool_call`** — Planning-only enforcement for CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`). When the active issue is not in \"started\" state:\n- Fetches the issue's current workflow state from the Linear API\n- If the issue is in Triage, Todo, Backlog, or any non-started state, prepends hard constraints to the worker's prompt:\n  - Workers may read/explore files and write plan files (PLAN.md, design docs)\n  - Workers must NOT create/modify/delete source code, run deployments, or make system changes\n- This is the deepest layer of scope enforcement — even if the orchestrator LLM ignores prompt-level scope rules and calls a CLI tool anyway, the worker receives constraints that prevent implementation\n\n**`message_sending`** — Narration guard. Catches short (~250 char) \"Let me explore...\" responses where the agent narrates intent without actually calling tools:\n- Appends a warning: \"Agent acknowledged but may not have completed the task\"\n- Prevents users from thinking the agent did something when it only said it would\n\n### Response Delivery\n\nAgent responses follow an **emitActivity-first** pattern:\n\n1. Try `emitActivity(sessionId, { type: \"response\", body })` — appears as agent activity in Linear's UI, no duplicate comment\n2. If `emitActivity` fails (no session, API error) → fall back to `createComment(issueId, body)`\n3. Comments posted outside sessions use `createCommentWithDedup()` — pre-registers the comment ID to prevent the echo webhook from triggering reprocessing\n\n### Close Issue Flow\n\nWhen intent classification returns `close_issue`:\n\n```mermaid\nflowchart TD\n    A[\"close_issue intent\"] --\u003e B[\"Fetch issue details\"]\n    B --\u003e C[\"Find team's completed state\"]\n    C --\u003e D[\"Create agent session on issue\"]\n    D --\u003e E[\"Emit 'preparing closure report' thought\"]\n    E --\u003e F[\"Run agent in read-only mode\u003cbr/\u003e\u003ci\u003e(generates closure report)\u003c/i\u003e\"]\n    F --\u003e G[\"Transition issue → completed\"]\n    G --\u003e H[\"Post closure report\u003cbr/\u003e\u003ci\u003e(emitActivity → createComment fallback)\u003c/i\u003e\"]\n```\n\nThis is a **static action** — the intent triggers direct API calls orchestrated by the plugin, not by giving the agent write tools. The agent only generates the closure report text; all state transitions are handled by the plugin.\n\n### Dispatch Pipeline Internals\n\nThe full dispatch flow for implementing an issue:\n\n```mermaid\nflowchart TD\n    A[\"Issue assigned to app user\"] --\u003e B[\"1. Assess complexity tier\u003cbr/\u003e\u003ci\u003esmall / medium / high\u003c/i\u003e\"]\n    B --\u003e C[\"2. Create isolated git worktree\"]\n    C --\u003e D[\"3. Register dispatch in state file\"]\n    D --\u003e E[\"4. Write .claw/manifest.json\"]\n    E --\u003e F[\"5. Notify: dispatched as tier\"]\n\n    F --\u003e W[\"6. Worker phase\u003cbr/\u003e\u003ci\u003efilesystem: full, linear_issues: NO\u003c/i\u003e\u003cbr/\u003eBuild prompt → implement → save to .claw/\"]\n    W --\u003e|\"plugin code — automatic\"| AU[\"7. Audit phase\u003cbr/\u003e\u003ci\u003efilesystem: read, linear_issues: prompt-constrained\u003c/i\u003e\u003cbr/\u003eVerify criteria → inspect diff → JSON verdict\"]\n\n    AU --\u003e V{\"8. Verdict\"}\n    V --\u003e|PASS| DONE[\"Done ✔\u003cbr/\u003eupdateIssue → notify\"]\n    V --\u003e|\"FAIL ≤ max\"| RW[\"Rework\u003cbr/\u003e\u003ci\u003eattempt++, inject audit gaps\u003c/i\u003e\"]\n    RW --\u003e W\n    V --\u003e|\"FAIL \u003e max\"| STUCK[\"Stuck 🚨\u003cbr/\u003eescalate + notify\"]\n```\n\n**State persistence:** Dispatch state is written to `~/.openclaw/linear-dispatch-state.json` with active dispatches, completed history, session mappings, and processed event IDs.\n\n**Watchdog:** A configurable inactivity timer (`inactivitySec`, default 120s) monitors agent output. If no tool calls or text output for the configured period, the agent process is killed and retried once. If the retry also times out, the dispatch is escalated.\n\n### Dispatch State Machine\n\nAll transitions use compare-and-swap (CAS) to prevent races. `dispatch-state.json` is the canonical source of truth.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e DISPATCHED\n    DISPATCHED --\u003e WORKING\n    WORKING --\u003e AUDITING\n    AUDITING --\u003e DONE\n    AUDITING --\u003e WORKING : FAIL (attempt++)\n    WORKING --\u003e STUCK : watchdog kill 2x\n    AUDITING --\u003e STUCK : attempt \u003e max\n```\n\n### `linear_issues` Tool → API Mapping\n\nThe `linear_issues` registered tool translates agent requests into `LinearAgentApi` method calls:\n\n| Tool Action | API Methods Called |\n|---|---|\n| `read` | `getIssueDetails(issueId)` |\n| `create` | `getIssueDetails(parentIssueId)` (if parent) → `getTeamLabels` (if labels) → `createIssue(input)` |\n| `update` | `getIssueDetails` → `getTeamStates` (if status) → `getTeamLabels` (if labels) → `updateIssueExtended` |\n| `comment` | `createComment(issueId, body)` |\n| `list_states` | `getTeamStates(teamId)` |\n| `list_labels` | `getTeamLabels(teamId)` |\n\nThe `update` action's key feature is **name-to-ID resolution**: agents say `status: \"In Progress\"` and the tool automatically resolves it to the correct `stateId` via `getTeamStates`. Same for labels — `labels: [\"bug\", \"urgent\"]` resolves to `labelIds` via `getTeamLabels`. Case-insensitive matching with descriptive errors when names don't match.\n\nThe `create` action supports **sub-issue creation** via `parentIssueId`. When provided, the new issue inherits `teamId` and `projectId` from the parent, and the `GraphQL-Features: sub_issues` header is sent automatically. Agents are instructed to decompose large tasks into sub-issues for granular planning and parallel dispatch.\n\n---\n\n## Testing \u0026 Verification\n\n### Health check\n\nRun the doctor to verify your setup. It checks auth, config, prompts, connectivity, and dispatch health — and tells you exactly how to fix anything that's wrong:\n\n```bash\nopenclaw openclaw-linear doctor\n```\n\nExample output:\n\n```\n┌──────────────────────────────────────────────┐\n│            Linear Plugin Doctor              │\n└──────────────────────────────────────────────┘\n\n  Authentication \u0026 Tokens\n  ✔ Access token found (source: profile)\n  ✔ Token not expired (23h remaining)\n  ✔ API reachable — logged in as Test (TestOrg)\n\n  Agent Configuration\n  ✔ agent-profiles.json loaded (2 agents)\n  ✔ Default agent: coder\n\n  Coding Tools\n  ✔ coding-tools.json loaded (default: codex)\n  ✔ codex: found at /usr/local/bin/codex\n\n  Files \u0026 Directories\n  ✔ Dispatch state: 1 active, 5 completed\n  ✔ Prompts valid (5/5 sections, 4/4 variables)\n\n  Connectivity\n  ✔ Linear API reachable\n  ✔ Webhook gateway responding\n\n  Dispatch Health\n  ✔ No stale dispatches\n  ⚠ 2 completed dispatches older than 7 days\n    → Run: openclaw openclaw-linear doctor --fix to clean up\n\n  Summary: 11 passed, 1 warning, 0 errors\n```\n\nEvery warning and error includes a `→` line telling you what to do. Run `doctor --fix` to auto-repair what it can.\n\n### Code-run health check\n\nFor deeper diagnostics on coding tool backends (Claude Code, Codex, Gemini CLI), run the dedicated code-run doctor. It checks binary installation, API key configuration, and actually invokes each backend to verify it can authenticate and respond:\n\n```bash\nopenclaw openclaw-linear code-run doctor\n```\n\nExample output:\n\n```\nCode Run: Claude Code (Anthropic)\n  ✓ Binary: 2.1.50 (/home/claw/.npm-global/bin/claude)\n  ✓ API key: configured (ANTHROPIC_API_KEY)\n  ✓ Live test: responded in 3.2s\n\nCode Run: Codex (OpenAI)\n  ✓ Binary: 0.101.0 (/home/claw/.npm-global/bin/codex)\n  ✓ API key: configured (OPENAI_API_KEY)\n  ✓ Live test: responded in 2.8s\n\nCode Run: Gemini CLI (Google)\n  ✓ Binary: 0.28.2 (/home/claw/.npm-global/bin/gemini)\n  ✓ API key: configured (GEMINI_API_KEY)\n  ✓ Live test: responded in 4.1s\n\nCode Run: Routing\n  ✓ Default backend: codex\n  ✓ Mal → codex (default)\n  ✓ Kaylee → codex (default)\n  ✓ Inara → claude (override)\n  ✓ Callable backends: 3/3\n```\n\nThis is separate from the main `doctor` because each live test spawns a real CLI subprocess (~5-10s per backend). Use `--json` for machine-readable output.\n\n### Unit tests\n\n1000+ tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, native issue tools, cross-model review, notifications, watchdog, and infrastructure:\n\n```bash\ncd ~/claw-extensions/linear\nnpm run typecheck              # Validate source against the OpenClaw plugin SDK\nnpx vitest run                   # Run all tests\nnpx vitest run --reporter=verbose  # See every test name\nnpx vitest run src/pipeline/     # Just pipeline tests\n```\n\n### UAT (live integration tests)\n\nThe UAT script runs against your real Linear workspace. It creates actual issues, triggers the pipeline, and verifies the results.\n\n```bash\n# Run all UAT scenarios\nnpx tsx scripts/uat-linear.ts\n\n# Run a specific scenario\nnpx tsx scripts/uat-linear.ts --test dispatch\nnpx tsx scripts/uat-linear.ts --test planning\nnpx tsx scripts/uat-linear.ts --test mention\nnpx tsx scripts/uat-linear.ts --test intent\n```\n\n**What each scenario does:**\n\n#### `--test dispatch` (Single issue, full pipeline)\n\n1. Creates a test issue in Linear\n2. Assigns it to the agent\n3. Waits for the dispatch comment (confirms the agent picked it up)\n4. Waits for the audit result (pass, fail, or escalation)\n5. Reports success/failure with timing\n\n**Expected output:**\n\n```\n[dispatch] Created issue ENG-200: \"UAT: simple config tweak\"\n[dispatch] Assigned to agent — waiting for dispatch comment...\n[dispatch] ✔ Dispatch confirmed (12s) — assessed as small\n[dispatch] Waiting for audit result...\n[dispatch] ✔ Audit passed (94s) — issue marked done\n[dispatch] Total: 106s\n```\n\n#### `--test planning` (Project planning flow)\n\n1. Creates a root issue in a test project\n2. Posts `plan this project` comment\n3. Waits for the planner's welcome message\n4. Posts feature requirements\n5. Waits for the planner to create issues\n6. Posts `finalize plan`\n7. Waits for plan approval or failure\n\n**Expected output:**\n\n```\n[planning] Created project \"UAT Planning Test\"\n[planning] Posted \"plan this project\" — waiting for welcome...\n[planning] ✔ Welcome received (8s)\n[planning] Posted feature description — waiting for response...\n[planning] ✔ Planner created 3 issues (15s)\n[planning] Posted \"finalize plan\" — waiting for audit...\n[planning] ✔ Plan approved (6s) — 3 issues queued for dispatch\n[planning] Total: 29s\n```\n\n#### `--test mention` (Agent routing)\n\n1. Creates a test issue\n2. Posts a comment mentioning a specific agent (e.g., `@kaylee`)\n3. Waits for that agent to respond\n4. Verifies the response came from the right agent\n\n**Expected output:**\n\n```\n[mention] Created issue ENG-201\n[mention] Posted \"@kaylee analyze this issue\"\n[mention] ✔ Kaylee responded (18s)\n[mention] Total: 18s\n```\n\n#### `--test intent` (Natural language routing)\n\n1. Creates a test issue and posts a question (no `@mention`)\n2. Verifies the bot responds (not silently dropped)\n3. Posts a comment with an agent name but no `@` prefix\n4. Verifies that agent responds\n5. Tests plan review flow with cross-model audit\n\n**Expected output:**\n\n```\n[intent] Created issue ENG-202\n[intent] Posted \"what can I do with this?\" — waiting for response...\n[intent] ✔ Bot responded to question (12s)\n[intent] Posted \"hey kaylee analyze this\" — waiting for response...\n[intent] ✔ Kaylee responded without @mention (15s)\n[intent] Total: 27s\n```\n\n### Verify notifications\n\n```bash\nopenclaw openclaw-linear notify test              # Send test to all targets\nopenclaw openclaw-linear notify test --channel discord  # Test one channel\nopenclaw openclaw-linear notify status             # Show what's configured\n```\n\n### Verify prompts\n\n```bash\nopenclaw openclaw-linear prompts validate   # Check for template errors\nopenclaw openclaw-linear prompts show       # View the active prompts\n```\n\n---\n\n## CLI Reference\n\n```bash\n# Setup \u0026 auth\nopenclaw openclaw-linear setup                     # Guided first-time setup (profiles, auth, webhook, doctor)\nopenclaw openclaw-linear auth                      # Run OAuth flow\nopenclaw openclaw-linear status                    # Check connection\n\n# Worktrees\nopenclaw openclaw-linear worktrees                 # List active worktrees\nopenclaw openclaw-linear worktrees --prune \u003cpath\u003e  # Remove a worktree\n\n# Multi-repo\nopenclaw openclaw-linear repos check               # Validate paths, preview labels\nopenclaw openclaw-linear repos sync                # Create missing repo: labels in Linear\n\n# Prompts\nopenclaw openclaw-linear prompts show              # View current prompts\nopenclaw openclaw-linear prompts path              # Show file path\nopenclaw openclaw-linear prompts validate          # Check for errors\n\n# Notifications\nopenclaw openclaw-linear notify status             # Show targets \u0026 events\nopenclaw openclaw-linear notify test               # Test all targets\nopenclaw openclaw-linear notify test --channel discord  # Test one channel\nopenclaw openclaw-linear notify setup              # Interactive setup\n\n# Webhooks\nopenclaw openclaw-linear webhooks status             # Show webhook config in Linear\nopenclaw openclaw-linear webhooks setup              # Auto-provision workspace webhook\nopenclaw openclaw-linear webhooks setup --dry-run    # Preview what would change\nopenclaw openclaw-linear webhooks setup --url \u003curl\u003e  # Use custom webhook URL\nopenclaw openclaw-linear webhooks delete \u003cid\u003e        # Delete a webhook by ID\n\n# Dispatch\n/dispatch list                                     # Active dispatches\n/dispatch status \u003cidentifier\u003e                      # Dispatch details\n/dispatch retry \u003cidentifier\u003e                       # Re-run stuck dispatch\n/dispatch escalate \u003cidentifier\u003e [reason]           # Force to stuck\n\n# Health\nopenclaw openclaw-linear doctor                    # Run health checks\nopenclaw openclaw-linear doctor --fix              # Auto-fix issues\nopenclaw openclaw-linear doctor --json             # JSON output\n\n# Code-run backends\nopenclaw openclaw-linear code-run doctor           # Deep check all backends (binary, API key, live test)\nopenclaw openclaw-linear code-run doctor --json    # JSON output\n```\n\n---\n\n## Troubleshooting\n\nQuick checks:\n\n```bash\nsystemctl --user status openclaw-gateway        # Is the gateway running?\nopenclaw openclaw-linear status                  # Is the token valid?\njournalctl --user -u openclaw-gateway -f         # Watch live logs\n```\n\n### Common Issues\n\n| Problem | Fix |\n|---|---|\n| Agent goes silent | Watchdog auto-kills after `inactivitySec` and retries. Check logs for `Watchdog KILL`. |\n| Dispatch stuck after watchdog | Both retries failed. Check `.claw/log.jsonl`. Re-assign issue to restart. |\n| `cli_*` uses wrong backend | Check `coding-tools.json` — per-agent override \u003e global default. Run `code-run doctor` to see routing. |\n| `cli_*` fails at runtime | Run `openclaw openclaw-linear code-run doctor` — checks binary, API key, and live callability for each backend. |\n| Webhook events not arriving | Run `openclaw openclaw-linear webhooks setup` to auto-provision. Both webhooks must point to `/linear/webhook`. Check tunnel is running. |\n| Tunnel down / webhooks silently failing | `systemctl status cloudflared` (or `systemctl --user status cloudflared`). Restart with `systemctl restart cloudflared`. Test: `curl -s -X POST https://your-domain.com/linear/webhook -H 'Content-Type: application/json' -d '{\"type\":\"test\"}'` — should return `\"ok\"`. |\n| OAuth token expired | Auto-refreshes. If stuck, re-run `openclaw openclaw-linear auth` and restart. |\n| Audit always fails | Run `openclaw openclaw-linear prompts validate` to check prompt syntax. |\n| Multi-repo not detected | Markers must be `\u003c!-- repos: name1, name2 --\u003e`. Names must match `repos` config keys. |\n| `/dispatch` not responding | Restart gateway. Check plugin loaded with `openclaw doctor`. |\n| Comments ignored (no response) | Check logs for intent classification results. If classifier fails, regex fallback may not match. |\n| Intent classifier slow | Set `classifierAgentId` to a small model agent (Haiku). Default uses your primary model. |\n| Cross-model review fails | The reviewer model CLI must be installed. Check logs for \"cross-model review unavailable\". |\n| Rich notifications are plain text | Set `\"richFormat\": true` in notifications config. |\n| Gateway rejects config keys | Strict validator. Run `openclaw doctor --fix`. |\n\nFor detailed diagnostics, see [docs/troubleshooting.md](docs/troubleshooting.md).\n\n---\n\n## Further Reading\n\n- [Architecture](docs/architecture.md) — Internal design, state machines, diagrams\n- [Troubleshooting](docs/troubleshooting.md) — Diagnostic commands, curl examples, log analysis\n- [Agents in Linear](https://linear.app/docs/agents-in-linear) — Linear's agent guidance system (workspace \u0026 team-level instructions)\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalltelemetry%2Fopenclaw-linear-plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcalltelemetry%2Fopenclaw-linear-plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalltelemetry%2Fopenclaw-linear-plugin/lists"}