Attach Linear issues from the worktree CLI (#5322)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-13 15:20:08 -07:00 committed by GitHub
parent eec18be688
commit e7906969cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 616 additions and 8 deletions

View File

@ -0,0 +1,63 @@
import { parseLinearIssueInput } from '../../shared/linear-links'
import { RuntimeClientError } from '../runtime-client'
type LinearIssueLinkParams = {
linkedLinearIssue: string | null
linkedLinearIssueWorkspaceId: string | null
linkedLinearIssueOrganizationUrlKey: string | null
}
export function getOptionalLinearIssueLinkFlag(
flags: Map<string, string | boolean>,
name: string,
options: { allowNull?: boolean } = {}
): LinearIssueLinkParams | undefined {
const value = getPresentStringFlag(flags, name)
if (value === undefined) {
return undefined
}
if (value.trim().toLowerCase() === 'null') {
if (!options.allowNull) {
throw new RuntimeClientError(
'invalid_argument',
'Omit --linear-issue on create, or pass a Linear issue identifier or URL.'
)
}
return {
linkedLinearIssue: null,
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null
}
}
const parsed = parseLinearIssueInput(value)
if (!parsed) {
throw new RuntimeClientError(
'invalid_argument',
'Pass a Linear issue identifier like STA-335, a Linear issue URL, or null to clear.'
)
}
return {
linkedLinearIssue: parsed.identifier,
// Why: changing a link must not keep a workspace id from a previous issue.
// The org key from URLs is enough for current-resolution to safely rehydrate it.
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: parsed.organizationUrlKey ?? null
}
}
function getPresentStringFlag(
flags: Map<string, string | boolean>,
name: string
): string | undefined {
if (!flags.has(name)) {
return undefined
}
const value = flags.get(name)
if (typeof value === 'string' && value.length > 0) {
return value
}
throw new RuntimeClientError('invalid_argument', `Missing value for --${name}`)
}

View File

@ -21,6 +21,7 @@ import {
resolveCurrentWorktreeSelector
} from '../selectors'
import { isTuiAgent } from '../../shared/tui-agent-config'
import { getOptionalLinearIssueLinkFlag } from './worktree-linear-issue-link'
type HookWarningResult = {
warning?: string
@ -219,11 +220,13 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
cwdParentWorktree = undefined
}
}
const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue')
const result = await client.call<RuntimeWorktreeCreateResult>('worktree.create', {
repo: getCreateRepoSelector(flags, cwdParentWorktree),
name: getRequiredStringFlag(flags, 'name'),
baseBranch: getOptionalStringFlag(flags, 'base-branch'),
linkedIssue: getOptionalNumberFlag(flags, 'issue'),
...linearIssueLink,
comment: getOptionalStringFlag(flags, 'comment'),
runHooks: flags.get('run-hooks') === true,
activate:
@ -246,10 +249,14 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
},
'worktree set': async ({ flags, client, cwd, json }) => {
assertParentFlagsCompatible(flags)
const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue', {
allowNull: true
})
const result = await client.call<{ worktree: RuntimeWorktreeRecord }>('worktree.set', {
worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client),
displayName: getOptionalStringFlag(flags, 'display-name'),
linkedIssue: getOptionalNullableNumberFlag(flags, 'issue'),
...linearIssueLink,
comment: getOptionalStringFlag(flags, 'comment'),
workspaceStatus: getOptionalStringFlag(flags, 'workspace-status'),
parentWorktree: await getOptionalWorktreeSelector(flags, 'parent-worktree', cwd, client),

View File

@ -185,10 +185,10 @@ Common Commands:
orca environment show --environment <selector> [--json]
orca environment rm --environment <selector> [--json]
orca worktree list [--repo <selector>] [--limit <n>] [--json]
orca worktree create --name <name> [--repo <selector>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--comment <text>] [--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]
orca worktree create --name <name> [--repo <selector>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--linear-issue <identifier-or-url>] [--comment <text>] [--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]
orca worktree show --worktree <selector> [--json]
orca worktree current [--json]
orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--comment <text>] [--workspace-status <id>] [--parent-worktree <selector>|--no-parent] [--json]
orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--linear-issue <identifier-or-url|null>] [--comment <text>] [--workspace-status <id>] [--parent-worktree <selector>|--no-parent] [--json]
orca worktree rm --worktree <selector> [--force] [--run-hooks] [--json]
orca worktree ps [--limit <n>] [--json]
orca file open <path> [--worktree <selector>] [--json]
@ -277,9 +277,12 @@ Examples:
$ orca repo list
$ orca worktree create --name agent-task --agent codex --prompt "hi"
$ orca worktree create --repo name:orca --name cli-test-1 --issue 273
$ orca worktree create --repo name:orca --name linear-task --linear-issue https://linear.app/stably/issue/STA-335/test-issue
$ orca worktree create --name linear-task --linear-issue STA-335
$ orca worktree show --worktree branch:Jinwoo-H/cli
$ orca worktree current
$ orca worktree set --worktree active --comment "waiting on review"
$ orca worktree set --worktree active --linear-issue null
$ orca worktree ps --limit 10
$ orca file open-changed --mode diff
$ orca file open src/App.tsx
@ -442,6 +445,8 @@ export function formatFlagHelp(flag: string): string {
interrupt: '--interrupt Send as an interrupt-style input when supported',
id: '--id <id> Identifier for a target item or permission',
issue: '--issue <number|null> Linked GitHub issue number',
'linear-issue':
'--linear-issue <id|url|null> Linked Linear issue identifier or URL; null clears on set',
json: '--json Emit machine-readable JSON',
key: '--key <key> Key argument for this command',
limit: '--limit <n> Maximum number of rows to return',

View File

@ -204,6 +204,23 @@ describe('orca root help', () => {
expect(createHelp).toContain('--parent-current Use the current linked issue as parent')
expect(callMock).not.toHaveBeenCalled()
})
it('advertises Linear issue linking on worktree create and set help', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['worktree', 'create', '--help'], '/tmp/repo')
expect(String(logSpy.mock.calls[0][0])).toContain('--linear-issue <identifier-or-url>')
logSpy.mockClear()
await main(['worktree', 'set', '--help'], '/tmp/repo')
const setHelp = String(logSpy.mock.calls[0][0])
expect(setHelp).toContain('--linear-issue <identifier-or-url|null>')
expect(setHelp).toContain('--linear-issue <id|url|null> Linked Linear issue identifier or URL')
expect(callMock).not.toHaveBeenCalled()
})
})
describe('orca cli worktree awareness', () => {
@ -628,6 +645,115 @@ describe('orca cli worktree awareness', () => {
})
})
it('passes Linear URL metadata through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_set_linear', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'https://linear.app/stably/issue/STA-335/test-issue',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably',
comment: undefined,
workspaceStatus: undefined,
parentWorktree: undefined,
noParent: false
})
})
it('clears all Linear metadata through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_clear_linear', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
linkedLinearIssue: null,
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'null',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
linkedLinearIssue: null,
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null,
comment: undefined,
workspaceStatus: undefined,
parentWorktree: undefined,
noParent: false
})
})
it('rejects invalid Linear issue values on worktree.set before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'not-a-linear-link',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Pass a Linear issue identifier like STA-335'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes workspace status through worktree.set', async () => {
queueFixtures(
callMock,
@ -664,6 +790,191 @@ describe('orca cli worktree awareness', () => {
})
})
it('passes Linear issue metadata through worktree.create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create_linear', {
worktree: {
...buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1'),
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'https://linear.app/stably/issue/STA-335/test-issue',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably',
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined
})
})
it('normalizes bare Linear identifiers through worktree.create', async () => {
queueFixtures(
callMock,
okFixture('req_create_linear_id', {
worktree: {
...buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1'),
linkedLinearIssue: 'STA-335'
},
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'sta-335',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
noParent: true,
callerTerminalHandle: undefined
})
})
it('rejects null Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'null',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Omit --linear-issue on create'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects invalid Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'not-a-linear-link',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Pass a Linear issue identifier like STA-335'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects missing Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --linear-issue'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes explicit activation through worktree.create', async () => {
queueFixtures(
callMock,

View File

@ -102,7 +102,7 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
path: ['worktree', 'create'],
summary: 'Create a new Orca-managed worktree',
usage:
'orca worktree create --name <name> [--repo <selector>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--comment <text>] [--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]',
'orca worktree create --name <name> [--repo <selector>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--linear-issue <identifier-or-url>] [--comment <text>] [--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]',
allowedFlags: [
...GLOBAL_FLAGS,
'repo',
@ -111,6 +111,7 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
'prompt',
'base-branch',
'issue',
'linear-issue',
'comment',
'setup',
'parent-worktree',
@ -132,6 +133,7 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
examples: [
'orca worktree create --name agent-task --agent codex --prompt "hi" --json',
'orca worktree create --repo id:<repoId> --name related-task --json',
'orca worktree create --repo id:<repoId> --name linear-task --linear-issue https://linear.app/stably/issue/STA-335/test-issue --json',
'orca worktree create --repo id:<repoId> --name agent-task --agent codex --prompt "hi" --json',
'orca worktree create --repo id:<repoId> --name related-task --parent-worktree active --json',
'orca worktree create --repo id:<repoId> --name independent-task --no-parent --json'
@ -141,19 +143,25 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
path: ['worktree', 'set'],
summary: 'Update Orca metadata for a worktree',
usage:
'orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--comment <text>] [--workspace-status <id>] [--parent-worktree <selector>|--no-parent] [--json]',
'orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--linear-issue <identifier-or-url|null>] [--comment <text>] [--workspace-status <id>] [--parent-worktree <selector>|--no-parent] [--json]',
allowedFlags: [
...GLOBAL_FLAGS,
'worktree',
'display-name',
'issue',
'linear-issue',
'comment',
'workspace-status',
'parent-worktree',
'no-parent'
],
notes: [
'Workspace status ids match the board columns (defaults: todo, in-progress, in-review, completed); custom statuses use their configured id.'
'Workspace status ids match the board columns (defaults: todo, in-progress, in-review, completed); custom statuses use their configured id.',
'Pass --linear-issue null to clear the Linear issue link.'
],
examples: [
'orca worktree set --worktree active --linear-issue STA-335 --json',
'orca worktree set --worktree active --linear-issue null --json'
]
},
{

View File

@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LinearWorkspace } from '../../shared/types'
const { connectedWorkspaces } = vi.hoisted(() => ({
connectedWorkspaces: [] as LinearWorkspace[]
}))
vi.mock('./issue-context-client', () => ({
getConnectedWorkspaces: () => connectedWorkspaces
}))
import {
getLinearCurrentIssueFromWorktree,
resolveLegacyLinearLinkWorkspace
} from './issue-context-current'
describe('linear issue current worktree link resolution', () => {
beforeEach(() => {
connectedWorkspaces.length = 0
})
it('uses split organization URL key metadata from CLI-created Linear links', () => {
const link = getLinearCurrentIssueFromWorktree({
id: 'repo::/tmp/worktree',
path: '/tmp/worktree',
linkedLinearIssue: 'sta-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
})
expect(link).toMatchObject({
identifier: 'STA-335',
workspaceId: null,
organizationUrlKey: 'stably',
worktreeId: 'repo::/tmp/worktree'
})
})
it('backfills workspace id from split organization URL key metadata', () => {
connectedWorkspaces.push(
makeWorkspace('workspace-1', 'stably'),
makeWorkspace('workspace-2', 'acme')
)
expect(resolveLegacyLinearLinkWorkspace('STA-335', 'stably')).toEqual({
workspaceId: 'workspace-1',
organizationUrlKey: 'stably'
})
})
it('keeps ambiguous split organization URL key backfill workspace-free', () => {
connectedWorkspaces.push(
makeWorkspace('workspace-1', 'stably'),
makeWorkspace('workspace-2', 'stably')
)
expect(resolveLegacyLinearLinkWorkspace('STA-335', 'stably')).toEqual({
organizationUrlKey: 'stably'
})
})
})
function makeWorkspace(id: string, organizationUrlKey: string): LinearWorkspace {
return {
id,
organizationId: id,
organizationName: organizationUrlKey,
organizationUrlKey,
displayName: organizationUrlKey,
email: `${id}@example.com`
}
}

View File

@ -38,9 +38,12 @@ export function getLinearCurrentIssueFromWorktree(worktree: {
}
}
export function resolveLegacyLinearLinkWorkspace(identifier: string): CurrentIssueLink['backfill'] {
export function resolveLegacyLinearLinkWorkspace(
identifier: string,
splitOrganizationUrlKey?: string | null
): CurrentIssueLink['backfill'] {
const parsed = parseLinearIssueInput(identifier)
const organizationUrlKey = parsed?.organizationUrlKey
const organizationUrlKey = splitOrganizationUrlKey ?? parsed?.organizationUrlKey
if (!organizationUrlKey) {
return undefined
}

View File

@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LinearClientForWorkspace } from './client'
import type { LinearWorkspace } from '../../shared/types'
const getClients = vi.fn()
const getStatus = vi.fn()
const isAuthError = vi.fn()
const clearToken = vi.fn()
vi.mock('./client', () => ({
acquire: vi.fn().mockResolvedValue(undefined),
release: vi.fn(),
getClients: (...args: unknown[]) => getClients(...args),
getStatus: (...args: unknown[]) => getStatus(...args),
isAuthError: (...args: unknown[]) => isAuthError(...args),
clearToken: (...args: unknown[]) => clearToken(...args)
}))
function workspace(id: string, organizationUrlKey: string): LinearWorkspace {
return {
id,
organizationId: id,
organizationName: organizationUrlKey,
organizationUrlKey,
displayName: 'Ada',
email: 'ada@example.com'
}
}
function makeEntry(options: {
workspace: LinearWorkspace
rawRequest: ReturnType<typeof vi.fn>
}): LinearClientForWorkspace {
return {
workspace: options.workspace,
client: {
client: { rawRequest: options.rawRequest }
}
} as unknown as LinearClientForWorkspace
}
function rawIssue(identifier: string) {
return {
id: `${identifier}-id`,
identifier,
title: `Title ${identifier}`,
url: `https://linear.app/stably/issue/${identifier}`,
labels: { nodes: [] }
}
}
describe('Linear issue context', () => {
beforeEach(() => {
vi.clearAllMocks()
getStatus.mockReturnValue({ workspaces: [] })
isAuthError.mockReturnValue(false)
})
it('resolves --current worktree links written as split Linear CLI metadata', async () => {
const stably = workspace('workspace-stably', 'stably')
const rawRequest = vi.fn().mockResolvedValue({ data: { issue: rawIssue('STA-335') } })
getStatus.mockReturnValue({ workspaces: [stably] })
getClients.mockReturnValue([makeEntry({ workspace: stably, rawRequest })])
const { readLinearIssueContext } = await import('./issue-context')
await expect(
readLinearIssueContext(
{
current: true,
include: { attachments: false, children: false, comments: false, relations: false },
depth: 0
},
async () => ({
identifier: 'STA-335',
workspaceId: null,
organizationUrlKey: 'stably',
worktreeId: 'repo::/tmp/repo/feature',
worktreePath: '/tmp/repo/feature'
})
)
).resolves.toMatchObject({
issue: { identifier: 'STA-335' },
meta: {
resolved: {
workspaceId: 'workspace-stably',
worktreeId: 'repo::/tmp/repo/feature',
worktreePath: '/tmp/repo/feature'
}
}
})
expect(getClients).toHaveBeenCalledWith('workspace-stably')
})
})

View File

@ -15083,7 +15083,10 @@ export class OrcaRuntimeService {
const link = getLinearCurrentIssueFromWorktree(worktree)
if (!link.workspaceId) {
const backfill = resolveLegacyLinearLinkWorkspace(worktree.linkedLinearIssue ?? '')
const backfill = resolveLegacyLinearLinkWorkspace(
worktree.linkedLinearIssue ?? '',
worktree.linkedLinearIssueOrganizationUrlKey
)
if (backfill?.workspaceId) {
this.store.setWorktreeMeta(worktree.id, {
linkedLinearIssueWorkspaceId: backfill.workspaceId,

View File

@ -155,6 +155,8 @@ export const WorktreeSet = WorktreeSelector.extend({
linkedIssue: TriStateLinkedIssue,
linkedPR: TriStateLinkedIssue,
linkedLinearIssue: z.union([z.string(), z.null()]).optional(),
linkedLinearIssueWorkspaceId: z.union([z.string(), z.null()]).optional(),
linkedLinearIssueOrganizationUrlKey: z.union([z.string(), z.null()]).optional(),
linkedGitLabMR: TriStateLinkedIssue,
linkedGitLabIssue: TriStateLinkedIssue,
isArchived: OptionalBoolean,

View File

@ -45,6 +45,8 @@ describe('worktree RPC methods', () => {
linkedIssue: 123,
linkedPR: 456,
linkedLinearIssue: undefined,
linkedLinearIssueWorkspaceId: undefined,
linkedLinearIssueOrganizationUrlKey: undefined,
linkedGitLabIssue: 789,
linkedGitLabMR: 321,
comment: undefined,
@ -258,6 +260,33 @@ describe('worktree RPC methods', () => {
})
})
it('forwards Linear metadata through worktree.set', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
updateManagedWorktreeMeta: vi.fn().mockResolvedValue({ id: 'wt-1' })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
const response = await dispatcher.dispatch(
makeRequest('worktree.set', {
worktree: 'id:wt-1',
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
})
)
expect(response).toMatchObject({ ok: true })
expect(runtime.updateManagedWorktreeMeta).toHaveBeenCalledWith(
'id:wt-1',
expect.objectContaining({
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
})
)
})
it('rejects worktree.set when both parent and no-parent are supplied', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',

View File

@ -117,6 +117,8 @@ export const WORKTREE_METHODS: RpcMethod[] = [
linkedIssue: params.linkedIssue,
linkedPR: params.linkedPR,
linkedLinearIssue: params.linkedLinearIssue,
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
linkedGitLabMR: params.linkedGitLabMR,
linkedGitLabIssue: params.linkedGitLabIssue,
comment: params.comment,

View File

@ -76,6 +76,12 @@ describe('RPC optional pipe schemas', () => {
telemetrySource: 'raw-source'
})
expectParses(methodParams(WORKTREE_METHODS, 'worktree.create'), { repo: 'repo-1' })
expectParses(methodParams(WORKTREE_METHODS, 'worktree.set'), {
worktree: 'id:wt-1',
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
})
expectParses(methodParams(WORKTREE_METHODS, 'worktree.prefetchCreateBase'), { repo: 'repo-1' })
})
})

View File

@ -52,6 +52,10 @@ describe('linear links', () => {
identifier: 'ENG-123',
organizationUrlKey: 'acme'
})
expect(parseLinearIssueInput('https://linear.app/stably/issue/STA-335/test-issue')).toEqual({
identifier: 'STA-335',
organizationUrlKey: 'stably'
})
})
it('rejects non-Linear issue input', () => {