@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 +108 -22
- package/package.json +1 -1
- package/src/infra/doctor.test.ts +1 -1
- package/src/pipeline/webhook-dedup.test.ts +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @calltelemetry/openclaw-linear
|
|
2
2
|
|
|
3
|
+
[](https://github.com/calltelemetry/openclaw-linear-plugin/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/calltelemetry/openclaw-linear-plugin)
|
|
5
|
+
[](https://www.npmjs.com/package/@calltelemetry/openclaw-linear)
|
|
3
6
|
[](https://github.com/calltelemetry/openclaw)
|
|
4
7
|
[](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
|
-
####
|
|
114
|
+
#### Install cloudflared
|
|
63
115
|
|
|
64
116
|
```bash
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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: <
|
|
84
|
-
credentials-file: /home/<user>/.cloudflared/<
|
|
168
|
+
tunnel: <TUNNEL_ID>
|
|
169
|
+
credentials-file: /home/<user>/.cloudflared/<TUNNEL_ID>.json
|
|
85
170
|
|
|
86
171
|
ingress:
|
|
87
|
-
- hostname:
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
package/src/infra/doctor.test.ts
CHANGED
|
@@ -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",
|