@enrichlayer/el-linear 1.9.0 → 1.15.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.
Files changed (135) hide show
  1. package/README.md +139 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/attachments.js +2 -1
  20. package/dist/commands/batch.js +18 -21
  21. package/dist/commands/comments.js +22 -33
  22. package/dist/commands/config.js +178 -5
  23. package/dist/commands/cycles.js +2 -1
  24. package/dist/commands/documents.js +2 -1
  25. package/dist/commands/graphql.js +4 -6
  26. package/dist/commands/init/aliases.js +1 -1
  27. package/dist/commands/init/defaults.d.ts +2 -1
  28. package/dist/commands/init/index.js +45 -35
  29. package/dist/commands/init/oauth.d.ts +4 -1
  30. package/dist/commands/init/oauth.js +22 -4
  31. package/dist/commands/init/shared.d.ts +24 -2
  32. package/dist/commands/init/shared.js +35 -4
  33. package/dist/commands/init/token.d.ts +3 -3
  34. package/dist/commands/init/token.js +5 -24
  35. package/dist/commands/init/workspace.d.ts +2 -1
  36. package/dist/commands/init/workspace.js +1 -1
  37. package/dist/commands/introspect.d.ts +27 -0
  38. package/dist/commands/introspect.js +178 -0
  39. package/dist/commands/issue-id.js +1 -3
  40. package/dist/commands/issues/branch.js +9 -1
  41. package/dist/commands/issues/description.js +2 -6
  42. package/dist/commands/issues/link-references.d.ts +21 -0
  43. package/dist/commands/issues/link-references.js +171 -0
  44. package/dist/commands/issues/relations.d.ts +44 -0
  45. package/dist/commands/issues/relations.js +132 -0
  46. package/dist/commands/issues.js +269 -309
  47. package/dist/commands/labels.js +15 -24
  48. package/dist/commands/profile.js +1 -0
  49. package/dist/commands/project-milestones.js +13 -20
  50. package/dist/commands/projects.d.ts +2 -0
  51. package/dist/commands/projects.js +157 -44
  52. package/dist/commands/read-shortcut.d.ts +1 -1
  53. package/dist/commands/read-shortcut.js +28 -8
  54. package/dist/commands/refs.js +75 -8
  55. package/dist/commands/releases.js +26 -30
  56. package/dist/commands/search.js +49 -33
  57. package/dist/commands/teams.js +2 -1
  58. package/dist/commands/templates.js +9 -14
  59. package/dist/commands/users.js +5 -2
  60. package/dist/config/config.d.ts +99 -1
  61. package/dist/config/config.js +264 -52
  62. package/dist/config/error-enrichment.d.ts +62 -0
  63. package/dist/config/error-enrichment.js +417 -0
  64. package/dist/config/issue-validation.d.ts +37 -0
  65. package/dist/config/issue-validation.js +63 -1
  66. package/dist/config/paths.d.ts +2 -8
  67. package/dist/config/paths.js +4 -2
  68. package/dist/config/resolver.d.ts +8 -1
  69. package/dist/config/resolver.js +11 -5
  70. package/dist/main.js +13 -1
  71. package/dist/queries/attachments-types.d.ts +30 -0
  72. package/dist/queries/attachments-types.js +5 -0
  73. package/dist/queries/comments-types.d.ts +55 -0
  74. package/dist/queries/comments-types.js +5 -0
  75. package/dist/queries/common.d.ts +2 -2
  76. package/dist/queries/common.js +8 -0
  77. package/dist/queries/documents-types.d.ts +62 -0
  78. package/dist/queries/documents-types.js +9 -0
  79. package/dist/queries/introspect-types.d.ts +58 -0
  80. package/dist/queries/introspect-types.js +10 -0
  81. package/dist/queries/issues-types.d.ts +481 -0
  82. package/dist/queries/issues-types.js +23 -0
  83. package/dist/queries/issues.d.ts +51 -10
  84. package/dist/queries/issues.js +147 -5
  85. package/dist/queries/labels-types.d.ts +65 -0
  86. package/dist/queries/labels-types.js +5 -0
  87. package/dist/queries/project-milestones-types.d.ts +92 -0
  88. package/dist/queries/project-milestones-types.js +10 -0
  89. package/dist/queries/project-milestones.d.ts +1 -1
  90. package/dist/queries/projects-types.d.ts +76 -0
  91. package/dist/queries/projects-types.js +5 -0
  92. package/dist/queries/projects.d.ts +2 -0
  93. package/dist/queries/projects.js +22 -0
  94. package/dist/queries/releases-types.d.ts +85 -0
  95. package/dist/queries/releases-types.js +5 -0
  96. package/dist/queries/search-types.d.ts +102 -0
  97. package/dist/queries/search-types.js +6 -0
  98. package/dist/queries/templates-types.d.ts +62 -0
  99. package/dist/queries/templates-types.js +9 -0
  100. package/dist/types/linear.d.ts +21 -3
  101. package/dist/utils/auto-link-references.d.ts +3 -3
  102. package/dist/utils/auto-link-references.js +30 -34
  103. package/dist/utils/extract-field.d.ts +19 -0
  104. package/dist/utils/extract-field.js +99 -0
  105. package/dist/utils/file-service.d.ts +6 -13
  106. package/dist/utils/file-service.js +0 -2
  107. package/dist/utils/formatters/summary.js +6 -1
  108. package/dist/utils/graphql-attachments-service.js +6 -9
  109. package/dist/utils/graphql-documents-service.js +19 -25
  110. package/dist/utils/graphql-issues-service.d.ts +112 -46
  111. package/dist/utils/graphql-issues-service.js +398 -206
  112. package/dist/utils/graphql-service.d.ts +10 -12
  113. package/dist/utils/graphql-service.js +0 -3
  114. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  115. package/dist/utils/issue-reference-extractor.js +5 -3
  116. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  117. package/dist/utils/issues-service-bootstrap.js +27 -0
  118. package/dist/utils/linear-service.d.ts +21 -14
  119. package/dist/utils/linear-service.js +73 -11
  120. package/dist/utils/markdown-prosemirror.js +12 -12
  121. package/dist/utils/mention-resolver.js +1 -1
  122. package/dist/utils/output.d.ts +82 -2
  123. package/dist/utils/output.js +76 -11
  124. package/dist/utils/project-slug.d.ts +21 -0
  125. package/dist/utils/project-slug.js +45 -0
  126. package/dist/utils/protected-ranges.d.ts +14 -0
  127. package/dist/utils/protected-ranges.js +88 -2
  128. package/dist/utils/sanitize-for-log.d.ts +24 -0
  129. package/dist/utils/sanitize-for-log.js +38 -0
  130. package/dist/utils/table-formatter.js +24 -0
  131. package/dist/utils/validators.d.ts +7 -2
  132. package/dist/utils/validators.js +6 -0
  133. package/dist/utils/workspace-url.d.ts +5 -1
  134. package/dist/utils/workspace-url.js +53 -7
  135. package/package.json +2 -2
@@ -1,25 +1,26 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { loadConfig } from "../config/config.js";
3
+ import { enrichProjectResolverError, enrichValidationErrors, } from "../config/error-enrichment.js";
3
4
  import { enforceValidation, validateIssueCreation, } from "../config/issue-validation.js";
4
5
  import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
5
6
  import { resolveDefaultStatus } from "../config/status-defaults.js";
6
7
  import { enforceTerms } from "../config/term-enforcer.js";
7
- import { LIST_COMMENTS_QUERY } from "../queries/comments.js";
8
- import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, } from "../queries/issues.js";
9
- import { autoLinkReferences, } from "../utils/auto-link-references.js";
8
+ import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
10
9
  import { createFileService } from "../utils/file-service.js";
11
10
  import { applyFooter } from "../utils/footer.js";
12
11
  import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
13
- import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
14
12
  import { createGraphQLService, } from "../utils/graphql-service.js";
13
+ import { createIssuesService } from "../utils/issues-service-bootstrap.js";
15
14
  import { createLinearService, } from "../utils/linear-service.js";
16
15
  import { logger } from "../utils/logger.js";
17
- import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
16
+ import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
18
17
  import { getRootOpts } from "../utils/root-opts.js";
19
18
  import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
20
- import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
19
+ import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
21
20
  import { gitCheckoutBranch, toBranchName } from "./issues/branch.js";
22
- import { maybeAutoLink, prepareAutoLinkedDescription, prepareDescriptionRewrite, pushDescriptionUpdate, readDescriptionFile, resolveDescription, } from "./issues/description.js";
21
+ import { maybeAutoLink, prepareAutoLinkedDescription, readDescriptionFile, resolveDescription, } from "./issues/description.js";
22
+ import { handleLinkReferencesIssue } from "./issues/link-references.js";
23
+ import { buildIncomingRelationEntries, buildOutgoingRelationEntries, createRelations, } from "./issues/relations.js";
23
24
  import { readIssues } from "./read-shortcut.js";
24
25
  const IMAGE_EXTENSIONS = new Set([
25
26
  ".png",
@@ -35,54 +36,10 @@ function isImageFile(filename) {
35
36
  const ext = filename.lastIndexOf(".");
36
37
  return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
37
38
  }
38
- function transformIssueRelation(rel) {
39
- const issue = rel.issue;
40
- const relatedIssue = rel.relatedIssue;
41
- return {
42
- id: rel.id,
43
- type: rel.type,
44
- issue: {
45
- id: issue.id,
46
- identifier: issue.identifier,
47
- title: issue.title,
48
- },
49
- relatedIssue: {
50
- id: relatedIssue.id,
51
- identifier: relatedIssue.identifier,
52
- title: relatedIssue.title,
53
- },
54
- };
55
- }
56
- async function createRelations(sourceId, options, graphQLService, linearService) {
57
- const relations = [];
58
- const relationSpecs = [
59
- { option: "relatedTo", type: "related" },
60
- { option: "blocks", type: "blocks" },
61
- { option: "blockedBy", type: "blocks", reverse: true },
62
- { option: "duplicateOf", type: "duplicate" },
63
- ];
64
- for (const spec of relationSpecs) {
65
- const value = options[spec.option];
66
- if (!value) {
67
- continue;
68
- }
69
- for (const id of splitList(value)) {
70
- const targetId = await linearService.resolveIssueId(id);
71
- const rel = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
72
- input: {
73
- issueId: spec.reverse ? targetId : sourceId,
74
- relatedIssueId: spec.reverse ? sourceId : targetId,
75
- type: spec.type,
76
- },
77
- });
78
- const create = rel.issueRelationCreate;
79
- const issueRelation = create?.issueRelation;
80
- relations.push(transformIssueRelation(issueRelation));
81
- }
82
- }
83
- return relations;
84
- }
85
39
  function validateUpdateOptions(options) {
40
+ if (options.delegate && options.clearDelegate) {
41
+ throw new Error("Cannot use --delegate and --clear-delegate together");
42
+ }
86
43
  if (options.parentTicket && options.clearParentTicket) {
87
44
  throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
88
45
  }
@@ -108,7 +65,7 @@ function validateUpdateOptions(options) {
108
65
  throw new Error("Cannot use --description and --append-description together");
109
66
  }
110
67
  }
111
- function buildUpdateArgs(issueId, options, assigneeId) {
68
+ function buildUpdateArgs(issueId, options, assigneeId, delegateId) {
112
69
  let labelIds;
113
70
  if (options.clearLabels) {
114
71
  labelIds = [];
@@ -131,6 +88,7 @@ function buildUpdateArgs(issueId, options, assigneeId) {
131
88
  statusId: options.status,
132
89
  priority: priorityInput ? validatePriority(priorityInput) : undefined,
133
90
  assigneeId,
91
+ delegateId,
134
92
  projectId: options.project,
135
93
  labelIds,
136
94
  parentId: options.parentTicket || (options.clearParentTicket ? null : undefined),
@@ -183,44 +141,61 @@ function sortIssues(issues, sort) {
183
141
  }
184
142
  return sorted;
185
143
  }
144
+ /**
145
+ * Map commander's `options.project` (string | false | undefined) onto the
146
+ * `SearchIssueArgs.project` discriminant. Commander turns `--no-project`
147
+ * into `false`; a missing flag is `undefined`; a string is the project
148
+ * name or UUID to filter to.
149
+ */
150
+ function resolveProjectFlag(value) {
151
+ if (value === false)
152
+ return { kind: "none" };
153
+ if (value)
154
+ return { kind: "id", id: value };
155
+ return undefined;
156
+ }
186
157
  async function handleListIssues(options, command) {
187
158
  if (options.label && !options.labels) {
188
159
  options.labels = options.label;
189
160
  }
190
161
  const rootOpts = getRootOpts(command);
191
- const graphQLService = await createGraphQLService(rootOpts);
192
- const linearService = await createLinearService(rootOpts);
193
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
162
+ const { issuesService } = await createIssuesService(rootOpts);
194
163
  const hasFilters = options.team ||
195
164
  options.labels ||
196
165
  options.status ||
197
166
  options.assignee ||
167
+ options.delegate ||
198
168
  options.project ||
199
169
  options.project === false ||
200
170
  options.priority;
171
+ const limit = parsePositiveInt(options.limit, "--limit");
201
172
  if (hasFilters) {
202
173
  const searchArgs = {
203
174
  teamId: options.team ? resolveTeam(options.team) : undefined,
204
175
  assigneeId: options.assignee
205
176
  ? await resolveAssignee(options.assignee, rootOpts)
206
177
  : undefined,
207
- projectId: options.project || undefined,
208
- noProject: options.project === false,
178
+ delegateId: options.delegate
179
+ ? resolveMember(options.delegate)
180
+ : undefined,
181
+ project: resolveProjectFlag(options.project),
209
182
  labelNames: options.labels ? splitList(options.labels) : undefined,
210
183
  status: options.status ? splitList(options.status) : undefined,
211
184
  priority: options.priority
212
185
  ? parsePriorityFilter(options.priority)
213
186
  : undefined,
214
187
  orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
215
- limit: Number.parseInt(options.limit, 10),
188
+ limit,
216
189
  };
217
190
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
191
+ warnIfTruncated(result.length, limit);
218
192
  outputIssues(result, options.format, options.fields, {
219
193
  team: options.team,
220
194
  });
221
195
  }
222
196
  else {
223
- const result = sortIssues(await issuesService.getIssues(Number.parseInt(options.limit, 10)), options.sort);
197
+ const result = sortIssues(await issuesService.getIssues(limit), options.sort);
198
+ warnIfTruncated(result.length, limit);
224
199
  outputIssues(result, options.format, options.fields, {});
225
200
  }
226
201
  }
@@ -229,27 +204,58 @@ async function handleSearchIssues(query, options, command) {
229
204
  options.labels = options.label;
230
205
  }
231
206
  const rootOpts = getRootOpts(command);
232
- const graphQLService = await createGraphQLService(rootOpts);
233
- const linearService = await createLinearService(rootOpts);
234
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
207
+ const { issuesService } = await createIssuesService(rootOpts);
208
+ const limit = parsePositiveInt(options.limit, "--limit");
235
209
  const searchArgs = {
236
210
  query,
237
211
  teamId: options.team ? resolveTeam(options.team) : undefined,
238
212
  assigneeId: options.assignee
239
213
  ? await resolveAssignee(options.assignee, rootOpts)
240
214
  : undefined,
241
- projectId: options.project || undefined,
242
- noProject: options.project === false,
215
+ delegateId: options.delegate
216
+ ? resolveMember(options.delegate)
217
+ : undefined,
218
+ project: resolveProjectFlag(options.project),
243
219
  status: options.status ? splitList(options.status) : undefined,
244
220
  labelNames: options.labels ? splitList(options.labels) : undefined,
245
221
  priority: options.priority
246
222
  ? parsePriorityFilter(options.priority)
247
223
  : undefined,
248
- limit: Number.parseInt(options.limit, 10),
224
+ limit,
249
225
  };
250
226
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
227
+ warnIfTruncated(result.length, limit);
251
228
  outputIssues(result, options.format, options.fields, { query });
252
229
  }
230
+ /**
231
+ * Run an `issuesService.createIssue` / `.updateIssue` call and, if it
232
+ * throws a `Project "<input>" not found` resolver error, enrich the
233
+ * message with team-scoped project suggestions before re-throwing. Same
234
+ * suggestion shape as the `Missing --project` validation path — agents
235
+ * get a concrete retry list whether the failure was "forgot to pass
236
+ * --project" or "passed --project but it didn't resolve".
237
+ *
238
+ * Best-effort: any other error class flows through unchanged, and any
239
+ * failure inside enrichment falls back to the original message.
240
+ */
241
+ async function withProjectResolverEnrichment(fn, options, services) {
242
+ try {
243
+ return await fn();
244
+ }
245
+ catch (err) {
246
+ if (err instanceof Error && options.team) {
247
+ const enriched = await enrichProjectResolverError(err.message, { team: options.team, title: options.title }, services);
248
+ if (enriched !== err.message) {
249
+ // Mutate in place rather than throwing a fresh Error — preserves
250
+ // the original error subclass and stack trace so consumers that
251
+ // rely on `instanceof` (or read `.stack` via EL_LINEAR_DEBUG=1)
252
+ // keep working. Only the user-facing message changes.
253
+ err.message = enriched;
254
+ }
255
+ }
256
+ throw err;
257
+ }
258
+ }
253
259
  async function resolveCreateInputs(title, options, rootOpts) {
254
260
  const config = loadConfig();
255
261
  enforceTerms([title, options.description], { strict: options.strict });
@@ -291,6 +297,22 @@ async function resolveCreateInputs(title, options, rootOpts) {
291
297
  if (validationResult.normalizedLabels) {
292
298
  options.labels = validationResult.normalizedLabels.join(",");
293
299
  }
300
+ // Match the team-resolution rule used for the create call below
301
+ // (line ~389): fall back to config.defaultTeam when --team is omitted.
302
+ // Without this, users with a configured default team would see bare
303
+ // validation errors with no suggestions — the exact opposite of what
304
+ // the AI-coding-agent use case needs.
305
+ const enrichTeam = options.team || config.defaultTeam;
306
+ if (validationResult.errors.length > 0 && enrichTeam) {
307
+ try {
308
+ const graphQLService = await createGraphQLService(rootOpts);
309
+ const linearService = await createLinearService(rootOpts);
310
+ await enrichValidationErrors(validationResult, { team: enrichTeam, title }, { graphQLService, linearService });
311
+ }
312
+ catch {
313
+ // Best-effort enrichment — never block the original error.
314
+ }
315
+ }
294
316
  enforceValidation(validationResult);
295
317
  }
296
318
  if (!effectivePriorityInput) {
@@ -301,9 +323,16 @@ async function resolveCreateInputs(title, options, rootOpts) {
301
323
  const assigneeId = effectiveAssignee
302
324
  ? await resolveAssignee(effectiveAssignee, rootOpts)
303
325
  : undefined;
326
+ const delegateId = options.delegate
327
+ ? resolveMember(options.delegate)
328
+ : undefined;
304
329
  let labelIds = [];
305
330
  if (options.labels) {
306
- labelIds = resolveLabels(splitList(options.labels), teamInput);
331
+ // No `teamInput`: team-scoped labels are resolved API-side against the
332
+ // issue's *final* team, which `createIssue` may auto-switch to match
333
+ // the project. Resolving a team-scoped config UUID here would pin the
334
+ // label to the pre-switch team and Linear would reject the mismatch.
335
+ labelIds = resolveLabels(splitList(options.labels));
307
336
  }
308
337
  if (options.claude) {
309
338
  const claudeId = config.labels.workspace.claude;
@@ -327,6 +356,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
327
356
  teamInput,
328
357
  teamId,
329
358
  assigneeId,
359
+ delegateId,
330
360
  labelIds,
331
361
  status,
332
362
  subscriberIds,
@@ -365,7 +395,7 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
365
395
  }
366
396
  async function handleCreateIssue(title, options, command) {
367
397
  const rootOpts = getRootOpts(command);
368
- const { teamInput, teamId, assigneeId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
398
+ const { teamInput, teamId, assigneeId, delegateId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
369
399
  const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
370
400
  const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
371
401
  // Append messageFooter (config or --footer flag) so auto-link picks up any
@@ -377,14 +407,12 @@ async function handleCreateIssue(title, options, command) {
377
407
  footer: explicitFooter,
378
408
  noFooter,
379
409
  }) ?? "";
380
- const graphQLService = await createGraphQLService(rootOpts);
381
- const linearService = await createLinearService(rootOpts);
382
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
410
+ const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
383
411
  // Wrap valid issue identifiers as markdown links before creating, so the description
384
412
  // saved on Linear has clickable refs from the start. Self-reference can't apply here
385
413
  // because the issue doesn't exist yet — pass undefined.
386
414
  const prepared = await prepareAutoLinkedDescription(description || undefined, options, undefined, linearService, graphQLService);
387
- const result = await issuesService.createIssue({
415
+ const result = await withProjectResolverEnrichment(() => issuesService.createIssue({
388
416
  // title is omitted when --from-template is set without an override,
389
417
  // so Linear copies the template's title server-side.
390
418
  ...(title ? { title } : {}),
@@ -392,6 +420,7 @@ async function handleCreateIssue(title, options, command) {
392
420
  teamInput,
393
421
  description: prepared.description,
394
422
  assigneeId,
423
+ delegateId,
395
424
  priority,
396
425
  projectId: options.project,
397
426
  statusId: status,
@@ -407,7 +436,7 @@ async function handleCreateIssue(title, options, command) {
407
436
  ...(typeof options.fromTemplate === "string" && options.fromTemplate
408
437
  ? { templateId: options.fromTemplate }
409
438
  : {}),
410
- });
439
+ }), { team: options.team ?? loadConfig().defaultTeam, title }, { graphQLService, linearService });
411
440
  const relations = await createRelations(result.id, options, graphQLService, linearService);
412
441
  // Pass the ORIGINAL description (pre-wrap) so the extractor's prose-keyword inference
413
442
  // ("blocked by", "duplicates", etc.) sees `keyword DEV-100` instead of `keyword [DEV-100](url)`.
@@ -460,22 +489,19 @@ async function handleHistoryIssue(issueId, _options, command) {
460
489
  const graphQLService = await createGraphQLService(rootOpts);
461
490
  const linearService = await createLinearService(rootOpts);
462
491
  const resolvedId = await linearService.resolveIssueId(issueId);
463
- const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
464
- id: resolvedId,
465
- });
492
+ const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, { id: resolvedId });
466
493
  if (!result.issue) {
467
494
  throw new Error(`Issue "${issueId}" not found`);
468
495
  }
469
496
  const issue = result.issue;
470
- const stateHistory = issue.stateHistory;
471
- const spans = (stateHistory?.nodes ?? []).map((span) => ({
497
+ const spans = issue.stateHistory.nodes.map((span) => ({
472
498
  state: {
473
499
  id: span.state.id,
474
500
  name: span.state.name,
475
501
  type: span.state.type,
476
502
  },
477
503
  startedAt: span.startedAt,
478
- endedAt: span.endedAt || undefined,
504
+ endedAt: span.endedAt ?? undefined,
479
505
  }));
480
506
  outputSuccess({
481
507
  id: issue.id,
@@ -499,82 +525,19 @@ async function handleRelateIssue(issueId, options, command) {
499
525
  meta: { count: relations.length, source: issueId },
500
526
  });
501
527
  }
502
- /**
503
- * Invert relation type for incoming (inverse) relations so the output
504
- * reads naturally from the perspective of the queried issue.
505
- * e.g. if DEV-100 "blocks" DEV-200, and we query DEV-200,
506
- * the inverse relation type is "blocks" but direction is incoming → "blockedBy".
507
- */
508
- function normalizeInverseType(type) {
509
- if (type === "blocks") {
510
- return "blockedBy";
511
- }
512
- return type;
513
- }
514
- function buildRelatedIssueSummary(peer) {
515
- const state = peer.state;
516
- const assignee = peer.assignee;
517
- const team = peer.team;
518
- return {
519
- id: peer.id,
520
- identifier: peer.identifier,
521
- title: peer.title,
522
- ...(state
523
- ? { state: { id: state.id, name: state.name } }
524
- : {}),
525
- ...(peer.priority == null ? {} : { priority: peer.priority }),
526
- ...(assignee
527
- ? {
528
- assignee: {
529
- id: assignee.id,
530
- name: assignee.name,
531
- },
532
- }
533
- : {}),
534
- ...(team
535
- ? {
536
- team: {
537
- id: team.id,
538
- key: team.key,
539
- name: team.name,
540
- },
541
- }
542
- : {}),
543
- };
544
- }
545
- function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
546
- const entries = [];
547
- for (const rel of nodes ?? []) {
548
- const peer = rel[peerKey];
549
- if (!peer) {
550
- continue;
551
- }
552
- entries.push({
553
- id: rel.id,
554
- type: normalizeType(rel.type),
555
- direction,
556
- issue: buildRelatedIssueSummary(peer),
557
- });
558
- }
559
- return entries;
560
- }
561
528
  async function handleRelatedIssues(issueId, _options, command) {
562
529
  const rootOpts = getRootOpts(command);
563
530
  const graphQLService = await createGraphQLService(rootOpts);
564
531
  const linearService = await createLinearService(rootOpts);
565
532
  const resolvedId = await linearService.resolveIssueId(issueId);
566
- const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
567
- id: resolvedId,
568
- });
533
+ const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
569
534
  if (!result.issue) {
570
535
  throw new Error(`Issue "${issueId}" not found`);
571
536
  }
572
537
  const issue = result.issue;
573
- const relations = issue.relations;
574
- const inverseRelations = issue.inverseRelations;
575
538
  const entries = [
576
- ...buildRelationEntries(relations?.nodes, "relatedIssue", "outgoing", (raw) => raw),
577
- ...buildRelationEntries(inverseRelations?.nodes, "issue", "incoming", normalizeInverseType),
539
+ ...buildOutgoingRelationEntries(issue.relations.nodes),
540
+ ...buildIncomingRelationEntries(issue.inverseRelations.nodes),
578
541
  ];
579
542
  outputSuccess({
580
543
  id: issue.id,
@@ -584,157 +547,52 @@ async function handleRelatedIssues(issueId, _options, command) {
584
547
  meta: { count: entries.length },
585
548
  });
586
549
  }
587
- async function fetchCommentBodies(issueUuid, graphQLService) {
588
- const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
589
- issueId: issueUuid,
590
- first: 250,
591
- });
592
- const issue = result.issue;
593
- const comments = issue?.comments;
594
- const nodes = comments?.nodes ?? [];
595
- return nodes
596
- .map((c) => c.body)
597
- .filter((b) => typeof b === "string" && b.length > 0);
598
- }
599
- async function linkReferencesForIssue(args) {
600
- const comments = args.includeComments
601
- ? await fetchCommentBodies(args.issueUuid, args.graphQLService)
602
- : undefined;
603
- return autoLinkReferences({
604
- issueId: args.issueUuid,
605
- identifier: args.identifier,
606
- description: args.description,
607
- comments,
608
- dryRun: args.dryRun,
609
- graphQLService: args.graphQLService,
610
- linearService: args.linearService,
611
- preResolved: args.preResolved,
612
- });
613
- }
614
- async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
615
- const resolvedId = await linearService.resolveIssueId(issueId);
616
- // GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
617
- // we need for the description-only path. Comments are fetched separately on demand.
618
- const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
619
- id: resolvedId,
620
- });
621
- if (!issueResult.issue) {
622
- throw new Error(`Issue "${issueId}" not found`);
623
- }
624
- const issue = issueResult.issue;
625
- const identifier = issue.identifier;
626
- const description = issue.description ?? "";
627
- const dryRun = Boolean(options.dryRun);
628
- const includeComments = Boolean(options.includeComments);
629
- const rewriteDescription = Boolean(options.rewriteDescription);
630
- // If rewriting, validate refs once up front so we can both wrap the description and
631
- // pass the resolved map down to autoLink — avoids a second resolve round trip.
632
- const rewrite = rewriteDescription
633
- ? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
634
- : { preResolved: undefined, wrapped: undefined };
635
- const autoLinked = await linkReferencesForIssue({
636
- issueUuid: resolvedId,
637
- identifier,
638
- description,
639
- includeComments,
640
- dryRun,
641
- graphQLService,
642
- linearService,
643
- preResolved: rewrite.preResolved,
644
- });
645
- let descriptionRewritten = false;
646
- if (rewriteDescription && rewrite.wrapped && !dryRun) {
647
- await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
648
- descriptionRewritten = true;
649
- }
550
+ async function handleStartIssue(issueId, _options, command) {
551
+ const rootOpts = getRootOpts(command);
552
+ const { issuesService } = await createIssuesService(rootOpts);
553
+ const result = await issuesService.startIssue(issueId);
650
554
  outputSuccess({
651
- id: resolvedId,
652
- identifier,
653
- title: issue.title,
654
- autoLinked,
655
- ...(rewriteDescription
656
- ? {
657
- descriptionRewritten,
658
- ...(dryRun && rewrite.wrapped
659
- ? { descriptionPreview: rewrite.wrapped }
660
- : {}),
661
- }
662
- : {}),
663
- meta: {
664
- dryRun,
665
- includeComments,
666
- ...(rewriteDescription ? { rewriteDescription: true } : {}),
667
- },
555
+ ...result.issue,
556
+ started: result.started,
557
+ ...(result.previousState ? { previousState: result.previousState } : {}),
558
+ ...(result.targetState ? { targetState: result.targetState } : {}),
668
559
  });
669
560
  }
670
- async function handleLinkReferencesBatch(teamInput, options, graphQLService, linearService) {
671
- const teamId = resolveTeam(teamInput);
672
- const limit = options.limit
673
- ? Number.parseInt(options.limit, 10)
674
- : 100;
675
- if (!Number.isFinite(limit) || limit <= 0) {
676
- throw new Error(`--limit must be a positive integer, got: ${options.limit}`);
677
- }
678
- const dryRun = Boolean(options.dryRun);
679
- const includeComments = Boolean(options.includeComments);
680
- const scan = await graphQLService.rawRequest(SCAN_ISSUES_QUERY, {
681
- filter: { team: { id: { eq: teamId } } },
682
- first: limit,
683
- });
684
- const issues = scan.issues?.nodes;
685
- const nodes = issues ?? [];
686
- const entries = [];
687
- for (const node of nodes) {
688
- const issueUuid = node.id;
689
- const identifier = node.identifier;
690
- const description = node.description ?? "";
691
- const autoLinked = await linkReferencesForIssue({
692
- issueUuid,
693
- identifier,
694
- description,
695
- includeComments,
696
- dryRun,
697
- graphQLService,
698
- linearService,
699
- });
700
- entries.push({ identifier, autoLinked });
701
- }
702
- // Summary counts across the batch
703
- const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
704
- const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
705
- const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
706
- outputSuccess({
707
- data: entries,
708
- meta: {
709
- count: entries.length,
710
- team: teamInput,
711
- dryRun,
712
- includeComments,
713
- totals: {
714
- linked: totalLinked,
715
- skipped: totalSkipped,
716
- failed: totalFailed,
717
- },
718
- },
719
- });
561
+ // Field-mutating options on `issues update`, post alias-normalization.
562
+ // If none of these are set, an `issues update` invocation that carries only
563
+ // relation flags is treated as relation-only (see handleUpdateIssue): it must
564
+ // behave exactly like `issues relate` and NOT issue an issueUpdate mutation,
565
+ // which would otherwise apply the config defaultPriority fallback and bump the
566
+ // issue's activity for what the user intended as a pure relation add.
567
+ const UPDATE_FIELD_OPTION_KEYS = [
568
+ "title",
569
+ "description",
570
+ "appendDescription",
571
+ "status",
572
+ "priority",
573
+ "assignee",
574
+ "delegate",
575
+ "clearDelegate",
576
+ "project",
577
+ "labels",
578
+ "clearLabels",
579
+ "parentTicket",
580
+ "clearParentTicket",
581
+ "projectMilestone",
582
+ "clearProjectMilestone",
583
+ "cycle",
584
+ "clearCycle",
585
+ "dueDate",
586
+ "clearDueDate",
587
+ ];
588
+ function hasFieldUpdates(options) {
589
+ return UPDATE_FIELD_OPTION_KEYS.some((key) => options[key] !== undefined && options[key] !== false);
720
590
  }
721
- async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
722
- const rootOpts = getRootOpts(command);
723
- const graphQLService = await createGraphQLService(rootOpts);
724
- const linearService = await createLinearService(rootOpts);
725
- if (options.team) {
726
- if (issueIdOrTeamFlag) {
727
- throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
728
- }
729
- if (options.rewriteDescription) {
730
- throw new Error("--rewrite-description is single-issue only. Use it case by case (one issueId per invocation) — bulk description rewrites bot-author the whole team's history.");
731
- }
732
- return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
733
- }
734
- if (!issueIdOrTeamFlag) {
735
- throw new Error("issueId is required (or use --team <key> to scan a whole team)");
736
- }
737
- return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
591
+ function hasRelationFlags(options) {
592
+ return Boolean(options.relatedTo ||
593
+ options.blocks ||
594
+ options.blockedBy ||
595
+ options.duplicateOf);
738
596
  }
739
597
  async function handleUpdateIssue(issueId, options, command) {
740
598
  // Normalize aliases
@@ -744,19 +602,37 @@ async function handleUpdateIssue(issueId, options, command) {
744
602
  if (options.label && !options.labels) {
745
603
  options.labels = options.label;
746
604
  }
605
+ if (options.parent && !options.parentTicket) {
606
+ options.parentTicket = options.parent;
607
+ }
747
608
  // Resolve --description-file before validation
748
609
  if (options.descriptionFile) {
749
610
  options.description = readDescriptionFile(options.descriptionFile);
750
611
  }
751
612
  validateUpdateOptions(options);
752
613
  const rootOpts = getRootOpts(command);
753
- const graphQLService = await createGraphQLService(rootOpts);
754
- const linearService = await createLinearService(rootOpts);
614
+ const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
615
+ // Relation-only update: no field changes, just relation flags. Behave
616
+ // identically to `issues relate` — never call updateIssue, so no
617
+ // defaultPriority side effect and no spurious issue activity.
618
+ if (!hasFieldUpdates(options) && hasRelationFlags(options)) {
619
+ const sourceId = await linearService.resolveIssueId(issueId);
620
+ const relations = await createRelations(sourceId, options, graphQLService, linearService);
621
+ if (relations.length === 0) {
622
+ throw new Error("Specify at least one of: --related-to, --blocks, --blocked-by, --duplicate-of");
623
+ }
624
+ outputSuccess({
625
+ data: relations,
626
+ meta: { count: relations.length, source: issueId },
627
+ });
628
+ return;
629
+ }
755
630
  if (options.appendDescription) {
756
631
  const resolved = await linearService.resolveIssueId(issueId);
757
- const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", { id: resolved });
758
- const issue = current.issue;
759
- const existing = issue?.description ?? "";
632
+ const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
633
+ id: resolved,
634
+ });
635
+ const existing = current.issue?.description ?? "";
760
636
  options.description = `${existing}\n${options.appendDescription}`;
761
637
  }
762
638
  if (options.title || options.description) {
@@ -780,12 +656,24 @@ async function handleUpdateIssue(issueId, options, command) {
780
656
  if (prepared.description !== undefined) {
781
657
  options.description = prepared.description;
782
658
  }
783
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
784
659
  const assigneeId = options.assignee
785
660
  ? await resolveAssignee(options.assignee, rootOpts)
786
661
  : undefined;
787
- const updateArgs = buildUpdateArgs(issueId, options, assigneeId);
788
- const result = await issuesService.updateIssue(updateArgs, options.labelBy || "adding");
662
+ const delegateId = options.clearDelegate
663
+ ? null
664
+ : options.delegate
665
+ ? resolveMember(options.delegate)
666
+ : undefined;
667
+ const updateArgs = buildUpdateArgs(issueId, options, assigneeId, delegateId);
668
+ const result = await withProjectResolverEnrichment(() => issuesService.updateIssue(updateArgs, options.labelBy || "adding"), {
669
+ team: options.team ?? loadConfig().defaultTeam,
670
+ title: options.title,
671
+ }, { graphQLService, linearService });
672
+ // Mirror `issues create` / `issues relate`: the --related-to / --blocks /
673
+ // --blocked-by / --duplicate-of flags create sidebar relations on the
674
+ // updated issue. Without this, callers wanting to add a relation to an
675
+ // existing issue have to make a second `issues relate` call.
676
+ const relations = await createRelations(result.id, options, graphQLService, linearService);
789
677
  const autoLinked = originalDescription
790
678
  ? await maybeAutoLink({
791
679
  issueId: result.id,
@@ -797,7 +685,35 @@ async function handleUpdateIssue(issueId, options, command) {
797
685
  linearService,
798
686
  })
799
687
  : undefined;
800
- outputSuccess(autoLinked ? { ...result, autoLinked } : result);
688
+ outputSuccess({
689
+ ...result,
690
+ ...(relations.length > 0 ? { relations } : {}),
691
+ ...(autoLinked ? { autoLinked } : {}),
692
+ });
693
+ }
694
+ async function handleArchiveIssue(issueId, _options, command) {
695
+ const rootOpts = getRootOpts(command);
696
+ const { issuesService } = await createIssuesService(rootOpts);
697
+ const result = await issuesService.archiveIssue(issueId);
698
+ outputSuccess({
699
+ success: true,
700
+ archived: true,
701
+ ...result,
702
+ });
703
+ }
704
+ async function handleDeleteIssue(issueId, options, command) {
705
+ const rootOpts = getRootOpts(command);
706
+ const { issuesService } = await createIssuesService(rootOpts);
707
+ const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
708
+ const result = await issuesService.deleteIssue(issueId, {
709
+ permanentlyDelete,
710
+ });
711
+ outputSuccess({
712
+ success: true,
713
+ deleted: true,
714
+ permanentlyDeleted: permanentlyDelete,
715
+ ...result,
716
+ });
801
717
  }
802
718
  async function handleRetrolink(options, command) {
803
719
  // 1. Read current branch
@@ -839,9 +755,7 @@ async function handleRetrolink(options, command) {
839
755
  ? commits.map((msg) => `- ${msg}`).join("\n")
840
756
  : `Retrolinked from branch: ${currentBranch}`;
841
757
  const rootOpts = getRootOpts(command);
842
- const graphQLService = await createGraphQLService(rootOpts);
843
- const linearService = await createLinearService(rootOpts);
844
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
758
+ const { issuesService } = await createIssuesService(rootOpts);
845
759
  let result;
846
760
  if (options.issue) {
847
761
  // Link to existing issue — fetch it and rename branch
@@ -867,7 +781,13 @@ async function handleRetrolink(options, command) {
867
781
  let newBranch;
868
782
  if (result.branchName) {
869
783
  newBranch = toBranchName(result.branchName);
870
- execFileSync("git", ["branch", "-m", oldBranch, newBranch], {
784
+ // `--` BEFORE the ref operands terminates option parsing so a
785
+ // name starting with `-` (server bug, malicious slug, forked
786
+ // Linear-compatible API) is parsed as a ref name rather than a
787
+ // git flag. Defense in depth — the default feature/ prefix
788
+ // already blocks the common case, but the prefix is configurable
789
+ // (DEV-4064).
790
+ execFileSync("git", ["branch", "-m", "--", oldBranch, newBranch], {
871
791
  stdio: "pipe",
872
792
  });
873
793
  }
@@ -889,6 +809,7 @@ export function setupIssuesCommands(program) {
889
809
  .option("-l, --limit <number>", "limit results", "25")
890
810
  .option("--team <team>", "filter by team key (EL: resolves names)")
891
811
  .option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
812
+ .option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
892
813
  .option("--project <project>", "filter by project name or ID")
893
814
  .option("--no-project", "filter issues with no project assigned")
894
815
  .option("--labels <labels>", "filter by labels (comma-separated names)")
@@ -897,13 +818,14 @@ export function setupIssuesCommands(program) {
897
818
  .option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
898
819
  .option("--sort <field>", "sort results (priority, status, created, updated)")
899
820
  .option("--format <format>", "output format (json, summary, table, md, csv)", "json")
900
- .option("--fields <fields>", "columns for table/csv (comma-separated: identifier,title,status,priority,assignee,project,team,labels,updated)")
821
+ .option("--fields <fields>", "columns for table/csv (comma-separated: identifier,title,status,priority,assignee,delegate,project,team,labels,updated)")
901
822
  .action(handleAsyncCommand(handleListIssues));
902
823
  issues
903
824
  .command("search <query>")
904
825
  .description("Search issues.")
905
826
  .option("--team <team>", "filter by team key, name, or ID")
906
827
  .option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
828
+ .option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
907
829
  .option("--project <project>", "filter by project name or ID")
908
830
  .option("--no-project", "filter issues with no project assigned")
909
831
  .option("--status <status>", "filter by status (comma-separated)")
@@ -924,6 +846,7 @@ export function setupIssuesCommands(program) {
924
846
  .option("--template <name>", "use a named description template from config.descriptionTemplates")
925
847
  .option("--from-template <id>", "instantiate the issue from a Linear server-side template (UUID from `el-linear templates list`). Sets templateId on the underlying issueCreate mutation; Linear copies the template's title, description, labels, priority, etc. as the new issue's defaults. Override any field with the matching --title / --description / --labels flag.")
926
848
  .option("-a, --assignee <assignee>", "assign to user (name, alias, or UUID)")
849
+ .option("--delegate <delegate>", "delegate implementation to an agent app user (name, alias, or UUID)")
927
850
  .option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
928
851
  .option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
929
852
  .option("--project <project>", "add to project (name or ID)")
@@ -935,6 +858,7 @@ export function setupIssuesCommands(program) {
935
858
  .option("--cycle <cycle>", "cycle name or ID")
936
859
  .option("--status <status>", "status name or ID")
937
860
  .option("--parent-ticket <parentId>", "parent issue ID or identifier")
861
+ .option("--parent <parentId>", "alias for --parent-ticket")
938
862
  .option("--subscriber <subscribers>", "subscribers (comma-separated names, aliases, or UUIDs)")
939
863
  .option("--strict", "strict brand validation (error instead of warning)")
940
864
  .option("--related-to <issues>", "related issues (comma-separated identifiers)")
@@ -952,6 +876,10 @@ export function setupIssuesCommands(program) {
952
876
  if (options.label && !options.labels) {
953
877
  options.labels = options.label;
954
878
  }
879
+ // Normalize --parent alias to --parent-ticket
880
+ if (options.parent && !options.parentTicket) {
881
+ options.parentTicket = options.parent;
882
+ }
955
883
  const title = options.title || titleArg;
956
884
  // --from-template lets Linear copy the template's title, so a
957
885
  // local title is optional in that path. Without --from-template,
@@ -968,7 +896,10 @@ export function setupIssuesCommands(program) {
968
896
  .alias("get")
969
897
  .alias("show")
970
898
  .description("Get issue details. Accepts multiple IDs for batch retrieval.")
971
- .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.\nMultiple IDs: el-linear issue get DEV-123 DEV-456 DEV-789")
899
+ .option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
900
+ "Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
901
+ "Outputs the section text only — no JSON envelope. Single-issue only.")
902
+ .addHelpText("after", '\nBoth UUID and identifiers like ABC-123 are supported.\nMultiple IDs: el-linear issue get DEV-123 DEV-456 DEV-789\nExtract a section: el-linear issue read DEV-123 --field "Done when"')
972
903
  .action(handleAsyncCommand(readIssues));
973
904
  issues
974
905
  .command("update <issueId>")
@@ -984,12 +915,15 @@ export function setupIssuesCommands(program) {
984
915
  .option("--state <status>", "alias for --status (new status name or ID)")
985
916
  .option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
986
917
  .option("--assignee <assignee>", "new assignee (name, alias, or UUID)")
918
+ .option("--delegate <delegate>", "delegate implementation to an agent app user (name, alias, or UUID)")
919
+ .option("--clear-delegate", "remove the issue delegate")
987
920
  .option("--project <project>", "new project (name or ID)")
988
921
  .option("--labels <labels>", "labels (comma-separated names or IDs)")
989
922
  .option("--label <labels>", "alias for --labels")
990
923
  .option("--label-by <mode>", "how to apply labels: 'adding' (default) or 'overwriting'")
991
924
  .option("--clear-labels", "remove all labels from issue")
992
925
  .option("--parent-ticket <parentId>", "set parent issue")
926
+ .option("--parent <parentId>", "alias for --parent-ticket")
993
927
  .option("--clear-parent-ticket", "clear parent relationship")
994
928
  .option("--project-milestone <milestone>", "set project milestone")
995
929
  .option("--clear-project-milestone", "clear project milestone")
@@ -999,12 +933,38 @@ export function setupIssuesCommands(program) {
999
933
  .option("--clear-due-date", "clear due date")
1000
934
  .option("--strict", "strict brand validation")
1001
935
  .option("--no-auto-link", "skip auto-linking issue references found in the description")
936
+ .option("--related-to <issues>", "related issues (comma-separated)")
937
+ .option("--blocks <issues>", "issues this blocks (comma-separated)")
938
+ .option("--blocked-by <issues>", "issues blocking this (comma-separated)")
939
+ .option("--duplicate-of <issue>", "mark as duplicate of another issue")
1002
940
  .action(handleAsyncCommand(handleUpdateIssue));
941
+ issues
942
+ .command("archive <issueId>")
943
+ .description("Archive an issue.")
944
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
945
+ .action(handleAsyncCommand(handleArchiveIssue));
946
+ issues
947
+ .command("delete <issueId>")
948
+ .description("Delete (trash) an issue.")
949
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported. Use --permanently-delete only when you intend to skip Linear's grace period; Linear requires admin permissions.")
950
+ .option("--permanently-delete", "skip Linear's 30-day grace period (admin only)")
951
+ .option("--hard", "alias for --permanently-delete")
952
+ .action(handleAsyncCommand(handleDeleteIssue));
953
+ issues
954
+ .command("hard-delete <issueId>")
955
+ .description("Permanently delete an issue immediately (admin only).")
956
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
957
+ .action(handleAsyncCommand((issueId, options, command) => handleDeleteIssue(issueId, { ...options, permanentlyDelete: true }, command)));
1003
958
  issues
1004
959
  .command("history <issueId>")
1005
960
  .description("Show issue state transition history (time in each status).")
1006
961
  .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
1007
962
  .action(handleAsyncCommand(handleHistoryIssue));
963
+ issues
964
+ .command("start <issueId>")
965
+ .description("Move an issue to the team's first started workflow state, unless it is already started or terminal.")
966
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
967
+ .action(handleAsyncCommand(handleStartIssue));
1008
968
  issues
1009
969
  .command("relate <issueId>")
1010
970
  .description("Create issue relations.")