@sentry/junior-linear 0.120.0 → 0.122.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 +1 -1
- package/dist/index.js +128 -102
- package/dist/tools/create-issue.d.ts +31 -4
- package/dist/tools/update-issue.d.ts +47 -0
- package/package.json +2 -2
- package/skills/linear/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ export const plugins = defineJuniorPlugins([linearPlugin()]);
|
|
|
19
19
|
|
|
20
20
|
This package does not require a shared `LINEAR_API_KEY` or a custom OAuth app for the default setup. Each user connects their own Linear account the first time Junior calls a Linear MCP tool. Junior sends the authorization link privately and resumes the same Slack thread automatically after the user authorizes.
|
|
21
21
|
|
|
22
|
-
Issue creation uses a Junior-owned tool backed by Linear's hosted MCP connection. This lets Junior
|
|
22
|
+
Issue creation uses a Junior-owned tool backed by Linear's hosted MCP connection. This lets Junior link created issues to the conversation while keeping Linear's existing OAuth flow. Other Linear operations continue to use the hosted MCP tools.
|
|
23
23
|
|
|
24
24
|
Optional: set channel defaults when a Slack thread usually routes work to the same Linear destination:
|
|
25
25
|
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,9 @@ import {
|
|
|
6
6
|
// src/tools/create-issue.ts
|
|
7
7
|
import {
|
|
8
8
|
definePluginTool,
|
|
9
|
-
PluginToolInputError,
|
|
10
9
|
pluginToolResultSchema
|
|
11
10
|
} from "@sentry/junior-plugin-api";
|
|
12
11
|
import { z } from "zod";
|
|
13
|
-
var CREATE_ISSUE_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
14
|
-
var CREATE_ISSUE_LOCK_TTL_MS = 6e4;
|
|
15
12
|
var inputSchema = z.object({
|
|
16
13
|
team: z.string().trim().min(1).describe("Linear team name or identifier."),
|
|
17
14
|
title: z.string().trim().min(1).max(60).describe("Issue title."),
|
|
@@ -30,27 +27,10 @@ var outputSchema = pluginToolResultSchema.extend({
|
|
|
30
27
|
status: z.literal("success"),
|
|
31
28
|
target: z.literal("createIssue"),
|
|
32
29
|
data: z.object({
|
|
33
|
-
issue: issueSchema.nullable()
|
|
34
|
-
providerText: z.string()
|
|
30
|
+
issue: issueSchema.nullable()
|
|
35
31
|
}),
|
|
36
|
-
issue: issueSchema.nullable()
|
|
37
|
-
providerText: z.string()
|
|
32
|
+
issue: issueSchema.nullable()
|
|
38
33
|
});
|
|
39
|
-
var completedStateSchema = z.object({
|
|
40
|
-
createdAtMs: z.number(),
|
|
41
|
-
issue: issueSchema.nullable(),
|
|
42
|
-
providerText: z.string(),
|
|
43
|
-
status: z.literal("completed")
|
|
44
|
-
}).strict();
|
|
45
|
-
var pendingStateSchema = z.object({
|
|
46
|
-
createdAtMs: z.number(),
|
|
47
|
-
status: z.literal("pending")
|
|
48
|
-
}).strict();
|
|
49
|
-
var stateSchema = z.union([completedStateSchema, pendingStateSchema]);
|
|
50
|
-
function parseState(value) {
|
|
51
|
-
const parsed = stateSchema.safeParse(value);
|
|
52
|
-
return parsed.success ? parsed.data : void 0;
|
|
53
|
-
}
|
|
54
34
|
function collectObjects(value, objects) {
|
|
55
35
|
if (!value || typeof value !== "object") {
|
|
56
36
|
return;
|
|
@@ -70,11 +50,6 @@ function stringField(objects, field) {
|
|
|
70
50
|
}
|
|
71
51
|
return void 0;
|
|
72
52
|
}
|
|
73
|
-
function providerText(result) {
|
|
74
|
-
return result.content.filter(
|
|
75
|
-
(part) => part.type === "text"
|
|
76
|
-
).map((part) => part.text).join("\n\n").trim();
|
|
77
|
-
}
|
|
78
53
|
function parseJsonText(text) {
|
|
79
54
|
try {
|
|
80
55
|
return JSON.parse(text);
|
|
@@ -83,14 +58,20 @@ function parseJsonText(text) {
|
|
|
83
58
|
}
|
|
84
59
|
}
|
|
85
60
|
function extractIssue(result) {
|
|
86
|
-
const text = providerText(result);
|
|
87
61
|
const objects = [];
|
|
88
62
|
collectObjects(result.structuredContent, objects);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/https:\/\/linear\.app\/[^\s<>)"']+\/issue\/([A-Z][A-Z0-9]*-\d+)[^\s<>)"']*/i
|
|
63
|
+
const textParts = result.content.flatMap(
|
|
64
|
+
(part) => part.type === "text" ? [part.text] : []
|
|
92
65
|
);
|
|
93
|
-
const
|
|
66
|
+
for (const text of textParts) {
|
|
67
|
+
collectObjects(parseJsonText(text), objects);
|
|
68
|
+
}
|
|
69
|
+
const urlMatch = textParts.map(
|
|
70
|
+
(text) => text.match(
|
|
71
|
+
/https:\/\/linear\.app\/[^\s<>)"']+\/issue\/([A-Z][A-Z0-9]*-\d+)[^\s<>)"']*/i
|
|
72
|
+
)
|
|
73
|
+
).find((match) => match !== null);
|
|
74
|
+
const identifierMatch = textParts.map((text) => text.match(/\b[A-Z][A-Z0-9]*-\d+\b/)).find((match) => match !== null);
|
|
94
75
|
const issue = {
|
|
95
76
|
id: stringField(objects, "id"),
|
|
96
77
|
identifier: stringField(objects, "identifier") ?? urlMatch?.[1]?.toUpperCase() ?? identifierMatch?.[0],
|
|
@@ -107,16 +88,22 @@ function extractIssue(result) {
|
|
|
107
88
|
}
|
|
108
89
|
return parsed.data;
|
|
109
90
|
}
|
|
110
|
-
function toolResult(issue,
|
|
111
|
-
const data = { issue
|
|
91
|
+
function toolResult(issue, content) {
|
|
92
|
+
const data = { issue };
|
|
112
93
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
94
|
+
content,
|
|
95
|
+
details: {
|
|
96
|
+
ok: true,
|
|
97
|
+
status: "success",
|
|
98
|
+
target: "createIssue",
|
|
99
|
+
data,
|
|
100
|
+
...data
|
|
101
|
+
}
|
|
118
102
|
};
|
|
119
103
|
}
|
|
104
|
+
function authorizationPendingResult() {
|
|
105
|
+
return toolResult(null, [{ type: "text", text: "Authorization pending." }]);
|
|
106
|
+
}
|
|
120
107
|
async function annotateIssue(ctx, issue) {
|
|
121
108
|
if (!issue?.identifier || !issue.url) {
|
|
122
109
|
return;
|
|
@@ -129,10 +116,6 @@ async function annotateIssue(ctx, issue) {
|
|
|
129
116
|
status: "open"
|
|
130
117
|
});
|
|
131
118
|
}
|
|
132
|
-
function required(value, name) {
|
|
133
|
-
if (!value?.trim()) throw new PluginToolInputError(`${name} is required`);
|
|
134
|
-
return value.trim();
|
|
135
|
-
}
|
|
136
119
|
function createLinearIssueTool(ctx) {
|
|
137
120
|
return definePluginTool({
|
|
138
121
|
description: "Create a Linear issue through Linear's hosted MCP provider and link the created issue to the current Junior conversation.",
|
|
@@ -140,7 +123,7 @@ function createLinearIssueTool(ctx) {
|
|
|
140
123
|
outputSchema,
|
|
141
124
|
annotations: {
|
|
142
125
|
destructiveHint: false,
|
|
143
|
-
idempotentHint:
|
|
126
|
+
idempotentHint: false,
|
|
144
127
|
openWorldHint: true,
|
|
145
128
|
readOnlyHint: false,
|
|
146
129
|
title: "Create Linear issue"
|
|
@@ -153,64 +136,106 @@ function createLinearIssueTool(ctx) {
|
|
|
153
136
|
if (!mcp) {
|
|
154
137
|
throw new Error("Linear MCP provider is unavailable.");
|
|
155
138
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
139
|
+
const result = await mcp.callTool({
|
|
140
|
+
name: "save_issue",
|
|
141
|
+
arguments: input,
|
|
142
|
+
toolCallId: options.toolCallId
|
|
143
|
+
});
|
|
144
|
+
if (result.status === "authorization_pending") {
|
|
145
|
+
return authorizationPendingResult();
|
|
146
|
+
}
|
|
147
|
+
if (result.status === "error") {
|
|
148
|
+
throw new Error(result.message);
|
|
149
|
+
}
|
|
150
|
+
const issue = extractIssue(result);
|
|
151
|
+
await annotateIssue(ctx, issue);
|
|
152
|
+
return toolResult(issue, result.content);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/tools/update-issue.ts
|
|
158
|
+
import {
|
|
159
|
+
definePluginTool as definePluginTool2,
|
|
160
|
+
pluginToolResultSchema as pluginToolResultSchema2
|
|
161
|
+
} from "@sentry/junior-plugin-api";
|
|
162
|
+
import { z as z2 } from "zod";
|
|
163
|
+
var nullableName = (description) => z2.string().trim().min(1).nullable().describe(description).optional();
|
|
164
|
+
var inputSchema2 = z2.object({
|
|
165
|
+
id: z2.string().trim().min(1).describe("Linear issue identifier or ID to update."),
|
|
166
|
+
assignee: nullableName(
|
|
167
|
+
'Assignee name, email, identifier, or "me". Use null to clear.'
|
|
168
|
+
),
|
|
169
|
+
cycle: nullableName("Cycle name or identifier. Use null to clear."),
|
|
170
|
+
description: z2.string().nullable().describe("Issue description. Use null to clear.").optional(),
|
|
171
|
+
estimate: z2.number().nonnegative().nullable().describe("Issue estimate. Use null to clear.").optional(),
|
|
172
|
+
labels: z2.array(z2.string().trim().min(1)).describe("Complete set of issue label names or identifiers.").optional(),
|
|
173
|
+
priority: z2.enum(["low", "medium", "high", "urgent"]).nullable().describe("Linear priority. Use null to clear.").optional(),
|
|
174
|
+
project: nullableName("Project name or identifier. Use null to clear."),
|
|
175
|
+
state: z2.string().trim().min(1).describe("Workflow state name or identifier.").optional(),
|
|
176
|
+
team: z2.string().trim().min(1).describe("Team name or identifier.").optional(),
|
|
177
|
+
title: z2.string().trim().min(1).max(60).describe("Issue title.").optional()
|
|
178
|
+
}).strict().superRefine((input, ctx) => {
|
|
179
|
+
if (Object.keys(input).every((key) => key === "id")) {
|
|
180
|
+
ctx.addIssue({
|
|
181
|
+
code: "custom",
|
|
182
|
+
message: "At least one issue field must be provided."
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
var outputSchema2 = pluginToolResultSchema2.extend({
|
|
187
|
+
ok: z2.literal(true),
|
|
188
|
+
status: z2.literal("success"),
|
|
189
|
+
target: z2.literal("updateIssue")
|
|
190
|
+
});
|
|
191
|
+
function toolResult2(content) {
|
|
192
|
+
return {
|
|
193
|
+
content,
|
|
194
|
+
details: {
|
|
195
|
+
ok: true,
|
|
196
|
+
status: "success",
|
|
197
|
+
target: "updateIssue"
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function authorizationPendingResult2() {
|
|
202
|
+
return toolResult2([{ type: "text", text: "Authorization pending." }]);
|
|
203
|
+
}
|
|
204
|
+
function createLinearUpdateIssueTool(ctx) {
|
|
205
|
+
return definePluginTool2({
|
|
206
|
+
description: "Update an existing Linear issue through Linear's hosted MCP provider. Fetch the issue first when a change could overwrite existing fields.",
|
|
207
|
+
inputSchema: inputSchema2,
|
|
208
|
+
outputSchema: outputSchema2,
|
|
209
|
+
annotations: {
|
|
210
|
+
destructiveHint: false,
|
|
211
|
+
idempotentHint: true,
|
|
212
|
+
openWorldHint: true,
|
|
213
|
+
readOnlyHint: false,
|
|
214
|
+
title: "Update Linear issue"
|
|
215
|
+
},
|
|
216
|
+
describeProposal(input) {
|
|
217
|
+
return `Update Linear issue ${input.id}`;
|
|
218
|
+
},
|
|
219
|
+
async execute(input, options) {
|
|
220
|
+
const mcp = ctx.mcp;
|
|
221
|
+
if (!mcp) {
|
|
222
|
+
throw new Error("Linear MCP provider is unavailable.");
|
|
223
|
+
}
|
|
158
224
|
if (await mcp.prepare() === "authorization_pending") {
|
|
159
|
-
return
|
|
225
|
+
return authorizationPendingResult2();
|
|
226
|
+
}
|
|
227
|
+
const result = await mcp.callTool({
|
|
228
|
+
name: "save_issue",
|
|
229
|
+
arguments: input,
|
|
230
|
+
toolCallId: options.toolCallId
|
|
231
|
+
});
|
|
232
|
+
if (result.status === "authorization_pending") {
|
|
233
|
+
return authorizationPendingResult2();
|
|
234
|
+
}
|
|
235
|
+
if (result.status === "error") {
|
|
236
|
+
throw new Error(result.message);
|
|
160
237
|
}
|
|
161
|
-
|
|
162
|
-
return await ctx.state.withLock(
|
|
163
|
-
`${key}:lock`,
|
|
164
|
-
CREATE_ISSUE_LOCK_TTL_MS,
|
|
165
|
-
async () => {
|
|
166
|
-
const state = parseState(await ctx.state.get(key));
|
|
167
|
-
if (state?.status === "completed") {
|
|
168
|
-
await annotateIssue(ctx, state.issue);
|
|
169
|
-
return toolResult(state.issue, state.providerText);
|
|
170
|
-
}
|
|
171
|
-
if (state?.status === "pending") {
|
|
172
|
-
throw new Error(
|
|
173
|
-
"Linear issue creation for this tool call has an uncertain pending result; refusing to create a duplicate issue."
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
await ctx.state.set(
|
|
177
|
-
key,
|
|
178
|
-
{ status: "pending", createdAtMs: Date.now() },
|
|
179
|
-
CREATE_ISSUE_STATE_TTL_MS
|
|
180
|
-
);
|
|
181
|
-
const result = await mcp.callTool({
|
|
182
|
-
name: "create_issue",
|
|
183
|
-
arguments: input,
|
|
184
|
-
toolCallId
|
|
185
|
-
});
|
|
186
|
-
if (result.status === "authorization_pending") {
|
|
187
|
-
await ctx.state.delete(key);
|
|
188
|
-
return toolResult(null, "Authorization pending.");
|
|
189
|
-
}
|
|
190
|
-
if (result.status === "error") {
|
|
191
|
-
await ctx.state.delete(key);
|
|
192
|
-
throw new Error(result.message);
|
|
193
|
-
}
|
|
194
|
-
const text = providerText(result);
|
|
195
|
-
const issue = extractIssue(result);
|
|
196
|
-
const completedState = {
|
|
197
|
-
status: "completed",
|
|
198
|
-
createdAtMs: Date.now(),
|
|
199
|
-
issue,
|
|
200
|
-
providerText: text
|
|
201
|
-
};
|
|
202
|
-
try {
|
|
203
|
-
await ctx.state.set(key, completedState, CREATE_ISSUE_STATE_TTL_MS);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
"Linear issue was created, but Junior could not persist the completed issue state.",
|
|
207
|
-
{ cause: error }
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
await annotateIssue(ctx, issue);
|
|
211
|
-
return toolResult(issue, text);
|
|
212
|
-
}
|
|
213
|
-
);
|
|
238
|
+
return toolResult2(result.content);
|
|
214
239
|
}
|
|
215
240
|
});
|
|
216
241
|
}
|
|
@@ -226,14 +251,15 @@ function linearPlugin() {
|
|
|
226
251
|
mcp: {
|
|
227
252
|
transport: "http",
|
|
228
253
|
url: "https://mcp.linear.app/mcp",
|
|
229
|
-
wrappedTools: ["
|
|
254
|
+
wrappedTools: ["save_issue"]
|
|
230
255
|
},
|
|
231
256
|
name: "linear"
|
|
232
257
|
},
|
|
233
258
|
hooks: {
|
|
234
259
|
tools(ctx) {
|
|
235
260
|
return {
|
|
236
|
-
createIssue: createLinearIssueTool(ctx)
|
|
261
|
+
createIssue: createLinearIssueTool(ctx),
|
|
262
|
+
updateIssue: createLinearUpdateIssueTool(ctx)
|
|
237
263
|
};
|
|
238
264
|
}
|
|
239
265
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
|
|
1
|
+
import { type PluginToolResultEnvelope, type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
|
|
2
2
|
/** Create a Linear issue through the hosted MCP provider and link it to the conversation. */
|
|
3
3
|
export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
|
|
4
4
|
team: string;
|
|
@@ -18,7 +18,6 @@ export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext):
|
|
|
18
18
|
title?: string | undefined;
|
|
19
19
|
url?: string | undefined;
|
|
20
20
|
} | null;
|
|
21
|
-
providerText: string;
|
|
22
21
|
};
|
|
23
22
|
issue: {
|
|
24
23
|
id?: string | undefined;
|
|
@@ -26,7 +25,6 @@ export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext):
|
|
|
26
25
|
title?: string | undefined;
|
|
27
26
|
url?: string | undefined;
|
|
28
27
|
} | null;
|
|
29
|
-
providerText: string;
|
|
30
28
|
truncated?: boolean | undefined;
|
|
31
29
|
continuation?: {
|
|
32
30
|
arguments: Record<string, unknown>;
|
|
@@ -37,4 +35,33 @@ export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext):
|
|
|
37
35
|
message: string;
|
|
38
36
|
retryable?: boolean | undefined;
|
|
39
37
|
} | undefined;
|
|
40
|
-
}
|
|
38
|
+
}, PluginToolResultEnvelope<{
|
|
39
|
+
[x: string]: unknown;
|
|
40
|
+
ok: true;
|
|
41
|
+
status: "success";
|
|
42
|
+
target: "createIssue";
|
|
43
|
+
data: {
|
|
44
|
+
issue: {
|
|
45
|
+
id?: string | undefined;
|
|
46
|
+
identifier?: string | undefined;
|
|
47
|
+
title?: string | undefined;
|
|
48
|
+
url?: string | undefined;
|
|
49
|
+
} | null;
|
|
50
|
+
};
|
|
51
|
+
issue: {
|
|
52
|
+
id?: string | undefined;
|
|
53
|
+
identifier?: string | undefined;
|
|
54
|
+
title?: string | undefined;
|
|
55
|
+
url?: string | undefined;
|
|
56
|
+
} | null;
|
|
57
|
+
truncated?: boolean | undefined;
|
|
58
|
+
continuation?: {
|
|
59
|
+
arguments: Record<string, unknown>;
|
|
60
|
+
reason?: string | undefined;
|
|
61
|
+
} | undefined;
|
|
62
|
+
error?: string | {
|
|
63
|
+
kind: string;
|
|
64
|
+
message: string;
|
|
65
|
+
retryable?: boolean | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
}>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type PluginToolResultEnvelope, type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
|
|
2
|
+
/** Update a Linear issue through the hosted MCP provider. */
|
|
3
|
+
export declare function createLinearUpdateIssueTool(ctx: ToolRegistrationHookContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
|
|
4
|
+
id: string;
|
|
5
|
+
assignee?: string | null | undefined;
|
|
6
|
+
cycle?: string | null | undefined;
|
|
7
|
+
description?: string | null | undefined;
|
|
8
|
+
estimate?: number | null | undefined;
|
|
9
|
+
labels?: string[] | undefined;
|
|
10
|
+
priority?: "low" | "medium" | "high" | "urgent" | null | undefined;
|
|
11
|
+
project?: string | null | undefined;
|
|
12
|
+
state?: string | undefined;
|
|
13
|
+
team?: string | undefined;
|
|
14
|
+
title?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
[x: string]: unknown;
|
|
17
|
+
ok: true;
|
|
18
|
+
status: "success";
|
|
19
|
+
target: "updateIssue";
|
|
20
|
+
data?: unknown;
|
|
21
|
+
truncated?: boolean | undefined;
|
|
22
|
+
continuation?: {
|
|
23
|
+
arguments: Record<string, unknown>;
|
|
24
|
+
reason?: string | undefined;
|
|
25
|
+
} | undefined;
|
|
26
|
+
error?: string | {
|
|
27
|
+
kind: string;
|
|
28
|
+
message: string;
|
|
29
|
+
retryable?: boolean | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
}, PluginToolResultEnvelope<{
|
|
32
|
+
[x: string]: unknown;
|
|
33
|
+
ok: true;
|
|
34
|
+
status: "success";
|
|
35
|
+
target: "updateIssue";
|
|
36
|
+
data?: unknown;
|
|
37
|
+
truncated?: boolean | undefined;
|
|
38
|
+
continuation?: {
|
|
39
|
+
arguments: Record<string, unknown>;
|
|
40
|
+
reason?: string | undefined;
|
|
41
|
+
} | undefined;
|
|
42
|
+
error?: string | {
|
|
43
|
+
kind: string;
|
|
44
|
+
message: string;
|
|
45
|
+
retryable?: boolean | undefined;
|
|
46
|
+
} | undefined;
|
|
47
|
+
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-linear",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.122.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"zod": "^4.4.3",
|
|
26
|
-
"@sentry/junior-plugin-api": "0.
|
|
26
|
+
"@sentry/junior-plugin-api": "0.122.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.9.1",
|
package/skills/linear/SKILL.md
CHANGED
|
@@ -68,6 +68,7 @@ If any gate fails, revise and re-check before calling the Linear create/update t
|
|
|
68
68
|
6. Execute:
|
|
69
69
|
|
|
70
70
|
- Create new issues with `linear_createIssue`.
|
|
71
|
+
- Update existing issues with `linear_updateIssue`.
|
|
71
72
|
- For updates, prefer partial changes over full rewrites. Fetch current issue state first if the mutation could overwrite structured fields or duplicate an existing comment.
|
|
72
73
|
- Check for duplicates silently before creating a new issue when the request appears related to existing work.
|
|
73
74
|
|