@calltelemetry/openclaw-linear 0.6.0 → 0.7.0

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
@@ -39,8 +39,9 @@ An OpenClaw plugin that connects Linear to AI agents. Issues get triaged automat
39
39
  | **Real-time progress** | Agent activity (thinking, acting, responding) streams to Linear's UI |
40
40
  | **Unified `code_run`** | One tool, three backends (Codex, Claude Code, Gemini), configurable per agent |
41
41
  | **Issue management** | Agents use `linearis` CLI to update status, close issues, add comments |
42
- | **Customizable prompts** | `prompts.yaml` -- edit worker, audit, and rework prompts without rebuilding |
43
- | **Discord notifications** | Dispatch lifecycle events posted to a Discord channel |
42
+ | **Project planner** | Interactive interview builds out Linear issue hierarchies with dependency DAGs |
43
+ | **Customizable prompts** | `prompts.yaml` -- edit worker, audit, planner, and rework prompts without rebuilding |
44
+ | **Multi-channel notifications** | Dispatch events fan out to Discord, Slack, Telegram, Signal, or any OpenClaw channel |
44
45
  | **Artifact logging** | Every dispatch writes structured logs to `.claw/` in the worktree |
45
46
 
46
47
  ---
@@ -74,6 +75,12 @@ sequenceDiagram
74
75
  Plugin->>Agents: QA agent
75
76
  Agents-->>Plugin: Response
76
77
  Plugin-->>Linear: Branded comment
78
+
79
+ You->>Linear: Comment "@ctclaw plan this project"
80
+ Linear->>Plugin: Webhook (Comment)
81
+ Plugin->>Agents: Planner agent
82
+ Agents-->>Linear: Creates epics + issues + dependency DAG
83
+ Agents-->>Linear: Interview question
77
84
  ```
78
85
 
79
86
  ---
@@ -122,6 +129,23 @@ stateDiagram-v2
122
129
 
123
130
  All transitions use compare-and-swap (CAS) to prevent races. `dispatch-state.json` is the canonical source of truth.
124
131
 
132
+ ### Project Planner Pipeline
133
+
134
+ When `@ctclaw plan this project` is commented on a root issue, the plugin enters planning mode:
135
+
136
+ ```mermaid
137
+ flowchart TD
138
+ A["Comment: 'plan this project'"] --> B["INITIATE<br/>Register session, post welcome"]
139
+ B --> C["INTERVIEW<br/>Agent asks questions,<br/>creates issues + DAG"]
140
+ C -->|user responds| C
141
+ C -->|"'finalize plan'"| D["AUDIT<br/>DAG validation, completeness checks"]
142
+ D -->|Pass| E["APPROVED<br/>Project ready for dispatch"]
143
+ D -->|Fail| C
144
+ C -->|"'abandon'"| F["ABANDONED"]
145
+ ```
146
+
147
+ During planning mode, the planner creates epics, sub-issues, and dependency relationships (`blocks`/`blocked_by`) via 5 dedicated tools. Issue dispatch is blocked for projects in planning mode.
148
+
125
149
  ### Webhook Event Router
126
150
 
127
151
  ```mermaid
@@ -129,7 +153,7 @@ flowchart TD
129
153
  A["POST /linear/webhook"] --> B{"Event Type"}
130
154
  B --> C["AgentSessionEvent.created → Dispatch pipeline"]
131
155
  B --> D["AgentSessionEvent.prompted → Resume session"]
132
- B --> E["Comment.create → @mention routing"]
156
+ B --> E["Comment.create → @mention routing<br/>or planning mode"]
133
157
  B --> F["Issue.create → Auto-triage"]
134
158
  B --> G["Issue.update → Dispatch if assigned"]
135
159
  B --> H["AppUserNotification → Direct response"]
@@ -180,7 +204,9 @@ openclaw-linear/
180
204
  | |-- dispatch-service.ts Background monitor: stale detection, recovery, cleanup
181
205
  | |-- active-session.ts In-memory session registry (issueId -> session)
182
206
  | |-- tier-assess.ts Issue complexity assessment (junior/medior/senior)
183
- | +-- artifacts.ts .claw/ directory: manifest, logs, verdicts, summaries
207
+ | |-- artifacts.ts .claw/ directory: manifest, logs, verdicts, summaries
208
+ | |-- planner.ts Project planner orchestration (interview, audit)
209
+ | +-- planning-state.ts File-backed planning state (mirrors dispatch-state)
184
210
  |
185
211
  |-- agent/ Agent execution & monitoring
186
212
  | |-- agent.ts Embedded runner + subprocess fallback, retry on watchdog kill
@@ -193,7 +219,8 @@ openclaw-linear/
193
219
  | |-- claude-tool.ts Claude Code CLI runner (JSONL -> Linear activities)
194
220
  | |-- codex-tool.ts Codex CLI runner (JSONL -> Linear activities)
195
221
  | |-- gemini-tool.ts Gemini CLI runner (JSONL -> Linear activities)
196
- | +-- orchestration-tools.ts spawn_agent / ask_agent for multi-agent delegation
222
+ | |-- orchestration-tools.ts spawn_agent / ask_agent for multi-agent delegation
223
+ | +-- planner-tools.ts 5 planning tools (create/link/update issues, audit DAG)
197
224
  |
198
225
  |-- api/ Linear API & auth
199
226
  | |-- linear-api.ts GraphQL client, token resolution, auto-refresh
@@ -203,7 +230,7 @@ openclaw-linear/
203
230
  +-- infra/ Infrastructure utilities
204
231
  |-- cli.ts CLI subcommands (auth, status, worktrees, prompts)
205
232
  |-- codex-worktree.ts Git worktree create/remove/status/PR helpers
206
- +-- notify.ts Discord notifier (+ noop fallback)
233
+ +-- notify.ts Multi-channel notifier (Discord, Slack, Telegram, Signal)
207
234
  ```
208
235
 
209
236
  ---
@@ -401,6 +428,9 @@ Once set up, the plugin responds to Linear events automatically:
401
428
  | Assign an issue to the agent | Worker-audit pipeline runs with watchdog protection |
402
429
  | Trigger an agent session | Agent responds directly in the session |
403
430
  | Comment `@qa check the tests` | QA agent responds with its expertise |
431
+ | Comment `@ctclaw plan this project` | Planner agent enters interview mode, builds issue DAG |
432
+ | Reply during planning mode | Planner creates/updates issues and asks next question |
433
+ | Comment "finalize plan" | DAG audit runs: cycles, orphans, missing estimates/priorities |
404
434
  | Ask "close this issue" | Agent runs `linearis issues update API-123 --status Done` |
405
435
  | Ask "use gemini to review" | Agent calls `code_run` with `backend: "gemini"` |
406
436
 
@@ -435,7 +465,7 @@ Set in `openclaw.json` under the plugin entry:
435
465
  | `codexTimeoutMs` | number | `600000` | Legacy timeout for coding CLIs (ms) |
436
466
  | `worktreeBaseDir` | string | `"~/.openclaw/worktrees"` | Base directory for worktrees |
437
467
  | `dispatchStatePath` | string | `"~/.openclaw/linear-dispatch-state.json"` | Dispatch state file |
438
- | `flowDiscordChannel` | string | -- | Discord channel ID for notifications |
468
+ | `notifications` | object | -- | [Notification targets and event toggles](#notifications) |
439
469
  | `promptsPath` | string | -- | Override path for `prompts.yaml` |
440
470
  | `maxReworkAttempts` | number | `2` | Max audit failures before escalation |
441
471
  | `inactivitySec` | number | `120` | Kill sessions with no I/O for this long |
@@ -511,17 +541,78 @@ openclaw openclaw-linear prompts validate # Validate structure and template va
511
541
 
512
542
  ## Notifications
513
543
 
514
- Configure `flowDiscordChannel` in plugin config with a Discord channel ID. Events:
544
+ Dispatch lifecycle events fan out to any combination of OpenClaw channels -- Discord, Slack, Telegram, Signal, or any channel the runtime supports.
515
545
 
516
- | Event | Message |
517
- |---|---|
518
- | Dispatch | `**API-123** dispatched -- Fix auth bug` |
519
- | Worker started | `**API-123** worker started (attempt 0)` |
520
- | Audit in progress | `**API-123** audit in progress` |
521
- | Audit passed | `**API-123** passed audit. PR ready.` |
522
- | Audit failed | `**API-123** failed audit (attempt 1). Gaps: ...` |
523
- | Escalation | `**API-123** needs human review -- audit failed 3x` |
524
- | Watchdog kill | `**API-123** killed by watchdog (no I/O for 120s). Retrying.` |
546
+ ### Configuration
547
+
548
+ Add a `notifications` object to your plugin config:
549
+
550
+ ```json
551
+ {
552
+ "plugins": {
553
+ "entries": {
554
+ "openclaw-linear": {
555
+ "config": {
556
+ "notifications": {
557
+ "targets": [
558
+ { "channel": "discord", "target": "1471743433566715974" },
559
+ { "channel": "slack", "target": "C0123456789", "accountId": "my-acct" },
560
+ { "channel": "telegram", "target": "-1003884997363" }
561
+ ],
562
+ "events": {
563
+ "auditing": false
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ **`targets`** -- Array of notification destinations. Each target specifies:
574
+
575
+ | Field | Required | Description |
576
+ |---|---|---|
577
+ | `channel` | Yes | OpenClaw channel name: `discord`, `slack`, `telegram`, `signal`, etc. |
578
+ | `target` | Yes | Channel/group/user ID to send to |
579
+ | `accountId` | No | Account ID for multi-account setups (Slack) |
580
+
581
+ **`events`** -- Per-event-type toggles. All events are enabled by default. Set to `false` to suppress.
582
+
583
+ ### Events
584
+
585
+ | Event | Kind | Example Message |
586
+ |---|---|---|
587
+ | Dispatch | `dispatch` | `API-123 dispatched -- Fix auth bug` |
588
+ | Worker started | `working` | `API-123 worker started (attempt 0)` |
589
+ | Audit in progress | `auditing` | `API-123 audit in progress` |
590
+ | Audit passed | `audit_pass` | `API-123 passed audit. PR ready.` |
591
+ | Audit failed | `audit_fail` | `API-123 failed audit (attempt 1). Gaps: no tests, missing validation` |
592
+ | Escalation | `escalation` | `API-123 needs human review -- audit failed 3x` |
593
+ | Stale detection | `stuck` | `API-123 stuck -- stale 2h` |
594
+ | Watchdog kill | `watchdog_kill` | `API-123 killed by watchdog (no I/O for 120s). Retrying (attempt 0).` |
595
+
596
+ ### Delivery
597
+
598
+ Messages route through OpenClaw's native runtime channel API:
599
+
600
+ - **Discord** -- `runtime.channel.discord.sendMessageDiscord()`
601
+ - **Slack** -- `runtime.channel.slack.sendMessageSlack()` (passes `accountId` for multi-workspace)
602
+ - **Telegram** -- `runtime.channel.telegram.sendMessageTelegram()` (silent mode)
603
+ - **Signal** -- `runtime.channel.signal.sendMessageSignal()`
604
+ - **Other** -- Falls back to `openclaw message send` CLI
605
+
606
+ Failures are isolated per target -- one channel going down doesn't block the others.
607
+
608
+ ### Notify CLI
609
+
610
+ ```bash
611
+ openclaw openclaw-linear notify status # Show configured targets and suppressed events
612
+ openclaw openclaw-linear notify test # Send test notification to all targets
613
+ openclaw openclaw-linear notify test --channel discord # Test only discord targets
614
+ openclaw openclaw-linear notify setup # Interactive target setup
615
+ ```
525
616
 
526
617
  ---
527
618
 
@@ -649,6 +740,13 @@ openclaw openclaw-linear worktrees --prune <path> # Remove a worktree
649
740
  openclaw openclaw-linear prompts show # Print current prompts
650
741
  openclaw openclaw-linear prompts path # Print resolved prompts file path
651
742
  openclaw openclaw-linear prompts validate # Validate prompt structure
743
+ openclaw openclaw-linear notify status # Show notification targets and event toggles
744
+ openclaw openclaw-linear notify test # Send test notification to all targets
745
+ openclaw openclaw-linear notify test --channel slack # Test specific channel only
746
+ openclaw openclaw-linear notify setup # Interactive notification target setup
747
+ openclaw openclaw-linear doctor # Run comprehensive health checks
748
+ openclaw openclaw-linear doctor --fix # Auto-fix safe issues
749
+ openclaw openclaw-linear doctor --json # Output results as JSON
652
750
  ```
653
751
 
654
752
  ---
package/index.ts CHANGED
@@ -9,7 +9,9 @@ import { LinearAgentApi, resolveLinearToken } from "./src/api/linear-api.js";
9
9
  import { createDispatchService } from "./src/pipeline/dispatch-service.js";
10
10
  import { readDispatchState, lookupSessionMapping, getActiveDispatch } from "./src/pipeline/dispatch-state.js";
11
11
  import { triggerAudit, processVerdict, type HookContext } from "./src/pipeline/pipeline.js";
12
- import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "./src/infra/notify.js";
12
+ import { createNotifierFromConfig, type NotifyFn } from "./src/infra/notify.js";
13
+ import { readPlanningState, setPlanningCache } from "./src/pipeline/planning-state.js";
14
+ import { createPlannerTools } from "./src/tools/planner-tools.js";
13
15
 
14
16
  export default function register(api: OpenClawPluginApi) {
15
17
  const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
@@ -36,6 +38,9 @@ export default function register(api: OpenClawPluginApi) {
36
38
  return createLinearTools(api, ctx);
37
39
  });
38
40
 
41
+ // Register planner tools (context injected at runtime via setActivePlannerContext)
42
+ api.registerTool(() => createPlannerTools());
43
+
39
44
  // Register Linear webhook handler on a dedicated route
40
45
  api.registerHttpRoute({
41
46
  path: "/linear/webhook",
@@ -63,31 +68,22 @@ export default function register(api: OpenClawPluginApi) {
63
68
  // Register dispatch monitor service (stale detection, session hydration, cleanup)
64
69
  api.registerService(createDispatchService(api));
65
70
 
71
+ // Hydrate planning state on startup
72
+ readPlanningState(pluginConfig?.planningStatePath as string | undefined).then((state) => {
73
+ for (const session of Object.values(state.sessions)) {
74
+ if (session.status === "interviewing" || session.status === "plan_review") {
75
+ setPlanningCache(session);
76
+ api.logger.info(`Planning: restored session for ${session.projectName} (${session.rootIdentifier})`);
77
+ }
78
+ }
79
+ }).catch((err) => api.logger.warn(`Planning state hydration failed: ${err}`));
80
+
66
81
  // ---------------------------------------------------------------------------
67
82
  // Dispatch pipeline v2: notifier + agent_end lifecycle hook
68
83
  // ---------------------------------------------------------------------------
69
84
 
70
- // Instantiate notifier (Discord if configured, otherwise noop)
71
- const discordBotToken = (() => {
72
- try {
73
- const config = JSON.parse(
74
- require("node:fs").readFileSync(
75
- require("node:path").join(process.env.HOME ?? "/home/claw", ".openclaw", "openclaw.json"),
76
- "utf8",
77
- ),
78
- );
79
- return config?.channels?.discord?.token as string | undefined;
80
- } catch { return undefined; }
81
- })();
82
- const flowDiscordChannel = pluginConfig?.flowDiscordChannel as string | undefined;
83
-
84
- const notify: NotifyFn = (discordBotToken && flowDiscordChannel)
85
- ? createDiscordNotifier(discordBotToken, flowDiscordChannel)
86
- : createNoopNotifier();
87
-
88
- if (flowDiscordChannel && discordBotToken) {
89
- api.logger.info(`Linear dispatch: Discord notifications enabled (channel: ${flowDiscordChannel})`);
90
- }
85
+ // Instantiate notifier (Discord, Slack, or both — config-driven)
86
+ const notify: NotifyFn = createNotifierFromConfig(pluginConfig, api.runtime);
91
87
 
92
88
  // Register agent_end hook — safety net for sessions_spawn sub-agents.
93
89
  // In the current implementation, the worker→audit→verdict flow runs inline
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-linear",
3
3
  "name": "Linear Agent",
4
4
  "description": "Linear integration with OAuth support, agent pipeline, and webhook-driven AI agent lifecycle",
5
- "version": "0.2.0",
5
+ "version": "0.7.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -20,7 +20,40 @@
20
20
  "enableOrchestration": { "type": "boolean", "description": "Allow agents to spawn sub-agents via spawn_agent/ask_agent tools", "default": true },
21
21
  "worktreeBaseDir": { "type": "string", "description": "Base directory for persistent git worktrees (default: ~/.openclaw/worktrees)" },
22
22
  "dispatchStatePath": { "type": "string", "description": "Path to dispatch state JSON file (default: ~/.openclaw/linear-dispatch-state.json)" },
23
- "flowDiscordChannel": { "type": "string", "description": "Discord channel ID for dispatch lifecycle notifications (omit to disable)" },
23
+ "planningStatePath": { "type": "string", "description": "Path to planning state JSON file (default: ~/.openclaw/linear-planning-state.json)" },
24
+ "notifications": {
25
+ "type": "object",
26
+ "description": "Dispatch lifecycle notification config — fan-out to any combination of channels",
27
+ "properties": {
28
+ "targets": {
29
+ "type": "array",
30
+ "description": "Notification targets — each routes to an OpenClaw channel",
31
+ "items": {
32
+ "type": "object",
33
+ "required": ["channel", "target"],
34
+ "properties": {
35
+ "channel": { "type": "string", "description": "OpenClaw channel name: discord, slack, telegram, signal, etc." },
36
+ "target": { "type": "string", "description": "Channel/group/user ID to send to" },
37
+ "accountId": { "type": "string", "description": "Account ID for multi-account channel setups (optional)" }
38
+ }
39
+ }
40
+ },
41
+ "events": {
42
+ "type": "object",
43
+ "description": "Per-event-type toggles (all default true, set false to suppress)",
44
+ "properties": {
45
+ "dispatch": { "type": "boolean" },
46
+ "working": { "type": "boolean" },
47
+ "auditing": { "type": "boolean" },
48
+ "audit_pass": { "type": "boolean" },
49
+ "audit_fail": { "type": "boolean" },
50
+ "escalation": { "type": "boolean" },
51
+ "stuck": { "type": "boolean" },
52
+ "watchdog_kill": { "type": "boolean" }
53
+ }
54
+ }
55
+ }
56
+ },
24
57
  "promptsPath": { "type": "string", "description": "Override path for prompts.yaml (default: ships with plugin)" },
25
58
  "maxReworkAttempts": { "type": "number", "description": "Max audit failures before escalation", "default": 2 },
26
59
  "inactivitySec": { "type": "number", "description": "Kill sessions with no I/O for this many seconds (default: 120)", "default": 120 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",
package/prompts.yaml CHANGED
@@ -77,3 +77,50 @@ rework:
77
77
 
78
78
  Address these specific issues in your rework. Focus on the gaps listed above.
79
79
  Remember: you do NOT have linearis access. Just fix the code and return a text summary.
80
+
81
+ planner:
82
+ system: |
83
+ You are a product planning specialist working with Linear. Your job is to interview
84
+ the user about features they want to build, then decompose them into a well-structured
85
+ project with epics, issues, and sub-issues connected by a dependency graph.
86
+
87
+ STRUCTURE RULES:
88
+ - Epics are high-level feature areas. Mark them with isEpic=true.
89
+ - Issues under epics are concrete deliverables with acceptance criteria.
90
+ - Sub-issues are atomic work units that together complete a parent issue.
91
+ - Use "blocks" relationships to express ordering: if A must finish before B starts, A blocks B.
92
+ - Every issue description must include clear acceptance criteria.
93
+ - Every non-epic issue needs a story point estimate and priority.
94
+
95
+ INTERVIEW APPROACH:
96
+ - Ask ONE focused question at a time. Never dump a questionnaire.
97
+ - After each user response, create or update issues to capture what you learned.
98
+ - Briefly summarize what you added before asking your next question.
99
+ - When the plan feels complete, invite the user to say "finalize plan".
100
+
101
+ interview: |
102
+ ## Project: {{projectName}} ({{rootIdentifier}})
103
+
104
+ ### Current Plan
105
+ {{planSnapshot}}
106
+
107
+ ### Recent conversation ({{turnCount}} turns so far)
108
+ {{commentHistory}}
109
+
110
+ ### User just said:
111
+ > {{userMessage}}
112
+
113
+ Continue the planning interview. Use your tools to create/update Linear issues based
114
+ on the user's input, then respond with a summary of changes and your next question.
115
+
116
+ audit_prompt: |
117
+ The user wants to finalize the plan for {{projectName}}. Run the `audit_plan` tool.
118
+ If it passes, congratulate and summarize the complete plan.
119
+ If it fails, list the specific issues that need attention and ask the user to help.
120
+
121
+ welcome: |
122
+ I'm entering planning mode for **{{projectName}}**. I'll interview you about the
123
+ features you want to build, then structure everything into Linear issues with proper
124
+ epic hierarchy and dependency chains.
125
+
126
+ Let's start — what is this project about, and what are the main feature areas?
@@ -148,15 +148,22 @@ export class LinearAgentApi {
148
148
  return this.refreshToken ? `Bearer ${this.accessToken}` : this.accessToken;
149
149
  }
150
150
 
151
- private async gql<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T> {
151
+ private async gql<T = unknown>(
152
+ query: string,
153
+ variables?: Record<string, unknown>,
154
+ extraHeaders?: Record<string, string>,
155
+ ): Promise<T> {
152
156
  await this.ensureValidToken();
153
157
 
158
+ const headers: Record<string, string> = {
159
+ "Content-Type": "application/json",
160
+ Authorization: this.authHeader(),
161
+ ...extraHeaders,
162
+ };
163
+
154
164
  const res = await fetch(LINEAR_GRAPHQL_URL, {
155
165
  method: "POST",
156
- headers: {
157
- "Content-Type": "application/json",
158
- Authorization: this.authHeader(),
159
- },
166
+ headers,
160
167
  body: JSON.stringify({ query, variables }),
161
168
  });
162
169
 
@@ -165,12 +172,15 @@ export class LinearAgentApi {
165
172
  this.expiresAt = 0; // force refresh
166
173
  await this.ensureValidToken();
167
174
 
175
+ const retryHeaders: Record<string, string> = {
176
+ "Content-Type": "application/json",
177
+ Authorization: this.authHeader(),
178
+ ...extraHeaders,
179
+ };
180
+
168
181
  const retry = await fetch(LINEAR_GRAPHQL_URL, {
169
182
  method: "POST",
170
- headers: {
171
- "Content-Type": "application/json",
172
- Authorization: this.authHeader(),
173
- },
183
+ headers: retryHeaders,
174
184
  body: JSON.stringify({ query, variables }),
175
185
  });
176
186
 
@@ -298,6 +308,9 @@ export class LinearAgentApi {
298
308
  labels: { nodes: Array<{ id: string; name: string }> };
299
309
  team: { id: string; name: string; issueEstimationType: string };
300
310
  comments: { nodes: Array<{ body: string; user: { name: string } | null; createdAt: string }> };
311
+ project: { id: string; name: string } | null;
312
+ parent: { id: string; identifier: string } | null;
313
+ relations: { nodes: Array<{ type: string; relatedIssue: { id: string; identifier: string; title: string } }> };
301
314
  }> {
302
315
  const data = await this.gql<{ issue: unknown }>(
303
316
  `query Issue($id: String!) {
@@ -318,6 +331,9 @@ export class LinearAgentApi {
318
331
  createdAt
319
332
  }
320
333
  }
334
+ project { id name }
335
+ parent { id identifier }
336
+ relations { nodes { type relatedIssue { id identifier title } } }
321
337
  }
322
338
  }`,
323
339
  { id: issueId },
@@ -356,6 +372,161 @@ export class LinearAgentApi {
356
372
  return data.team.labels.nodes;
357
373
  }
358
374
 
375
+ // ---------------------------------------------------------------------------
376
+ // Planning methods
377
+ // ---------------------------------------------------------------------------
378
+
379
+ async createIssue(input: {
380
+ teamId: string;
381
+ title: string;
382
+ description?: string;
383
+ projectId?: string;
384
+ parentId?: string;
385
+ priority?: number;
386
+ estimate?: number;
387
+ labelIds?: string[];
388
+ stateId?: string;
389
+ assigneeId?: string;
390
+ }): Promise<{ id: string; identifier: string }> {
391
+ // Sub-issues require the GraphQL-Features header
392
+ const extra = input.parentId ? { "GraphQL-Features": "sub_issues" } : undefined;
393
+ const data = await this.gql<{
394
+ issueCreate: { success: boolean; issue: { id: string; identifier: string } };
395
+ }>(
396
+ `mutation IssueCreate($input: IssueCreateInput!) {
397
+ issueCreate(input: $input) {
398
+ success
399
+ issue { id identifier }
400
+ }
401
+ }`,
402
+ { input },
403
+ extra,
404
+ );
405
+ return data.issueCreate.issue;
406
+ }
407
+
408
+ async createIssueRelation(input: {
409
+ issueId: string;
410
+ relatedIssueId: string;
411
+ type: "blocks" | "blocked_by" | "related" | "duplicate";
412
+ }): Promise<{ id: string }> {
413
+ const data = await this.gql<{
414
+ issueRelationCreate: { success: boolean; issueRelation: { id: string } };
415
+ }>(
416
+ `mutation IssueRelationCreate($input: IssueRelationCreateInput!) {
417
+ issueRelationCreate(input: $input) {
418
+ success
419
+ issueRelation { id }
420
+ }
421
+ }`,
422
+ { input },
423
+ );
424
+ return data.issueRelationCreate.issueRelation;
425
+ }
426
+
427
+ async getProject(projectId: string): Promise<{
428
+ id: string;
429
+ name: string;
430
+ description: string;
431
+ state: string;
432
+ teams: { nodes: Array<{ id: string; name: string }> };
433
+ }> {
434
+ const data = await this.gql<{ project: unknown }>(
435
+ `query Project($id: String!) {
436
+ project(id: $id) {
437
+ id
438
+ name
439
+ description
440
+ state
441
+ teams { nodes { id name } }
442
+ }
443
+ }`,
444
+ { id: projectId },
445
+ );
446
+ return data.project as any;
447
+ }
448
+
449
+ async getProjectIssues(projectId: string): Promise<Array<{
450
+ id: string;
451
+ identifier: string;
452
+ title: string;
453
+ description: string | null;
454
+ estimate: number | null;
455
+ priority: number;
456
+ state: { name: string; type: string };
457
+ parent: { id: string; identifier: string } | null;
458
+ labels: { nodes: Array<{ id: string; name: string }> };
459
+ relations: { nodes: Array<{ type: string; relatedIssue: { id: string; identifier: string; title: string } }> };
460
+ }>> {
461
+ const data = await this.gql<{
462
+ project: { issues: { nodes: unknown[] } };
463
+ }>(
464
+ `query ProjectIssues($id: String!) {
465
+ project(id: $id) {
466
+ issues {
467
+ nodes {
468
+ id
469
+ identifier
470
+ title
471
+ description
472
+ estimate
473
+ priority
474
+ state { name type }
475
+ parent { id identifier }
476
+ labels { nodes { id name } }
477
+ relations { nodes { type relatedIssue { id identifier title } } }
478
+ }
479
+ }
480
+ }
481
+ }`,
482
+ { id: projectId },
483
+ );
484
+ return data.project.issues.nodes as any;
485
+ }
486
+
487
+ async getTeamStates(teamId: string): Promise<Array<{
488
+ id: string;
489
+ name: string;
490
+ type: string;
491
+ }>> {
492
+ const data = await this.gql<{
493
+ team: { states: { nodes: Array<{ id: string; name: string; type: string }> } };
494
+ }>(
495
+ `query TeamStates($id: String!) {
496
+ team(id: $id) {
497
+ states: workflowStates { nodes { id name type } }
498
+ }
499
+ }`,
500
+ { id: teamId },
501
+ );
502
+ return data.team.states.nodes;
503
+ }
504
+
505
+ async updateIssueExtended(issueId: string, input: {
506
+ title?: string;
507
+ description?: string;
508
+ estimate?: number;
509
+ labelIds?: string[];
510
+ stateId?: string;
511
+ priority?: number;
512
+ projectId?: string;
513
+ parentId?: string;
514
+ assigneeId?: string;
515
+ dueDate?: string;
516
+ }): Promise<boolean> {
517
+ const data = await this.gql<{
518
+ issueUpdate: { success: boolean };
519
+ }>(
520
+ `mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
521
+ issueUpdate(id: $id, input: $input) {
522
+ success
523
+ }
524
+ }`,
525
+ { id: issueId, input },
526
+ );
527
+ return data.issueUpdate.success;
528
+ }
529
+
359
530
  async getAppNotifications(count: number = 5): Promise<Array<{
360
531
  id: string;
361
532
  type: string;