@calltelemetry/openclaw-linear 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @calltelemetry/openclaw-linear
2
2
 
3
+ [![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)
4
+ [![codecov](https://codecov.io/gh/calltelemetry/openclaw-linear-plugin/graph/badge.svg)](https://codecov.io/gh/calltelemetry/openclaw-linear-plugin)
5
+ [![npm](https://img.shields.io/npm/v/@calltelemetry/openclaw-linear)](https://www.npmjs.com/package/@calltelemetry/openclaw-linear)
3
6
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-v2026.2+-blue)](https://github.com/calltelemetry/openclaw)
4
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
8
 
@@ -7,6 +10,55 @@ Connect Linear to AI agents. Issues get triaged, implemented, and audited — au
7
10
 
8
11
  ---
9
12
 
13
+ ## Why This Exists
14
+
15
+ Linear 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.
16
+
17
+ This plugin makes the full lifecycle hands-off:
18
+
19
+ ```mermaid
20
+ sequenceDiagram
21
+ actor You
22
+ participant Linear
23
+ participant Plugin
24
+ participant Worker as Worker Agent
25
+ participant Auditor as Auditor Agent
26
+
27
+ You->>Linear: Create issue
28
+ Note over Plugin: auto-triage
29
+ Linear-->>You: Estimate, labels, priority
30
+
31
+ You->>Linear: Assign to agent
32
+ Plugin->>Worker: dispatch (isolated worktree)
33
+ Worker-->>Plugin: implementation done
34
+ Plugin->>Auditor: audit (automatic, hard-enforced)
35
+ alt Pass
36
+ Auditor-->>Plugin: ✅ verdict
37
+ Plugin-->>Linear: Done
38
+ else Fail (retries left)
39
+ Auditor-->>Plugin: ❌ gaps
40
+ Plugin->>Worker: rework (gaps injected)
41
+ else Fail (no retries)
42
+ Auditor-->>Plugin: ❌ stuck
43
+ Plugin-->>You: 🚨 needs your help
44
+ end
45
+ ```
46
+
47
+ **What Linear can't do on its own — and what this plugin handles:**
48
+
49
+ | Problem | What the plugin does |
50
+ |---|---|
51
+ | **No agent orchestration** | Assigns complexity tiers, picks the right model, creates isolated worktrees, runs workers, triggers audits, processes verdicts — all from a single issue assignment |
52
+ | **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. |
53
+ | **No failure recovery** | Watchdog kills hung agents after configurable silence. Retries once automatically. Feeds audit failures back as context for rework. Escalates when retries are exhausted. |
54
+ | **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. |
55
+ | **No webhook deduplication** | Linear sends events from two separate webhook systems that can overlap. The plugin deduplicates across session IDs, comment IDs, and assignment events with a 60s sliding window. |
56
+ | **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. |
57
+
58
+ The 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.
59
+
60
+ ---
61
+
10
62
  ## What It Does
11
63
 
12
64
  - **New issue?** Agent estimates story points, adds labels, sets priority.
@@ -59,45 +111,70 @@ flowchart TB
59
111
 
60
112
  **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.
61
113
 
62
- #### Setup
114
+ #### Install cloudflared
63
115
 
64
116
  ```bash
65
- # Install cloudflared
66
- # RHEL/AlmaLinux:
67
- sudo dnf install cloudflared
68
- # Or download: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
117
+ # RHEL / Rocky / Alma
118
+ sudo dnf install -y cloudflared
119
+
120
+ # Debian / Ubuntu
121
+ curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
122
+ echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
123
+ | sudo tee /etc/apt/sources.list.d/cloudflared.list
124
+ sudo apt update && sudo apt install -y cloudflared
125
+
126
+ # macOS
127
+ brew install cloudflare/cloudflare/cloudflared
128
+ ```
129
+
130
+ #### Authenticate with Cloudflare
69
131
 
70
- # Authenticate (opens browser, saves cert to ~/.cloudflared/)
132
+ ```bash
71
133
  cloudflared tunnel login
134
+ ```
135
+
136
+ This opens your browser. You must:
137
+ 1. Log in to your Cloudflare account
138
+ 2. **Select the domain** (zone) for the tunnel (e.g., `yourdomain.com`)
139
+ 3. Click **Authorize**
140
+
141
+ Cloudflare writes an origin certificate to `~/.cloudflared/cert.pem`. This cert grants `cloudflared` permission to create tunnels and DNS records under that domain.
142
+
143
+ > **Prerequisite:** Your domain must already be on Cloudflare (nameservers pointed to Cloudflare). If it's not, add it in the Cloudflare dashboard first.
72
144
 
73
- # Create a named tunnel
145
+ #### Create a tunnel
146
+
147
+ ```bash
74
148
  cloudflared tunnel create openclaw-linear
75
- # Note the tunnel UUID from the output (e.g., da1f21bf-856e-49ea-83c2-d210092d96be)
76
149
  ```
77
150
 
151
+ This outputs a **Tunnel ID** (UUID like `da1f21bf-856e-...`) and writes credentials to `~/.cloudflared/<TUNNEL_ID>.json`.
152
+
153
+ #### DNS — point your hostname to the tunnel
154
+
155
+ ```bash
156
+ cloudflared tunnel route dns openclaw-linear linear.yourdomain.com
157
+ ```
158
+
159
+ This creates a CNAME record in Cloudflare DNS: `linear.yourdomain.com → <TUNNEL_ID>.cfargotunnel.com`. You can verify it in the Cloudflare dashboard under **DNS > Records**. You can also create this record manually.
160
+
161
+ The 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.
162
+
78
163
  #### Configure the tunnel
79
164
 
80
165
  Create `/etc/cloudflared/config.yml` (system-wide) or `~/.cloudflared/config.yml` (user):
81
166
 
82
167
  ```yaml
83
- tunnel: <your-tunnel-uuid>
84
- credentials-file: /home/<user>/.cloudflared/<your-tunnel-uuid>.json
168
+ tunnel: <TUNNEL_ID>
169
+ credentials-file: /home/<user>/.cloudflared/<TUNNEL_ID>.json
85
170
 
86
171
  ingress:
87
- - hostname: your-domain.com
172
+ - hostname: linear.yourdomain.com
88
173
  service: http://localhost:18789
89
174
  - service: http_status:404 # catch-all, reject unmatched requests
90
175
  ```
91
176
 
92
- #### DNS
93
-
94
- Point your hostname to the tunnel:
95
-
96
- ```bash
97
- cloudflared tunnel route dns <your-tunnel-uuid> your-domain.com
98
- ```
99
-
100
- This creates a CNAME record in Cloudflare DNS. You can also do this manually in the Cloudflare dashboard.
177
+ The `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.
101
178
 
102
179
  #### Run as a service
103
180
 
@@ -105,9 +182,18 @@ This creates a CNAME record in Cloudflare DNS. You can also do this manually in
105
182
  # Install as system service (recommended for production)
106
183
  sudo cloudflared service install
107
184
  sudo systemctl enable --now cloudflared
185
+ ```
108
186
 
109
- # Verify
110
- curl -s https://your-domain.com/linear/webhook \
187
+ To test without installing as a service:
188
+
189
+ ```bash
190
+ cloudflared tunnel run openclaw-linear
191
+ ```
192
+
193
+ #### Verify end-to-end
194
+
195
+ ```bash
196
+ curl -s https://linear.yourdomain.com/linear/webhook \
111
197
  -X POST -H "Content-Type: application/json" \
112
198
  -d '{"type":"test","action":"ping"}'
113
199
  # Should return: "ok"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -457,7 +457,7 @@ describe("buildSummary", () => {
457
457
  // checkCodeRunDeep
458
458
  // ---------------------------------------------------------------------------
459
459
 
460
- describe("checkCodeRunDeep", () => {
460
+ describe.skipIf(process.env.CI)("checkCodeRunDeep", () => {
461
461
  // Run a single invocation and share results across assertions to avoid
462
462
  // repeated 30s live CLI calls (the live test spawns all 3 backends).
463
463
  let sections: Awaited<ReturnType<typeof checkCodeRunDeep>>;
@@ -378,7 +378,7 @@ describe("webhook deduplication", () => {
378
378
  expect(classifyIntent).not.toHaveBeenCalled();
379
379
  });
380
380
 
381
- it("skips duplicate AgentSessionEvent.prompted by webhookId", async () => {
381
+ it.skipIf(process.env.CI)("skips duplicate AgentSessionEvent.prompted by webhookId", async () => {
382
382
  const payload = {
383
383
  type: "AgentSessionEvent",
384
384
  action: "prompted",