Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ linear issue create --title "Fix bug" --description "Description here"
# Create and assign to yourself
linear issue create --assignee self

# Create and delegate to an agent user
linear issue create --delegate rowan

# Create with priority (1-4, where 1 is highest)
linear issue create --priority 1

Expand Down Expand Up @@ -157,6 +160,9 @@ update a specific issue:

```bash
linear issue update TEAM-123

# Delegate an existing issue to an agent user
linear issue update TEAM-123 --delegate rowan
```

#### other issue commands
Expand Down
41 changes: 38 additions & 3 deletions src/commands/issue/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getTeamKey,
getWorkflowStateByNameOrType,
getWorkflowStates,
lookupUser,
lookupUserId,
searchTeamsByKeySubstring,
selectOption,
Expand Down Expand Up @@ -455,6 +456,10 @@ export const createCommand = new Command()
"-a, --assignee <assignee:string>",
"Assign the issue to 'self' or someone (by username or name)",
)
.option(
"--delegate <delegate:string>",
"Delegate the issue to an agent user (by username or name)",
)
.option(
"--due-date <dueDate:string>",
"Due date of the issue",
Expand Down Expand Up @@ -515,6 +520,7 @@ export const createCommand = new Command()
{
start,
assignee,
delegate,
dueDate,
useDefaultTemplate,
parent: parentIdentifier,
Expand Down Expand Up @@ -559,7 +565,7 @@ export const createCommand = new Command()
}

// If no flags are provided (or only parent is provided), use interactive mode
const noFlagsProvided = !title && !assignee && !dueDate &&
const noFlagsProvided = !title && !assignee && !delegate && !dueDate &&
priority === undefined && estimate === undefined && !finalDescription &&
(!labels || labels.length === 0) &&
!team && !project && !state && !milestone && !cycle && !start
Expand Down Expand Up @@ -710,10 +716,38 @@ export const createCommand = new Command()
let assigneeId = undefined

if (assignee) {
assigneeId = await lookupUserId(assignee)
if (assigneeId == null) {
const resolvedAssignee = await lookupUser(assignee)
if (resolvedAssignee == null) {
throw new NotFoundError("User", assignee)
}
if (resolvedAssignee.app) {
throw new ValidationError(
`Cannot use --assignee with app user '${assignee}'`,
{
suggestion:
`Use --delegate ${assignee} to delegate the issue to an agent user.`,
},
)
}
assigneeId = resolvedAssignee.id
}

let delegateId: string | undefined
if (delegate) {
const resolvedDelegate = await lookupUser(delegate)
if (resolvedDelegate == null) {
throw new NotFoundError("User", delegate)
}
if (!resolvedDelegate.app) {
throw new ValidationError(
`Cannot use --delegate with human user '${delegate}'`,
{
suggestion:
`Use --assignee ${delegate} to assign the issue to a human user.`,
},
)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new validation that rejects using --delegate with a human user (!resolvedDelegate.app) isn’t covered by a snapshot test in this PR. Add a test case that passes --delegate with a non-app user and asserts the failure message/suggestion, similar to the new app-user rejection tests for --assignee.

Copilot uses AI. Check for mistakes.
}
delegateId = resolvedDelegate.id
}

const labelIds = []
Expand Down Expand Up @@ -802,6 +836,7 @@ export const createCommand = new Command()
const input = {
title,
assigneeId,
delegateId,
dueDate,
parentId,
priority,
Expand Down
40 changes: 37 additions & 3 deletions src/commands/issue/issue-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
getProjectIdByName,
getTeamIdByKey,
getWorkflowStateByNameOrType,
lookupUserId,
lookupUser,
} from "../../utils/linear.ts"
import {
CliError,
Expand All @@ -28,6 +28,10 @@ export const updateCommand = new Command()
"-a, --assignee <assignee:string>",
"Assign the issue to 'self' or someone (by username or name)",
)
.option(
"--delegate <delegate:string>",
"Delegate the issue to an agent user (by username or name)",
)
.option(
"--due-date <dueDate:string>",
"Due date of the issue",
Expand Down Expand Up @@ -82,6 +86,7 @@ export const updateCommand = new Command()
async (
{
assignee,
delegate,
dueDate,
parent,
priority,
Expand Down Expand Up @@ -175,10 +180,38 @@ export const updateCommand = new Command()

let assigneeId: string | undefined
if (assignee !== undefined) {
assigneeId = await lookupUserId(assignee)
if (!assigneeId) {
const resolvedAssignee = await lookupUser(assignee)
if (!resolvedAssignee) {
throw new NotFoundError("User", assignee)
}
if (resolvedAssignee.app) {
throw new ValidationError(
`Cannot use --assignee with app user '${assignee}'`,
{
suggestion:
`Use --delegate ${assignee} to delegate the issue to an agent user.`,
},
)
}
assigneeId = resolvedAssignee.id
}

let delegateId: string | undefined
if (delegate !== undefined) {
const resolvedDelegate = await lookupUser(delegate)
if (!resolvedDelegate) {
throw new NotFoundError("User", delegate)
}
if (!resolvedDelegate.app) {
throw new ValidationError(
`Cannot use --delegate with human user '${delegate}'`,
{
suggestion:
`Use --assignee ${delegate} to assign the issue to a human user.`,
},
)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new validation that rejects using --delegate with a human user (!resolvedDelegate.app) isn’t covered by a snapshot test. Add an issue update test that uses --delegate with a human user and asserts the error + suggestion output, mirroring the new app-user rejection test for --assignee.

Copilot uses AI. Check for mistakes.
}
delegateId = resolvedDelegate.id
}

const labelIds = []
Expand Down Expand Up @@ -229,6 +262,7 @@ export const updateCommand = new Command()

if (title !== undefined) input.title = title
if (assigneeId !== undefined) input.assigneeId = assigneeId
if (delegateId !== undefined) input.delegateId = delegateId
if (dueDate !== undefined) input.dueDate = dueDate
if (parent !== undefined) {
const parentIdentifier = await getIssueIdentifier(parent)
Expand Down
36 changes: 30 additions & 6 deletions src/utils/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,23 +691,36 @@ export async function searchTeamsByKeySubstring(
)
}

export async function lookupUserId(
export async function lookupUser(
/**
* email, username, display name, 'self', or '@me' for viewer
*/
input: "self" | "@me" | string,
): Promise<string | undefined> {
): Promise<
| {
id: string
email?: string | null
displayName?: string | null
name: string
app: boolean
}
| undefined
> {
if (input === "@me" || input === "self") {
const client = getGraphQLClient()
const query = gql(/* GraphQL */ `
query GetViewerId {
viewer {
id
email
displayName
name
app
}
}
`)
const data = await client.request(query, {})
return data.viewer.id
return data.viewer
} else {
const client = getGraphQLClient()
const query = gql(/* GraphQL */ `
Expand All @@ -726,6 +739,7 @@ export async function lookupUserId(
email
displayName
name
app
}
}
}
Expand All @@ -738,20 +752,30 @@ export async function lookupUserId(

for (const user of data.users.nodes) {
if (user.email?.toLowerCase() === input.toLowerCase()) {
return user.id
return user
}
}

for (const user of data.users.nodes) {
if (user.displayName?.toLowerCase() === input.toLowerCase()) {
return user.id
return user
}
}

return data.users.nodes[0]?.id
return data.users.nodes[0]
}
}

export async function lookupUserId(
/**
* email, username, display name, 'self', or '@me' for viewer
*/
input: "self" | "@me" | string,
): Promise<string | undefined> {
const user = await lookupUser(input)
return user?.id
}

export async function getIssueLabelIdByNameForTeam(
name: string,
teamKey: string,
Expand Down
20 changes: 20 additions & 0 deletions test/commands/issue/__snapshots__/issue-create.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Options:
-h, --help - Show this help.
--start - Start the issue after creation
-a, --assignee <assignee> - Assign the issue to 'self' or someone (by username or name)
--delegate <delegate> - Delegate the issue to an agent user (by username or name)
--due-date <dueDate> - Due date of the issue
--parent <parent> - Parent issue (if any) as a team_number code
-p, --priority <priority> - Priority of the issue (1-4, descending priority)
Expand Down Expand Up @@ -45,6 +46,25 @@ stderr:
""
`;

snapshot[`Issue Create Command - Delegate Happy Path 1`] = `
stdout:
"Creating issue in ENG

https://linear.app/test-team/issue/ENG-124/delegate-agent-work
"
stderr:
""
`;

snapshot[`Issue Create Command - Assignee Rejects App User 1`] = `
stdout:
""
stderr:
"✗ Failed to create issue: Cannot use --assignee with app user 'rowan'
Use --delegate rowan to delegate the issue to an agent user.
"
`;

snapshot[`Issue Create Command - With Milestone 1`] = `
stdout:
"Creating issue in ENG
Expand Down
21 changes: 21 additions & 0 deletions test/commands/issue/__snapshots__/issue-update.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Options:

-h, --help - Show this help.
-a, --assignee <assignee> - Assign the issue to 'self' or someone (by username or name)
--delegate <delegate> - Delegate the issue to an agent user (by username or name)
--due-date <dueDate> - Due date of the issue
--parent <parent> - Parent issue (if any) as a team_number code
-p, --priority <priority> - Priority of the issue (1-4, descending priority)
Expand Down Expand Up @@ -43,6 +44,26 @@ stderr:
""
`;

snapshot[`Issue Update Command - Delegate Happy Path 1`] = `
stdout:
"Updating issue ENG-123

✓ Updated issue ENG-123: Test Issue
https://linear.app/test-team/issue/ENG-123/test-issue
"
stderr:
""
`;

snapshot[`Issue Update Command - Assignee Rejects App User 1`] = `
stdout:
""
stderr:
"✗ Failed to update issue: Cannot use --assignee with app user 'rowan'
Use --delegate rowan to delegate the issue to an agent user.
"
`;

snapshot[`Issue Update Command - With Milestone 1`] = `
stdout:
"Updating issue ENG-123
Expand Down
Loading
Loading