linear-agent-bridge 0.1.2

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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +613 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +19 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/agent/context-builder.d.ts +9 -0
  8. package/dist/src/agent/context-builder.d.ts.map +1 -0
  9. package/dist/src/agent/context-builder.js +121 -0
  10. package/dist/src/agent/context-builder.js.map +1 -0
  11. package/dist/src/agent/plan-manager.d.ts +5 -0
  12. package/dist/src/agent/plan-manager.d.ts.map +1 -0
  13. package/dist/src/agent/plan-manager.js +11 -0
  14. package/dist/src/agent/plan-manager.js.map +1 -0
  15. package/dist/src/agent/response-tracker.d.ts +4 -0
  16. package/dist/src/agent/response-tracker.d.ts.map +1 -0
  17. package/dist/src/agent/response-tracker.js +11 -0
  18. package/dist/src/agent/response-tracker.js.map +1 -0
  19. package/dist/src/agent/session-token.d.ts +5 -0
  20. package/dist/src/agent/session-token.d.ts.map +1 -0
  21. package/dist/src/agent/session-token.js +15 -0
  22. package/dist/src/agent/session-token.js.map +1 -0
  23. package/dist/src/api/activity-ops.d.ts +2 -0
  24. package/dist/src/api/activity-ops.d.ts.map +1 -0
  25. package/dist/src/api/activity-ops.js +71 -0
  26. package/dist/src/api/activity-ops.js.map +1 -0
  27. package/dist/src/api/base-url.d.ts +3 -0
  28. package/dist/src/api/base-url.d.ts.map +1 -0
  29. package/dist/src/api/base-url.js +11 -0
  30. package/dist/src/api/base-url.js.map +1 -0
  31. package/dist/src/api/delegation-ops.d.ts +2 -0
  32. package/dist/src/api/delegation-ops.d.ts.map +1 -0
  33. package/dist/src/api/delegation-ops.js +51 -0
  34. package/dist/src/api/delegation-ops.js.map +1 -0
  35. package/dist/src/api/issue-ops.d.ts +2 -0
  36. package/dist/src/api/issue-ops.d.ts.map +1 -0
  37. package/dist/src/api/issue-ops.js +170 -0
  38. package/dist/src/api/issue-ops.js.map +1 -0
  39. package/dist/src/api/query-ops.d.ts +2 -0
  40. package/dist/src/api/query-ops.d.ts.map +1 -0
  41. package/dist/src/api/query-ops.js +77 -0
  42. package/dist/src/api/query-ops.js.map +1 -0
  43. package/dist/src/api/router.d.ts +12 -0
  44. package/dist/src/api/router.d.ts.map +1 -0
  45. package/dist/src/api/router.js +62 -0
  46. package/dist/src/api/router.js.map +1 -0
  47. package/dist/src/api/session-ops.d.ts +2 -0
  48. package/dist/src/api/session-ops.d.ts.map +1 -0
  49. package/dist/src/api/session-ops.js +96 -0
  50. package/dist/src/api/session-ops.js.map +1 -0
  51. package/dist/src/config.d.ts +3 -0
  52. package/dist/src/config.d.ts.map +1 -0
  53. package/dist/src/config.js +47 -0
  54. package/dist/src/config.js.map +1 -0
  55. package/dist/src/graphql/mutations.d.ts +9 -0
  56. package/dist/src/graphql/mutations.d.ts.map +1 -0
  57. package/dist/src/graphql/mutations.js +74 -0
  58. package/dist/src/graphql/mutations.js.map +1 -0
  59. package/dist/src/graphql/queries.d.ts +10 -0
  60. package/dist/src/graphql/queries.d.ts.map +1 -0
  61. package/dist/src/graphql/queries.js +121 -0
  62. package/dist/src/graphql/queries.js.map +1 -0
  63. package/dist/src/linear-client.d.ts +7 -0
  64. package/dist/src/linear-client.d.ts.map +1 -0
  65. package/dist/src/linear-client.js +72 -0
  66. package/dist/src/linear-client.js.map +1 -0
  67. package/dist/src/types.d.ts +85 -0
  68. package/dist/src/types.d.ts.map +1 -0
  69. package/dist/src/types.js +2 -0
  70. package/dist/src/types.js.map +1 -0
  71. package/dist/src/util.d.ts +13 -0
  72. package/dist/src/util.d.ts.map +1 -0
  73. package/dist/src/util.js +80 -0
  74. package/dist/src/util.js.map +1 -0
  75. package/dist/src/webhook/close-intent.d.ts +4 -0
  76. package/dist/src/webhook/close-intent.d.ts.map +1 -0
  77. package/dist/src/webhook/close-intent.js +37 -0
  78. package/dist/src/webhook/close-intent.js.map +1 -0
  79. package/dist/src/webhook/handler.d.ts +5 -0
  80. package/dist/src/webhook/handler.d.ts.map +1 -0
  81. package/dist/src/webhook/handler.js +441 -0
  82. package/dist/src/webhook/handler.js.map +1 -0
  83. package/dist/src/webhook/issue-policy.d.ts +7 -0
  84. package/dist/src/webhook/issue-policy.d.ts.map +1 -0
  85. package/dist/src/webhook/issue-policy.js +139 -0
  86. package/dist/src/webhook/issue-policy.js.map +1 -0
  87. package/dist/src/webhook/message-builder.d.ts +34 -0
  88. package/dist/src/webhook/message-builder.d.ts.map +1 -0
  89. package/dist/src/webhook/message-builder.js +144 -0
  90. package/dist/src/webhook/message-builder.js.map +1 -0
  91. package/dist/src/webhook/response-parser.d.ts +2 -0
  92. package/dist/src/webhook/response-parser.d.ts.map +1 -0
  93. package/dist/src/webhook/response-parser.js +51 -0
  94. package/dist/src/webhook/response-parser.js.map +1 -0
  95. package/dist/src/webhook/session-resolver.d.ts +6 -0
  96. package/dist/src/webhook/session-resolver.d.ts.map +1 -0
  97. package/dist/src/webhook/session-resolver.js +203 -0
  98. package/dist/src/webhook/session-resolver.js.map +1 -0
  99. package/dist/src/webhook/skip-filter.d.ts +4 -0
  100. package/dist/src/webhook/skip-filter.d.ts.map +1 -0
  101. package/dist/src/webhook/skip-filter.js +42 -0
  102. package/dist/src/webhook/skip-filter.js.map +1 -0
  103. package/dist/src/webhook/validation.d.ts +2 -0
  104. package/dist/src/webhook/validation.d.ts.map +1 -0
  105. package/dist/src/webhook/validation.js +11 -0
  106. package/dist/src/webhook/validation.js.map +1 -0
  107. package/openclaw.plugin.json +92 -0
  108. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tokezooo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,613 @@
1
+ # linear-agent-bridge
2
+
3
+ An [OpenClaw](https://github.com/nicepkg/openclaw) plugin that turns Linear's Agent Sessions into fully autonomous AI agent runs. When someone @mentions or delegates an issue to your agent in Linear, this plugin receives the webhook, spins up an OpenClaw agent, and gives it a rich set of tools to manage issues, communicate progress, delegate work, and close tasks — all without leaving Linear.
4
+
5
+ ## Table of Contents
6
+
7
+ - [How It Works](#how-it-works)
8
+ - [Features](#features)
9
+ - [Prerequisites](#prerequisites)
10
+ - [Installation](#installation)
11
+ - [Linear App Setup](#linear-app-setup)
12
+ - [Plugin Configuration](#plugin-configuration)
13
+ - [Webhook Setup](#webhook-setup)
14
+ - [Use Cases](#use-cases)
15
+ - [Agent API Reference](#agent-api-reference)
16
+ - [Issue Management](#issue-management)
17
+ - [Communication (Activities)](#communication-activities)
18
+ - [Session Management](#session-management)
19
+ - [Delegation](#delegation)
20
+ - [Queries](#queries)
21
+ - [Architecture](#architecture)
22
+ - [Development](#development)
23
+ - [Troubleshooting](#troubleshooting)
24
+ - [License](#license)
25
+
26
+ ## How It Works
27
+
28
+ ```
29
+ ┌──────────┐
30
+ │ Linear │
31
+ │ Workspace│
32
+ └────┬─────┘
33
+ │ Webhook (AgentSession / Comment)
34
+
35
+ ┌────────────────┐
36
+ │ This Plugin │
37
+ │ │
38
+ │ 1. Verify HMAC │
39
+ │ 2. Resolve │
40
+ │ session ID │
41
+ │ 3. Build │
42
+ │ enriched │
43
+ │ prompt │
44
+ │ 4. Issue │
45
+ │ per-session │
46
+ │ API token │
47
+ └───────┬────────┘
48
+ │ callGateway({ method: "agent", ... })
49
+
50
+ ┌────────────────┐
51
+ │ OpenClaw Agent │
52
+ │ │
53
+ │ Reads issue, │◄──── POST /plugins/linear/api
54
+ │ writes code, │ (bearer token auth)
55
+ │ posts updates │────► Linear GraphQL API
56
+ │ to Linear │
57
+ └────────────────┘
58
+ ```
59
+
60
+ 1. A user @mentions the agent or delegates an issue in Linear
61
+ 2. Linear sends a webhook to this plugin
62
+ 3. The plugin verifies the HMAC signature, resolves the agent session, and builds an enriched prompt containing the issue context and a full API reference
63
+ 4. An OpenClaw agent is launched with that prompt and a short-lived bearer token
64
+ 5. During execution, the agent calls back to the plugin's API proxy to post thoughts, update plans, create sub-issues, delegate, query data, and post final responses
65
+ 6. When the agent finishes, the token is revoked and the response is posted to Linear
66
+
67
+ ## Features
68
+
69
+ - **Full Linear Agent Protocol** — implements `created`, `prompted`, `stop` signal, agent plans, activities (thought/action/elicitation/response/error), proactive sessions
70
+ - **Rich Agent API** — during execution the agent can manage issues, post activities, update session plans, delegate work, query issue/team details, and more
71
+ - **Session Deduplication** — prevents duplicate agent runs when Linear sends both AgentSession and Comment webhooks for the same event
72
+ - **Close Intent Detection** — recognizes natural-language close commands in English and Russian ("close this task", "закрой задачу") and fast-paths them without a full agent run
73
+ - **Per-Session Security** — each agent run gets a unique cryptographic bearer token scoped to its session; revoked on completion
74
+ - **Issue Policies** — automatically moves issues to "started" state and delegates to the app user on session creation
75
+ - **Multi-Repo Routing** — maps Linear teams and projects to specific repository directories
76
+ - **Elicitation with Select** — the agent can present clickable option lists to users via the `select` signal
77
+ - **External URL Linking** — attaches external links (e.g. CI dashboard, PR) to the Linear session
78
+ - **Auto-Detection of Base URL** — works behind Tailscale or any reverse proxy; captures the public URL from the first webhook `Host` header
79
+
80
+ ## Prerequisites
81
+
82
+ - **Node.js** >= 18
83
+ - A running **OpenClaw gateway** instance
84
+ - A **Linear workspace** with admin access (to install an OAuth application)
85
+ - A publicly reachable URL for webhooks (Tailscale, ngrok, cloud deploy, etc.)
86
+
87
+ ## Installation
88
+
89
+ ```bash
90
+ # From npm (when published)
91
+ npm install linear-agent-bridge
92
+
93
+ # Or clone and build from source
94
+ git clone https://github.com/tokezooo/linear-agent-bridge.git
95
+ cd linear-agent-bridge
96
+ npm install
97
+ npm run build
98
+ ```
99
+
100
+ The plugin registers itself with OpenClaw via the `openclaw` field in `package.json`:
101
+
102
+ ```json
103
+ {
104
+ "openclaw": {
105
+ "extensions": ["./dist/index.js"]
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Linear App Setup
111
+
112
+ ### 1. Create a Linear Application
113
+
114
+ 1. Go to **Linear Settings** > **API** > **Applications** > [Create new](https://linear.app/settings/api/applications/new)
115
+ 2. Set a recognizable name (this is how users will see the agent in mentions and filters)
116
+ 3. Enable **Webhooks**
117
+ 4. Under webhook events, select **Agent session events**
118
+ 5. Set the webhook URL to: `https://<your-host>/plugins/linear/linear`
119
+
120
+ ### 2. OAuth Installation
121
+
122
+ Install the app into your workspace using the OAuth flow with `actor=app`:
123
+
124
+ ```
125
+ https://linear.app/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT&response_type=code&scope=read,write,issues:create,comments:create,app:assignable,app:mentionable&actor=app
126
+ ```
127
+
128
+ Key scopes:
129
+ | Scope | Purpose |
130
+ |-------|---------|
131
+ | `read`, `write` | Core issue/comment access |
132
+ | `issues:create` | Create issues and sub-issues |
133
+ | `comments:create` | Post comments |
134
+ | `app:assignable` | Allow delegation to the agent |
135
+ | `app:mentionable` | Allow @mentioning the agent |
136
+
137
+ ### 3. Get the Webhook Signing Secret
138
+
139
+ In your Linear application settings, copy the **Webhook signing secret**. This is used for HMAC-SHA256 signature verification of incoming webhooks.
140
+
141
+ ## Plugin Configuration
142
+
143
+ Configure the plugin in your OpenClaw config under the plugin's section. All options are defined in `openclaw.plugin.json`.
144
+
145
+ ### Required
146
+
147
+ | Option | Type | Description |
148
+ |--------|------|-------------|
149
+ | `linearApiKey` | `string` | Linear OAuth token (from the OAuth flow above) |
150
+ | `linearWebhookSecret` | `string` | Webhook signing secret for HMAC verification |
151
+
152
+ ### Recommended
153
+
154
+ | Option | Type | Default | Description |
155
+ |--------|------|---------|-------------|
156
+ | `devAgentId` | `string` | `"dev"` | OpenClaw agent ID to handle Linear issues |
157
+ | `defaultDir` | `string` | — | Default repository directory for agent work |
158
+
159
+ ### Issue Policies
160
+
161
+ | Option | Type | Default | Description |
162
+ |--------|------|---------|-------------|
163
+ | `delegateOnCreate` | `boolean` | `true` | Auto-delegate issues to the app user when a session is created |
164
+ | `startOnCreate` | `boolean` | `true` | Move issues to "started" workflow state on session creation |
165
+
166
+ ### Multi-Repo Routing
167
+
168
+ | Option | Type | Description |
169
+ |--------|------|-------------|
170
+ | `repoByTeam` | `object` | Map Linear team keys to repository directories. Example: `{ "ENG": "/home/code/backend", "WEB": "/home/code/frontend" }` |
171
+ | `repoByProject` | `object` | Map Linear project keys to repository directories. Takes precedence over `repoByTeam` |
172
+
173
+ ### Agent API
174
+
175
+ | Option | Type | Default | Description |
176
+ |--------|------|---------|-------------|
177
+ | `enableAgentApi` | `boolean` | `true` | Enable the API proxy that agents call during execution |
178
+ | `apiBaseUrl` | `string` | auto-detected | Override the auto-detected base URL for agent API callbacks |
179
+
180
+ ### External URLs
181
+
182
+ | Option | Type | Description |
183
+ |--------|------|-------------|
184
+ | `externalUrlBase` | `string` | URL template for session links. Supports `{session}` and `{issue}` placeholders. Example: `https://dash.example.com/sessions/{session}` |
185
+ | `externalUrlLabel` | `string` | Label for external links (default: `"OpenClaw session"`) |
186
+
187
+ ### Notifications
188
+
189
+ | Option | Type | Description |
190
+ |--------|------|-------------|
191
+ | `notifyChannel` | `string` | Channel for delivery notifications (e.g. `"discord"`) |
192
+ | `notifyTo` | `string` | Target for notifications (e.g. `"channel:123456"`) |
193
+ | `notifyAccountId` | `string` | Account ID for notifications |
194
+
195
+ ### Example Configuration
196
+
197
+ ```json
198
+ {
199
+ "linearApiKey": "lin_oauth_...",
200
+ "linearWebhookSecret": "whsec_...",
201
+ "devAgentId": "dev",
202
+ "defaultDir": "/home/projects/main-repo",
203
+ "repoByTeam": {
204
+ "ENG": "/home/projects/backend",
205
+ "WEB": "/home/projects/frontend"
206
+ },
207
+ "delegateOnCreate": true,
208
+ "startOnCreate": true,
209
+ "enableAgentApi": true,
210
+ "externalUrlBase": "https://dash.example.com/sessions/{session}"
211
+ }
212
+ ```
213
+
214
+ ## Webhook Setup
215
+
216
+ The plugin registers a POST endpoint at `/plugins/linear/linear`.
217
+
218
+ ### Security
219
+
220
+ - **HMAC-SHA256 Signature Verification** — every incoming webhook is verified against the `linearWebhookSecret` using the `linear-signature` header
221
+ - **Stale Webhook Rejection** — webhooks older than 60 seconds are rejected
222
+ - **Immediate 202 Response** — webhook processing happens asynchronously after responding to Linear
223
+
224
+ ### What Gets Processed
225
+
226
+ | Event Type | Action | Result |
227
+ |------------|--------|--------|
228
+ | AgentSession `created` | New session | Full agent run with enriched prompt |
229
+ | AgentSession `prompted` | Follow-up message | Agent continues with new context |
230
+ | Comment (on agent thread) | Follow-up | Resolved to session, triggers `prompted` |
231
+ | Signal `stop` | Halt | Agent posts stop confirmation, no run |
232
+ | Close intent ("close task") | Fast-path | Issue closed directly, no agent run |
233
+
234
+ ### What Gets Filtered
235
+
236
+ - `PermissionChange` and `OAuthApp` events (logged only)
237
+ - `AppUserNotification` events
238
+ - Self-authored comments (prevents feedback loops)
239
+ - System echo messages (e.g. "Starting work on...", "Agent run failed:")
240
+ - Empty prompts
241
+ - Duplicate events within the dedup window (5 seconds)
242
+
243
+ ## Use Cases
244
+
245
+ ### 1. Autonomous Issue Resolution
246
+
247
+ Delegate an issue to the agent in Linear. The agent receives the full issue context (title, description, labels, comments), writes code, creates sub-issues for subtasks, posts progress updates, and closes the issue when done.
248
+
249
+ ### 2. Interactive Code Review
250
+
251
+ @mention the agent in a comment asking for review. The agent reads the issue context, examines the linked code, and posts structured feedback as a response activity.
252
+
253
+ ### 3. Multi-Agent Delegation
254
+
255
+ The agent can delegate sub-tasks to other agents or reassign issues to human team members:
256
+
257
+ ```json
258
+ { "action": "delegate/assign", "issueId": "...", "delegateId": "other-agent-id" }
259
+ ```
260
+
261
+ ### 4. Issue Triage and Breakdown
262
+
263
+ Ask the agent to break down a large issue. It creates sub-issues with appropriate priorities and links them to the parent:
264
+
265
+ ```json
266
+ { "action": "issue/create-sub-issue", "title": "Implement auth middleware", "priority": 1 }
267
+ ```
268
+
269
+ ### 5. Progress Tracking
270
+
271
+ The agent shows real-time progress via session plans — structured checklists visible in the Linear UI:
272
+
273
+ ```json
274
+ {
275
+ "action": "session/plan",
276
+ "plan": [
277
+ { "content": "Analyze issue requirements", "status": "completed" },
278
+ { "content": "Implement solution", "status": "inProgress" },
279
+ { "content": "Write tests", "status": "pending" },
280
+ { "content": "Post summary", "status": "pending" }
281
+ ]
282
+ }
283
+ ```
284
+
285
+ ### 6. User Elicitation
286
+
287
+ The agent can ask the user to choose between options using the `select` signal:
288
+
289
+ ```json
290
+ {
291
+ "action": "activity/elicitation",
292
+ "body": "Which approach should I take?",
293
+ "signal": "select",
294
+ "signalMeta": {
295
+ "options": [
296
+ { "value": "Refactor the existing module" },
297
+ { "value": "Write a new implementation from scratch" }
298
+ ]
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### 7. Proactive Sessions
304
+
305
+ The agent can create new sessions on other issues or comments without being explicitly delegated:
306
+
307
+ ```json
308
+ { "action": "session/create-on-issue", "issueId": "issue-uuid" }
309
+ ```
310
+
311
+ ## Agent API Reference
312
+
313
+ During execution, the agent makes HTTP POST requests to a single endpoint. All requests use bearer token authentication and a JSON body with an `action` field.
314
+
315
+ **Endpoint:** `POST <apiBaseUrl>`
316
+ **Auth:** `Authorization: Bearer <per-session-token>`
317
+ **Content-Type:** `application/json`
318
+
319
+ Every request body **must** include an `action` field.
320
+
321
+ ### Issue Management
322
+
323
+ #### `issue/create` — Create a New Issue
324
+
325
+ | Field | Type | Required | Description |
326
+ |-------|------|----------|-------------|
327
+ | `teamId` | `string` | No (defaults to current) | Team to create issue in |
328
+ | `title` | `string` | **Yes** | Issue title |
329
+ | `description` | `string` | No | Issue description (Markdown) |
330
+ | `priority` | `number` | No | Priority 0-4 (0 = no priority, 1 = urgent, 4 = low) |
331
+ | `labelIds` | `string[]` | No | Label IDs to attach |
332
+ | `assigneeId` | `string` | No | Assignee user ID |
333
+ | `parentId` | `string` | No | Parent issue ID (creates sub-issue) |
334
+ | `stateId` | `string` | No | Initial workflow state |
335
+
336
+ #### `issue/update` — Update Issue Fields
337
+
338
+ | Field | Type | Required | Description |
339
+ |-------|------|----------|-------------|
340
+ | `issueId` | `string` | No (defaults to current) | Issue to update |
341
+ | `title` | `string` | No | New title |
342
+ | `description` | `string` | No | New description |
343
+ | `stateId` | `string` | No | New workflow state |
344
+ | `priority` | `number` | No | New priority |
345
+ | `labelIds` | `string[]` | No | Replace labels |
346
+ | `assigneeId` | `string` | No | New assignee |
347
+ | `delegateId` | `string` | No | New delegate |
348
+
349
+ #### `issue/close` — Close an Issue
350
+
351
+ | Field | Type | Required | Description |
352
+ |-------|------|----------|-------------|
353
+ | `issueId` | `string` | No (defaults to current) | Issue to close |
354
+
355
+ Resolves the team's "completed" workflow state and transitions the issue. Returns `alreadyClosed: true` if the issue is already completed or canceled.
356
+
357
+ #### `issue/create-sub-issue` — Create a Child Issue
358
+
359
+ | Field | Type | Required | Description |
360
+ |-------|------|----------|-------------|
361
+ | `title` | `string` | **Yes** | Sub-issue title |
362
+ | `parentId` | `string` | No (defaults to current) | Parent issue |
363
+ | `description` | `string` | No | Description |
364
+ | `priority` | `number` | No | Priority 0-4 |
365
+ | `labelIds` | `string[]` | No | Labels |
366
+ | `assigneeId` | `string` | No | Assignee |
367
+
368
+ #### `issue/link` — Link Two Issues
369
+
370
+ | Field | Type | Required | Description |
371
+ |-------|------|----------|-------------|
372
+ | `issueId` | `string` | No (defaults to current) | Source issue |
373
+ | `relatedIssueId` | `string` | **Yes** | Target issue |
374
+ | `type` | `string` | **Yes** | One of: `blocks`, `blocked_by`, `related`, `duplicate` |
375
+
376
+ ### Communication (Activities)
377
+
378
+ Activities are how the agent communicates with users in the Linear session UI.
379
+
380
+ #### `activity/thought` — Share Reasoning
381
+
382
+ | Field | Type | Required | Description |
383
+ |-------|------|----------|-------------|
384
+ | `body` | `string` | **Yes** | Markdown text |
385
+ | `ephemeral` | `boolean` | No | If `true`, shown temporarily and replaced by next activity |
386
+
387
+ #### `activity/action` — Show a Tool Call
388
+
389
+ | Field | Type | Required | Description |
390
+ |-------|------|----------|-------------|
391
+ | `activityAction` | `string` | **Yes** | Action verb (e.g. "Searching", "Building") |
392
+ | `parameter` | `string` | No | Subject of the action |
393
+ | `result` | `string` | No | Result (Markdown) |
394
+
395
+ #### `activity/elicitation` — Ask the User a Question
396
+
397
+ | Field | Type | Required | Description |
398
+ |-------|------|----------|-------------|
399
+ | `body` | `string` | **Yes** | Question text |
400
+ | `signal` | `string` | No | Set to `"select"` to show option buttons |
401
+ | `signalMeta` | `object` | No | `{ options: [{ value: "..." }] }` — list of choices |
402
+
403
+ #### `activity/response` — Post Final Response
404
+
405
+ | Field | Type | Required | Description |
406
+ |-------|------|----------|-------------|
407
+ | `body` | `string` | **Yes** | Final response (Markdown) |
408
+
409
+ Marks the session as having a posted response. The handler will skip auto-posting the agent's text output.
410
+
411
+ #### `activity/error` — Report an Error
412
+
413
+ | Field | Type | Required | Description |
414
+ |-------|------|----------|-------------|
415
+ | `body` | `string` | **Yes** | Error description |
416
+
417
+ ### Session Management
418
+
419
+ #### `session/plan` — Update Progress Checklist
420
+
421
+ | Field | Type | Required | Description |
422
+ |-------|------|----------|-------------|
423
+ | `plan` | `array` | **Yes** | Array of plan steps |
424
+
425
+ Each step: `{ content: "Step description", status: "pending" | "inProgress" | "completed" | "canceled" }`
426
+
427
+ **Note:** Replaces the entire plan each time. Always include all steps.
428
+
429
+ #### `session/create-on-issue` — Create Session on Another Issue
430
+
431
+ | Field | Type | Required | Description |
432
+ |-------|------|----------|-------------|
433
+ | `issueId` | `string` | **Yes** | Issue to create session on |
434
+
435
+ #### `session/create-on-comment` — Create Session on a Comment
436
+
437
+ | Field | Type | Required | Description |
438
+ |-------|------|----------|-------------|
439
+ | `commentId` | `string` | **Yes** | Comment to create session on |
440
+
441
+ #### `session/external-url` — Set External URL
442
+
443
+ | Field | Type | Required | Description |
444
+ |-------|------|----------|-------------|
445
+ | `url` | `string` | **Yes** | URL to attach |
446
+ | `label` | `string` | No | Link label (default: `"Link"`) |
447
+
448
+ ### Delegation
449
+
450
+ #### `delegate/assign` — Delegate to Agent or User
451
+
452
+ | Field | Type | Required | Description |
453
+ |-------|------|----------|-------------|
454
+ | `issueId` | `string` | No (defaults to current) | Issue to delegate |
455
+ | `delegateId` | `string` | **Yes** | Target agent or user ID |
456
+
457
+ #### `delegate/reassign` — Change Assignee
458
+
459
+ | Field | Type | Required | Description |
460
+ |-------|------|----------|-------------|
461
+ | `issueId` | `string` | No (defaults to current) | Issue to reassign |
462
+ | `assigneeId` | `string` | **Yes** | New assignee ID |
463
+
464
+ ### Queries
465
+
466
+ #### `query/issue` — Get Full Issue Details
467
+
468
+ | Field | Type | Required | Description |
469
+ |-------|------|----------|-------------|
470
+ | `issueId` | `string` | No (defaults to current) | Issue ID |
471
+
472
+ Returns: labels, state, assignee, delegate, parent, children, relations, recent comments.
473
+
474
+ #### `query/team` — Get Team Info
475
+
476
+ | Field | Type | Required | Description |
477
+ |-------|------|----------|-------------|
478
+ | `teamId` | `string` | No (defaults to current) | Team ID |
479
+
480
+ Returns: workflow states, labels, members.
481
+
482
+ #### `query/repo-suggestions` — Get AI-Ranked Repository Suggestions
483
+
484
+ | Field | Type | Required | Description |
485
+ |-------|------|----------|-------------|
486
+ | `issueId` | `string` | No (defaults to current) | Issue for context |
487
+ | `candidateRepositories` | `array` | **Yes** | `[{ hostname, repositoryFullName }]` |
488
+
489
+ Returns ranked suggestions with confidence scores.
490
+
491
+ #### `query/viewer` — Get Current App Identity
492
+
493
+ No parameters. Returns the authenticated app's user ID.
494
+
495
+ ## Architecture
496
+
497
+ ```
498
+ index.ts ← Entry point: registers HTTP routes
499
+ ├── src/
500
+ │ ├── types.ts ← Shared TypeScript interfaces
501
+ │ ├── config.ts ← Plugin config normalization
502
+ │ ├── util.ts ← HTTP/JSON helpers
503
+ │ ├── linear-client.ts ← Single gateway for all Linear GraphQL calls
504
+ │ ├── graphql/
505
+ │ │ ├── queries.ts ← GraphQL query strings
506
+ │ │ └── mutations.ts ← GraphQL mutation strings
507
+ │ ├── webhook/
508
+ │ │ ├── handler.ts ← Main webhook handler + agent orchestration
509
+ │ │ ├── validation.ts ← HMAC-SHA256 signature verification
510
+ │ │ ├── session-resolver.ts ← Session ID lookup (direct → cache → GraphQL)
511
+ │ │ ├── message-builder.ts ← Agent prompt construction
512
+ │ │ ├── response-parser.ts ← Parse agent output into response text
513
+ │ │ ├── issue-policy.ts ← Auto-start and auto-delegate policies
514
+ │ │ ├── close-intent.ts ← Natural language close detection
515
+ │ │ └── skip-filter.ts ← System echo and self-comment filtering
516
+ │ ├── api/
517
+ │ │ ├── router.ts ← API proxy router (bearer token auth)
518
+ │ │ ├── base-url.ts ← Auto-detect public URL from Host header
519
+ │ │ ├── issue-ops.ts ← Issue CRUD operations
520
+ │ │ ├── activity-ops.ts ← Agent activity posting
521
+ │ │ ├── session-ops.ts ← Session management (plans, proactive creation)
522
+ │ │ ├── delegation-ops.ts ← Issue delegation and reassignment
523
+ │ │ └── query-ops.ts ← Read-only queries (issue, team, viewer)
524
+ │ └── agent/
525
+ │ ├── session-token.ts ← Per-run bearer token lifecycle
526
+ │ ├── context-builder.ts ← Enriched prompt with API documentation
527
+ │ ├── response-tracker.ts ← Tracks whether agent already posted a response
528
+ │ └── plan-manager.ts ← In-memory plan state per session
529
+ ```
530
+
531
+ ### Key Design Patterns
532
+
533
+ | Pattern | Description |
534
+ |---------|-------------|
535
+ | **Single Linear Gateway** | All GraphQL communication goes through `callLinear()` which handles auth, errors, and logging |
536
+ | **Per-Session Tokens** | Each agent run gets a unique `crypto.randomBytes(32)` bearer token, revoked on completion |
537
+ | **Response Deduplication** | If the agent posts a response via the API, the handler skips auto-posting the text output |
538
+ | **Cascading Session Resolution** | Direct field → in-memory cache → GraphQL query with retry (120ms/350ms/800ms backoff) |
539
+ | **Side-Effect Registration** | API handlers register themselves via `registerApiHandler()` and are imported in `index.ts` |
540
+ | **Dedup Window** | Prevents double agent runs when Linear sends both AgentSession + Comment webhooks (5s window) |
541
+
542
+ ## Development
543
+
544
+ ### Build
545
+
546
+ ```bash
547
+ npm run build # Runs tsc, outputs to dist/
548
+ ```
549
+
550
+ ### Project Structure
551
+
552
+ - **TypeScript** with `strict: true`, targeting ES2022
553
+ - **ESM** (`"type": "module"` in package.json)
554
+ - **Node16 module resolution**
555
+ - Zero runtime dependencies (only `@types/node` and `typescript` as dev deps)
556
+
557
+ ### Adding a New API Operation
558
+
559
+ 1. Create a file in `src/api/` (e.g. `my-ops.ts`)
560
+ 2. Import and use `registerApiHandler`:
561
+
562
+ ```typescript
563
+ import { registerApiHandler } from "./router.js";
564
+ import { sendJson } from "../util.js";
565
+
566
+ registerApiHandler("/my/action", async ({ api, cfg, context, body, res }) => {
567
+ // context.sessionId, context.issueId, context.teamId are available
568
+ // body contains the parsed JSON request
569
+ sendJson(res, 200, { ok: true });
570
+ });
571
+ ```
572
+
573
+ 3. Add a side-effect import in `index.ts`:
574
+
575
+ ```typescript
576
+ import "./src/api/my-ops.js";
577
+ ```
578
+
579
+ The handler is now available as `{ "action": "my/action" }` through the API proxy.
580
+
581
+ ## Troubleshooting
582
+
583
+ ### Webhook not reaching the plugin
584
+
585
+ - Verify the URL in Linear application settings matches `https://<host>/plugins/linear/linear`
586
+ - Ensure your host is publicly reachable (check with `curl`)
587
+ - Check that "Agent session events" is enabled in the Linear app webhook settings
588
+
589
+ ### 401 Unauthorized on webhook
590
+
591
+ - Verify `linearWebhookSecret` matches the signing secret from Linear app settings
592
+ - Check that the webhook is not stale (>60 seconds old) — clock sync issues can cause this
593
+
594
+ ### Agent doesn't respond in Linear
595
+
596
+ - Check that `linearApiKey` is a valid OAuth token with the required scopes
597
+ - Verify the agent ID in `devAgentId` matches a configured OpenClaw agent
598
+ - Check OpenClaw gateway logs for errors
599
+
600
+ ### Agent API calls fail
601
+
602
+ - If `apiBaseUrl` is not set, the URL is auto-detected from the first webhook's `Host` header. Ensure your reverse proxy forwards the correct `Host`
603
+ - For Tailscale setups, the `Host` header with `.ts.net` domain is used automatically
604
+ - Set `apiBaseUrl` explicitly to bypass auto-detection
605
+
606
+ ### Self-comment loop
607
+
608
+ - The plugin automatically filters out comments authored by the app itself using the `viewer` query
609
+ - If you see loops, verify that the `linearApiKey` belongs to the same OAuth application that sends the webhooks
610
+
611
+ ## License
612
+
613
+ [MIT](LICENSE) &copy; [tokezooo](https://github.com/tokezooo)
@@ -0,0 +1,8 @@
1
+ import type { OpenClawPluginApi } from "./src/types.js";
2
+ import "./src/api/issue-ops.js";
3
+ import "./src/api/activity-ops.js";
4
+ import "./src/api/session-ops.js";
5
+ import "./src/api/delegation-ops.js";
6
+ import "./src/api/query-ops.js";
7
+ export default function register(api: OpenClawPluginApi): void;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKxD,OAAO,wBAAwB,CAAC;AAChC,OAAO,2BAA2B,CAAC;AACnC,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,wBAAwB,CAAC;AAEhC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAU7D"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createLinearWebhook } from "./src/webhook/handler.js";
2
+ import { createApiRouter } from "./src/api/router.js";
3
+ // Side-effect imports: register all API endpoint handlers
4
+ import "./src/api/issue-ops.js";
5
+ import "./src/api/activity-ops.js";
6
+ import "./src/api/session-ops.js";
7
+ import "./src/api/delegation-ops.js";
8
+ import "./src/api/query-ops.js";
9
+ export default function register(api) {
10
+ api.registerHttpRoute({
11
+ path: "/plugins/linear/linear",
12
+ handler: createLinearWebhook(api),
13
+ });
14
+ api.registerHttpRoute({
15
+ path: "/plugins/linear/api",
16
+ handler: createApiRouter(api),
17
+ });
18
+ }
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,0DAA0D;AAC1D,OAAO,wBAAwB,CAAC;AAChC,OAAO,2BAA2B,CAAC;AACnC,OAAO,0BAA0B,CAAC;AAClC,OAAO,6BAA6B,CAAC;AACrC,OAAO,wBAAwB,CAAC;AAEhC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAsB;IACrD,GAAG,CAAC,iBAAiB,CAAC;QACpB,IAAI,EAAE,wBAAwB;QAC9B,OAAO,EAAE,mBAAmB,CAAC,GAAG,CAAC;KAClC,CAAC,CAAC;IAEH,GAAG,CAAC,iBAAiB,CAAC;QACpB,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC;KAC9B,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type MessageParams } from "../webhook/message-builder.js";
2
+ export interface EnrichedMessageParams extends MessageParams {
3
+ apiBaseUrl: string;
4
+ apiToken: string;
5
+ issueId: string;
6
+ teamId: string;
7
+ }
8
+ export declare function buildEnrichedMessage(params: EnrichedMessageParams): string;
9
+ //# sourceMappingURL=context-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-builder.d.ts","sourceRoot":"","sources":["../../../src/agent/context-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEjF,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAwH1E"}