@enrichlayer/el-linear 1.10.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 (106) hide show
  1. package/README.md +126 -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/batch.js +18 -21
  20. package/dist/commands/comments.js +5 -5
  21. package/dist/commands/config.js +178 -5
  22. package/dist/commands/init/aliases.js +1 -1
  23. package/dist/commands/init/defaults.d.ts +2 -1
  24. package/dist/commands/init/index.js +45 -35
  25. package/dist/commands/init/oauth.d.ts +4 -1
  26. package/dist/commands/init/oauth.js +22 -4
  27. package/dist/commands/init/shared.d.ts +24 -2
  28. package/dist/commands/init/shared.js +35 -4
  29. package/dist/commands/init/token.d.ts +3 -3
  30. package/dist/commands/init/token.js +5 -24
  31. package/dist/commands/init/workspace.d.ts +2 -1
  32. package/dist/commands/init/workspace.js +1 -1
  33. package/dist/commands/introspect.d.ts +27 -0
  34. package/dist/commands/introspect.js +178 -0
  35. package/dist/commands/issues/branch.js +9 -1
  36. package/dist/commands/issues/relations.d.ts +3 -14
  37. package/dist/commands/issues/relations.js +3 -3
  38. package/dist/commands/issues.js +222 -43
  39. package/dist/commands/labels.js +2 -1
  40. package/dist/commands/profile.js +1 -0
  41. package/dist/commands/projects.d.ts +2 -0
  42. package/dist/commands/projects.js +91 -7
  43. package/dist/commands/read-shortcut.d.ts +1 -1
  44. package/dist/commands/read-shortcut.js +28 -8
  45. package/dist/commands/refs.js +67 -8
  46. package/dist/commands/search.js +30 -5
  47. package/dist/commands/users.js +4 -2
  48. package/dist/config/config.d.ts +99 -1
  49. package/dist/config/config.js +264 -52
  50. package/dist/config/error-enrichment.d.ts +62 -0
  51. package/dist/config/error-enrichment.js +417 -0
  52. package/dist/config/issue-validation.d.ts +37 -0
  53. package/dist/config/issue-validation.js +63 -1
  54. package/dist/config/paths.d.ts +2 -8
  55. package/dist/config/paths.js +4 -2
  56. package/dist/config/resolver.d.ts +8 -1
  57. package/dist/config/resolver.js +9 -2
  58. package/dist/main.js +13 -1
  59. package/dist/queries/comments-types.d.ts +15 -9
  60. package/dist/queries/common.d.ts +2 -2
  61. package/dist/queries/common.js +8 -0
  62. package/dist/queries/documents-types.d.ts +4 -3
  63. package/dist/queries/introspect-types.d.ts +8 -7
  64. package/dist/queries/issues-types.d.ts +92 -27
  65. package/dist/queries/issues.d.ts +49 -10
  66. package/dist/queries/issues.js +125 -5
  67. package/dist/queries/labels-types.d.ts +7 -6
  68. package/dist/queries/project-milestones-types.d.ts +5 -4
  69. package/dist/queries/project-milestones.d.ts +1 -1
  70. package/dist/queries/projects-types.d.ts +8 -7
  71. package/dist/queries/releases-types.d.ts +5 -4
  72. package/dist/queries/search-types.d.ts +28 -12
  73. package/dist/queries/templates-types.d.ts +3 -2
  74. package/dist/types/linear.d.ts +13 -1
  75. package/dist/utils/auto-link-references.d.ts +3 -3
  76. package/dist/utils/auto-link-references.js +1 -10
  77. package/dist/utils/extract-field.d.ts +19 -0
  78. package/dist/utils/extract-field.js +99 -0
  79. package/dist/utils/file-service.d.ts +6 -13
  80. package/dist/utils/file-service.js +0 -2
  81. package/dist/utils/formatters/summary.js +6 -1
  82. package/dist/utils/graphql-issues-service.d.ts +101 -45
  83. package/dist/utils/graphql-issues-service.js +252 -39
  84. package/dist/utils/graphql-service.d.ts +10 -12
  85. package/dist/utils/graphql-service.js +0 -3
  86. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  87. package/dist/utils/issue-reference-extractor.js +5 -3
  88. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  89. package/dist/utils/issues-service-bootstrap.js +27 -0
  90. package/dist/utils/linear-service.d.ts +21 -14
  91. package/dist/utils/linear-service.js +73 -11
  92. package/dist/utils/markdown-prosemirror.js +12 -12
  93. package/dist/utils/mention-resolver.js +1 -1
  94. package/dist/utils/output.d.ts +81 -3
  95. package/dist/utils/output.js +61 -6
  96. package/dist/utils/project-slug.d.ts +21 -0
  97. package/dist/utils/project-slug.js +45 -0
  98. package/dist/utils/protected-ranges.d.ts +14 -0
  99. package/dist/utils/protected-ranges.js +88 -2
  100. package/dist/utils/sanitize-for-log.d.ts +24 -0
  101. package/dist/utils/sanitize-for-log.js +38 -0
  102. package/dist/utils/table-formatter.js +24 -0
  103. package/dist/utils/validators.d.ts +7 -2
  104. package/dist/utils/validators.js +6 -0
  105. package/dist/utils/workspace-url.js +20 -4
  106. package/package.json +2 -2
@@ -1,5 +1,6 @@
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";
@@ -8,11 +9,11 @@ import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../qu
8
9
  import { createFileService } from "../utils/file-service.js";
9
10
  import { applyFooter } from "../utils/footer.js";
10
11
  import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
11
- import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
12
- import { createGraphQLService } from "../utils/graphql-service.js";
13
- import { createLinearService } from "../utils/linear-service.js";
12
+ import { createGraphQLService, } from "../utils/graphql-service.js";
13
+ import { createIssuesService } from "../utils/issues-service-bootstrap.js";
14
+ import { createLinearService, } from "../utils/linear-service.js";
14
15
  import { logger } from "../utils/logger.js";
15
- import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
16
+ import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
16
17
  import { getRootOpts } from "../utils/root-opts.js";
17
18
  import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
18
19
  import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
@@ -36,6 +37,9 @@ function isImageFile(filename) {
36
37
  return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
37
38
  }
38
39
  function validateUpdateOptions(options) {
40
+ if (options.delegate && options.clearDelegate) {
41
+ throw new Error("Cannot use --delegate and --clear-delegate together");
42
+ }
39
43
  if (options.parentTicket && options.clearParentTicket) {
40
44
  throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
41
45
  }
@@ -61,7 +65,7 @@ function validateUpdateOptions(options) {
61
65
  throw new Error("Cannot use --description and --append-description together");
62
66
  }
63
67
  }
64
- function buildUpdateArgs(issueId, options, assigneeId) {
68
+ function buildUpdateArgs(issueId, options, assigneeId, delegateId) {
65
69
  let labelIds;
66
70
  if (options.clearLabels) {
67
71
  labelIds = [];
@@ -84,6 +88,7 @@ function buildUpdateArgs(issueId, options, assigneeId) {
84
88
  statusId: options.status,
85
89
  priority: priorityInput ? validatePriority(priorityInput) : undefined,
86
90
  assigneeId,
91
+ delegateId,
87
92
  projectId: options.project,
88
93
  labelIds,
89
94
  parentId: options.parentTicket || (options.clearParentTicket ? null : undefined),
@@ -136,44 +141,61 @@ function sortIssues(issues, sort) {
136
141
  }
137
142
  return sorted;
138
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
+ }
139
157
  async function handleListIssues(options, command) {
140
158
  if (options.label && !options.labels) {
141
159
  options.labels = options.label;
142
160
  }
143
161
  const rootOpts = getRootOpts(command);
144
- const graphQLService = await createGraphQLService(rootOpts);
145
- const linearService = await createLinearService(rootOpts);
146
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
162
+ const { issuesService } = await createIssuesService(rootOpts);
147
163
  const hasFilters = options.team ||
148
164
  options.labels ||
149
165
  options.status ||
150
166
  options.assignee ||
167
+ options.delegate ||
151
168
  options.project ||
152
169
  options.project === false ||
153
170
  options.priority;
171
+ const limit = parsePositiveInt(options.limit, "--limit");
154
172
  if (hasFilters) {
155
173
  const searchArgs = {
156
174
  teamId: options.team ? resolveTeam(options.team) : undefined,
157
175
  assigneeId: options.assignee
158
176
  ? await resolveAssignee(options.assignee, rootOpts)
159
177
  : undefined,
160
- projectId: options.project || undefined,
161
- noProject: options.project === false,
178
+ delegateId: options.delegate
179
+ ? resolveMember(options.delegate)
180
+ : undefined,
181
+ project: resolveProjectFlag(options.project),
162
182
  labelNames: options.labels ? splitList(options.labels) : undefined,
163
183
  status: options.status ? splitList(options.status) : undefined,
164
184
  priority: options.priority
165
185
  ? parsePriorityFilter(options.priority)
166
186
  : undefined,
167
187
  orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
168
- limit: parsePositiveInt(options.limit, "--limit"),
188
+ limit,
169
189
  };
170
190
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
191
+ warnIfTruncated(result.length, limit);
171
192
  outputIssues(result, options.format, options.fields, {
172
193
  team: options.team,
173
194
  });
174
195
  }
175
196
  else {
176
- const result = sortIssues(await issuesService.getIssues(parsePositiveInt(options.limit, "--limit")), options.sort);
197
+ const result = sortIssues(await issuesService.getIssues(limit), options.sort);
198
+ warnIfTruncated(result.length, limit);
177
199
  outputIssues(result, options.format, options.fields, {});
178
200
  }
179
201
  }
@@ -182,27 +204,58 @@ async function handleSearchIssues(query, options, command) {
182
204
  options.labels = options.label;
183
205
  }
184
206
  const rootOpts = getRootOpts(command);
185
- const graphQLService = await createGraphQLService(rootOpts);
186
- const linearService = await createLinearService(rootOpts);
187
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
207
+ const { issuesService } = await createIssuesService(rootOpts);
208
+ const limit = parsePositiveInt(options.limit, "--limit");
188
209
  const searchArgs = {
189
210
  query,
190
211
  teamId: options.team ? resolveTeam(options.team) : undefined,
191
212
  assigneeId: options.assignee
192
213
  ? await resolveAssignee(options.assignee, rootOpts)
193
214
  : undefined,
194
- projectId: options.project || undefined,
195
- noProject: options.project === false,
215
+ delegateId: options.delegate
216
+ ? resolveMember(options.delegate)
217
+ : undefined,
218
+ project: resolveProjectFlag(options.project),
196
219
  status: options.status ? splitList(options.status) : undefined,
197
220
  labelNames: options.labels ? splitList(options.labels) : undefined,
198
221
  priority: options.priority
199
222
  ? parsePriorityFilter(options.priority)
200
223
  : undefined,
201
- limit: parsePositiveInt(options.limit, "--limit"),
224
+ limit,
202
225
  };
203
226
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
227
+ warnIfTruncated(result.length, limit);
204
228
  outputIssues(result, options.format, options.fields, { query });
205
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
+ }
206
259
  async function resolveCreateInputs(title, options, rootOpts) {
207
260
  const config = loadConfig();
208
261
  enforceTerms([title, options.description], { strict: options.strict });
@@ -244,6 +297,22 @@ async function resolveCreateInputs(title, options, rootOpts) {
244
297
  if (validationResult.normalizedLabels) {
245
298
  options.labels = validationResult.normalizedLabels.join(",");
246
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
+ }
247
316
  enforceValidation(validationResult);
248
317
  }
249
318
  if (!effectivePriorityInput) {
@@ -254,9 +323,16 @@ async function resolveCreateInputs(title, options, rootOpts) {
254
323
  const assigneeId = effectiveAssignee
255
324
  ? await resolveAssignee(effectiveAssignee, rootOpts)
256
325
  : undefined;
326
+ const delegateId = options.delegate
327
+ ? resolveMember(options.delegate)
328
+ : undefined;
257
329
  let labelIds = [];
258
330
  if (options.labels) {
259
- 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));
260
336
  }
261
337
  if (options.claude) {
262
338
  const claudeId = config.labels.workspace.claude;
@@ -280,6 +356,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
280
356
  teamInput,
281
357
  teamId,
282
358
  assigneeId,
359
+ delegateId,
283
360
  labelIds,
284
361
  status,
285
362
  subscriberIds,
@@ -318,7 +395,7 @@ function buildDescriptionWithAttachments(baseDescription, uploadResults) {
318
395
  }
319
396
  async function handleCreateIssue(title, options, command) {
320
397
  const rootOpts = getRootOpts(command);
321
- 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);
322
399
  const uploadResults = await uploadAttachmentsIfNeeded(options, rootOpts);
323
400
  const descriptionWithAttachments = buildDescriptionWithAttachments(resolveDescription(options) || "", uploadResults);
324
401
  // Append messageFooter (config or --footer flag) so auto-link picks up any
@@ -330,14 +407,12 @@ async function handleCreateIssue(title, options, command) {
330
407
  footer: explicitFooter,
331
408
  noFooter,
332
409
  }) ?? "";
333
- const graphQLService = await createGraphQLService(rootOpts);
334
- const linearService = await createLinearService(rootOpts);
335
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
410
+ const { graphQLService, linearService, issuesService } = await createIssuesService(rootOpts);
336
411
  // Wrap valid issue identifiers as markdown links before creating, so the description
337
412
  // saved on Linear has clickable refs from the start. Self-reference can't apply here
338
413
  // because the issue doesn't exist yet — pass undefined.
339
414
  const prepared = await prepareAutoLinkedDescription(description || undefined, options, undefined, linearService, graphQLService);
340
- const result = await issuesService.createIssue({
415
+ const result = await withProjectResolverEnrichment(() => issuesService.createIssue({
341
416
  // title is omitted when --from-template is set without an override,
342
417
  // so Linear copies the template's title server-side.
343
418
  ...(title ? { title } : {}),
@@ -345,6 +420,7 @@ async function handleCreateIssue(title, options, command) {
345
420
  teamInput,
346
421
  description: prepared.description,
347
422
  assigneeId,
423
+ delegateId,
348
424
  priority,
349
425
  projectId: options.project,
350
426
  statusId: status,
@@ -360,7 +436,7 @@ async function handleCreateIssue(title, options, command) {
360
436
  ...(typeof options.fromTemplate === "string" && options.fromTemplate
361
437
  ? { templateId: options.fromTemplate }
362
438
  : {}),
363
- });
439
+ }), { team: options.team ?? loadConfig().defaultTeam, title }, { graphQLService, linearService });
364
440
  const relations = await createRelations(result.id, options, graphQLService, linearService);
365
441
  // Pass the ORIGINAL description (pre-wrap) so the extractor's prose-keyword inference
366
442
  // ("blocked by", "duplicates", etc.) sees `keyword DEV-100` instead of `keyword [DEV-100](url)`.
@@ -471,6 +547,53 @@ async function handleRelatedIssues(issueId, _options, command) {
471
547
  meta: { count: entries.length },
472
548
  });
473
549
  }
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);
554
+ outputSuccess({
555
+ ...result.issue,
556
+ started: result.started,
557
+ ...(result.previousState ? { previousState: result.previousState } : {}),
558
+ ...(result.targetState ? { targetState: result.targetState } : {}),
559
+ });
560
+ }
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);
590
+ }
591
+ function hasRelationFlags(options) {
592
+ return Boolean(options.relatedTo ||
593
+ options.blocks ||
594
+ options.blockedBy ||
595
+ options.duplicateOf);
596
+ }
474
597
  async function handleUpdateIssue(issueId, options, command) {
475
598
  // Normalize aliases
476
599
  if (options.state && !options.status) {
@@ -479,14 +602,31 @@ async function handleUpdateIssue(issueId, options, command) {
479
602
  if (options.label && !options.labels) {
480
603
  options.labels = options.label;
481
604
  }
605
+ if (options.parent && !options.parentTicket) {
606
+ options.parentTicket = options.parent;
607
+ }
482
608
  // Resolve --description-file before validation
483
609
  if (options.descriptionFile) {
484
610
  options.description = readDescriptionFile(options.descriptionFile);
485
611
  }
486
612
  validateUpdateOptions(options);
487
613
  const rootOpts = getRootOpts(command);
488
- const graphQLService = await createGraphQLService(rootOpts);
489
- 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
+ }
490
630
  if (options.appendDescription) {
491
631
  const resolved = await linearService.resolveIssueId(issueId);
492
632
  const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
@@ -516,12 +656,24 @@ async function handleUpdateIssue(issueId, options, command) {
516
656
  if (prepared.description !== undefined) {
517
657
  options.description = prepared.description;
518
658
  }
519
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
520
659
  const assigneeId = options.assignee
521
660
  ? await resolveAssignee(options.assignee, rootOpts)
522
661
  : undefined;
523
- const updateArgs = buildUpdateArgs(issueId, options, assigneeId);
524
- 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);
525
677
  const autoLinked = originalDescription
526
678
  ? await maybeAutoLink({
527
679
  issueId: result.id,
@@ -533,13 +685,15 @@ async function handleUpdateIssue(issueId, options, command) {
533
685
  linearService,
534
686
  })
535
687
  : undefined;
536
- outputSuccess(autoLinked ? { ...result, autoLinked } : result);
688
+ outputSuccess({
689
+ ...result,
690
+ ...(relations.length > 0 ? { relations } : {}),
691
+ ...(autoLinked ? { autoLinked } : {}),
692
+ });
537
693
  }
538
694
  async function handleArchiveIssue(issueId, _options, command) {
539
695
  const rootOpts = getRootOpts(command);
540
- const graphQLService = await createGraphQLService(rootOpts);
541
- const linearService = await createLinearService(rootOpts);
542
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
696
+ const { issuesService } = await createIssuesService(rootOpts);
543
697
  const result = await issuesService.archiveIssue(issueId);
544
698
  outputSuccess({
545
699
  success: true,
@@ -549,9 +703,7 @@ async function handleArchiveIssue(issueId, _options, command) {
549
703
  }
550
704
  async function handleDeleteIssue(issueId, options, command) {
551
705
  const rootOpts = getRootOpts(command);
552
- const graphQLService = await createGraphQLService(rootOpts);
553
- const linearService = await createLinearService(rootOpts);
554
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
706
+ const { issuesService } = await createIssuesService(rootOpts);
555
707
  const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
556
708
  const result = await issuesService.deleteIssue(issueId, {
557
709
  permanentlyDelete,
@@ -603,9 +755,7 @@ async function handleRetrolink(options, command) {
603
755
  ? commits.map((msg) => `- ${msg}`).join("\n")
604
756
  : `Retrolinked from branch: ${currentBranch}`;
605
757
  const rootOpts = getRootOpts(command);
606
- const graphQLService = await createGraphQLService(rootOpts);
607
- const linearService = await createLinearService(rootOpts);
608
- const issuesService = new GraphQLIssuesService(graphQLService, linearService);
758
+ const { issuesService } = await createIssuesService(rootOpts);
609
759
  let result;
610
760
  if (options.issue) {
611
761
  // Link to existing issue — fetch it and rename branch
@@ -631,7 +781,13 @@ async function handleRetrolink(options, command) {
631
781
  let newBranch;
632
782
  if (result.branchName) {
633
783
  newBranch = toBranchName(result.branchName);
634
- 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], {
635
791
  stdio: "pipe",
636
792
  });
637
793
  }
@@ -653,6 +809,7 @@ export function setupIssuesCommands(program) {
653
809
  .option("-l, --limit <number>", "limit results", "25")
654
810
  .option("--team <team>", "filter by team key (EL: resolves names)")
655
811
  .option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
812
+ .option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
656
813
  .option("--project <project>", "filter by project name or ID")
657
814
  .option("--no-project", "filter issues with no project assigned")
658
815
  .option("--labels <labels>", "filter by labels (comma-separated names)")
@@ -661,13 +818,14 @@ export function setupIssuesCommands(program) {
661
818
  .option("--priority <priority>", "filter by priority (comma-separated: urgent,high,medium,low,none or 0-4)")
662
819
  .option("--sort <field>", "sort results (priority, status, created, updated)")
663
820
  .option("--format <format>", "output format (json, summary, table, md, csv)", "json")
664
- .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)")
665
822
  .action(handleAsyncCommand(handleListIssues));
666
823
  issues
667
824
  .command("search <query>")
668
825
  .description("Search issues.")
669
826
  .option("--team <team>", "filter by team key, name, or ID")
670
827
  .option("--assignee <assignee>", "filter by assignee (name, alias, or ID)")
828
+ .option("--delegate <delegate>", "filter by delegated agent (name, alias, or ID)")
671
829
  .option("--project <project>", "filter by project name or ID")
672
830
  .option("--no-project", "filter issues with no project assigned")
673
831
  .option("--status <status>", "filter by status (comma-separated)")
@@ -688,6 +846,7 @@ export function setupIssuesCommands(program) {
688
846
  .option("--template <name>", "use a named description template from config.descriptionTemplates")
689
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.")
690
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)")
691
850
  .option("--no-assignee", "create unassigned even when config.defaultAssignee is set")
692
851
  .option("-p, --priority <priority>", "priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
693
852
  .option("--project <project>", "add to project (name or ID)")
@@ -699,6 +858,7 @@ export function setupIssuesCommands(program) {
699
858
  .option("--cycle <cycle>", "cycle name or ID")
700
859
  .option("--status <status>", "status name or ID")
701
860
  .option("--parent-ticket <parentId>", "parent issue ID or identifier")
861
+ .option("--parent <parentId>", "alias for --parent-ticket")
702
862
  .option("--subscriber <subscribers>", "subscribers (comma-separated names, aliases, or UUIDs)")
703
863
  .option("--strict", "strict brand validation (error instead of warning)")
704
864
  .option("--related-to <issues>", "related issues (comma-separated identifiers)")
@@ -716,6 +876,10 @@ export function setupIssuesCommands(program) {
716
876
  if (options.label && !options.labels) {
717
877
  options.labels = options.label;
718
878
  }
879
+ // Normalize --parent alias to --parent-ticket
880
+ if (options.parent && !options.parentTicket) {
881
+ options.parentTicket = options.parent;
882
+ }
719
883
  const title = options.title || titleArg;
720
884
  // --from-template lets Linear copy the template's title, so a
721
885
  // local title is optional in that path. Without --from-template,
@@ -732,7 +896,10 @@ export function setupIssuesCommands(program) {
732
896
  .alias("get")
733
897
  .alias("show")
734
898
  .description("Get issue details. Accepts multiple IDs for batch retrieval.")
735
- .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"')
736
903
  .action(handleAsyncCommand(readIssues));
737
904
  issues
738
905
  .command("update <issueId>")
@@ -748,12 +915,15 @@ export function setupIssuesCommands(program) {
748
915
  .option("--state <status>", "alias for --status (new status name or ID)")
749
916
  .option("-p, --priority <priority>", "new priority: name (none/urgent/high/medium/normal/low) or number (0-4)")
750
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")
751
920
  .option("--project <project>", "new project (name or ID)")
752
921
  .option("--labels <labels>", "labels (comma-separated names or IDs)")
753
922
  .option("--label <labels>", "alias for --labels")
754
923
  .option("--label-by <mode>", "how to apply labels: 'adding' (default) or 'overwriting'")
755
924
  .option("--clear-labels", "remove all labels from issue")
756
925
  .option("--parent-ticket <parentId>", "set parent issue")
926
+ .option("--parent <parentId>", "alias for --parent-ticket")
757
927
  .option("--clear-parent-ticket", "clear parent relationship")
758
928
  .option("--project-milestone <milestone>", "set project milestone")
759
929
  .option("--clear-project-milestone", "clear project milestone")
@@ -763,6 +933,10 @@ export function setupIssuesCommands(program) {
763
933
  .option("--clear-due-date", "clear due date")
764
934
  .option("--strict", "strict brand validation")
765
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")
766
940
  .action(handleAsyncCommand(handleUpdateIssue));
767
941
  issues
768
942
  .command("archive <issueId>")
@@ -786,6 +960,11 @@ export function setupIssuesCommands(program) {
786
960
  .description("Show issue state transition history (time in each status).")
787
961
  .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
788
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));
789
968
  issues
790
969
  .command("relate <issueId>")
791
970
  .description("Create issue relations.")
@@ -4,7 +4,7 @@ import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION,
4
4
  import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
5
5
  import { createGraphQLService } from "../utils/graphql-service.js";
6
6
  import { createLinearService } from "../utils/linear-service.js";
7
- import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { handleAsyncCommand, outputSuccess, warnIfTruncated, } from "../utils/output.js";
8
8
  import { getRootOpts } from "../utils/root-opts.js";
9
9
  import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
10
10
  async function handleCreateLabel(name, options, command) {
@@ -82,6 +82,7 @@ export function setupLabelsCommands(program) {
82
82
  const service = await createLinearService(rootOpts);
83
83
  return service.getLabels(teamFilter, limit, nameFilter);
84
84
  });
85
+ warnIfTruncated(result.labels.length, limit);
85
86
  outputSuccess({
86
87
  data: result.labels,
87
88
  meta: { count: result.labels.length },
@@ -48,6 +48,7 @@ export function setupProfileCommands(program) {
48
48
  data: {
49
49
  name: active.name ?? "<default>",
50
50
  configPath: active.configPath,
51
+ localConfigPath: active.localConfigPath,
51
52
  tokenPath: active.tokenPath,
52
53
  },
53
54
  });
@@ -1,4 +1,6 @@
1
1
  import type { Command, OptionValues } from "commander";
2
+ import type { LinearProject } from "../types/linear.js";
3
+ export declare function sortActiveFirst(projects: LinearProject[]): LinearProject[];
2
4
  export declare function resolveProjectStateFilter(options: OptionValues): {
3
5
  states?: string[];
4
6
  excludeStates?: string[];