Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
1cf0f46
feat(claude-accounts): resolve --account selectors to managed Claude …
fardenghi Sep 24, 2026
8a7e4fd
feat(claude-accounts): track Claude PTYs pinned to a non-active account
fardenghi Sep 24, 2026
8f000ae
feat(claude-accounts): prepare pinned Claude launches on a non-active…
fardenghi Sep 24, 2026
51fa681
feat(claude-accounts): guard pinned accounts from host switches, remo…
fardenghi Sep 24, 2026
7f4f1e9
feat(pty): spawn runtime Claude PTYs pinned to a requested account
fardenghi Sep 24, 2026
6ea647b
feat(runtime): carry --account from terminal create to the PTY and re…
fardenghi Sep 24, 2026
100386e
feat(worktree): start a created worktree's Claude agent on a requeste…
fardenghi Sep 24, 2026
d739f79
feat(orchestration): run worker-start --account workers on a pinned C…
fardenghi Sep 24, 2026
fdb0edc
feat(cli): add --account to worker-start and worktree create for Claude
fardenghi Sep 24, 2026
36a8b8b
test(worktree): keep the --account mapping test out of the shared pro…
fardenghi Sep 24, 2026
05058eb
fix(claude-accounts): claim accounts atomically against pinned launches
fardenghi Sep 24, 2026
b9de81a
fix(cli): refuse worker-start --account with --terminal before callin…
fardenghi Sep 24, 2026
36f1060
fix(claude-accounts): revalidate startup pinned seed markers against …
fardenghi Sep 24, 2026
9295b63
fix(claude-accounts): wait for a pinned reservation outside the auth …
fardenghi Sep 24, 2026
629da63
fix(claude-accounts): claim a pinned account before re-authentication…
fardenghi Sep 24, 2026
87c9fe4
fix(claude-accounts): refuse to pin an account old host Claude termin…
fardenghi Sep 24, 2026
c730a07
feat(claude-accounts): add per-project Claude account preference
Ethan-Rivas Sep 26, 2026
a8b458e
feat(claude-accounts): carry the pinned Claude account in launch config
Ethan-Rivas Sep 26, 2026
7502ff6
refactor(pty): share pinned Claude spawn helpers across launch lanes
Ethan-Rivas Sep 26, 2026
2565f4f
feat(claude-accounts): tag pinned launch refusals with a stable code
Ethan-Rivas Sep 26, 2026
814b9f9
feat(pty): pin Claude launches to the project account on both spawn l…
Ethan-Rivas Sep 26, 2026
fbea601
feat(claude-accounts): stamp the resolved Claude account on GUI launches
Ethan-Rivas Sep 26, 2026
bee7622
feat(settings): Claude account section and per-project ask setting
Ethan-Rivas Sep 26, 2026
1e466f9
feat(claude-accounts): ask which account to use per project
Ethan-Rivas Sep 26, 2026
7b6ca7d
feat(terminal): explain refused pinned launches and restart on the ac…
Ethan-Rivas Sep 26, 2026
94ed5c6
feat(claude-accounts): keep WSL projects off per-project Claude accounts
Ethan-Rivas Sep 26, 2026
69f4613
feat(terminal): show the pinned Claude account on the tab
Ethan-Rivas Sep 26, 2026
86dbd67
feat(claude-accounts): replay the recorded Claude account on resume a…
Ethan-Rivas Sep 26, 2026
8370049
feat(runtime): carry the launch's Claude account on agent-session RPC…
Ethan-Rivas Sep 26, 2026
b0b3935
feat(i18n): translate the per-project Claude account strings
Ethan-Rivas Sep 26, 2026
2cd7c2d
test(e2e): cover per-project Claude account launches
Ethan-Rivas Sep 26, 2026
2783aaf
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 26, 2026
442fcb6
fix(claude-accounts): launch WSL terminals unpinned and route the tab…
Ethan-Rivas Sep 26, 2026
1baf9cc
fix(claude-accounts): keep WSL and SSH tabs from showing a saved acco…
Ethan-Rivas Sep 26, 2026
f3a5f83
fix(i18n): make the Korean remember-account label a complete phrase
Ethan-Rivas Sep 26, 2026
6ce13cf
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 28, 2026
42dd573
fix(claude-accounts): keep main's pinned account apart from recorded …
Ethan-Rivas Sep 28, 2026
573ff1d
fix(claude-accounts): compare the record-level pinned account when re…
Ethan-Rivas Sep 28, 2026
b9d05c0
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 28, 2026
fc4d926
fix(claude-accounts): don't copy possibly spent credentials to the ho…
Ethan-Rivas Sep 28, 2026
29c7bf4
fix(claude-accounts): don't start an ask-on-launch project on an unch…
Ethan-Rivas Sep 29, 2026
6cb85c2
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 29, 2026
4858006
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 29, 2026
fb34204
test(claude-accounts): count main's new launch profile in the caller …
Ethan-Rivas Sep 29, 2026
aa7ec46
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 29, 2026
7eea324
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Sep 29, 2026
c5d64ed
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 1, 2026
6041c48
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 1, 2026
7d9278e
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 2, 2026
5094170
refactor(cli): move the worktree create spec to its own file to keep …
Ethan-Rivas Oct 2, 2026
0f88550
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 2, 2026
0fab477
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 3, 2026
9432b51
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 3, 2026
0aac4c7
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 3, 2026
2c094bc
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 5, 2026
8862679
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 5, 2026
d62bbfc
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 6, 2026
cd0f67f
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 6, 2026
aec383c
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 6, 2026
fdf3145
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 6, 2026
a452ac3
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
5a21536
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
379100f
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
58fe4b1
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
3722550
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
ffb8f3a
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
e777e32
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
3d58241
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
52ce2f0
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
8443713
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
e985233
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
ed86745
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 7, 2026
8780cbc
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 8, 2026
98f1edd
test(e2e): expect pinned Claude launches in the account's own folder
Ethan-Rivas Oct 8, 2026
dd3cf30
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 8, 2026
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
1 change: 1 addition & 0 deletions config/tsconfig.tc.web.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"../src/preload/runtime-environment-subscriptions.ts",
"../src/preload/usage-provider-api.ts",
"../src/shared/**/*",
"../src/main/claude-accounts/project-claude-account-resolution.ts",
"../src/main/gitlab/mappers.ts",
"../src/main/ipc/deferred-emoji-shortcode-dataset.ts",
"../src/main/ipc/worktree-branch-name.ts",
Expand Down
16 changes: 16 additions & 0 deletions mobile/src/transport/rpc-acceptance-policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isMethodNotFoundRefusal,
isStreamingOpenerReply,
requireRpcResultOrThrowCodedError,
requireRpcResultOrThrowMessage,
rpcObjectResultOrNull
} from './rpc-acceptance-policies'

Expand Down Expand Up @@ -166,3 +167,18 @@ describe('isStreamingOpenerReply', () => {
expect(isStreamingOpenerReply(response)).toBe(false)
})
})

describe('requireRpcResultOrThrowMessage', () => {
it('shows a pinned Claude refusal without its renderer marker', () => {
const refusal: RpcResponse = {
id: 'rpc-1',
ok: false,
error: {
code: 'runtime_error',
message: 'Account a@b.c is in use. [claude_pinned:host-sessions email=a%40b.c terminals=1]'
},
_meta: meta
}
expect(() => requireRpcResultOrThrowMessage(refusal)).toThrow(/^Account a@b\.c is in use\.$/)
})
})
3 changes: 2 additions & 1 deletion mobile/src/transport/rpc-acceptance-policies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RpcResponse, RpcSuccess } from './types'
import { stripClaudePinnedLaunchMarker } from '../../../src/shared/claude/claude-pinned-launch-error'

// Named acceptance policies for RPC replies. Call sites used to hand-roll these
// predicates and did not agree with each other; each policy here preserves one
Expand Down Expand Up @@ -35,7 +36,7 @@ export function isStreamingOpenerReply(
/** New-tab errors historically show the host message without its diagnostic code. */
export function requireRpcResultOrThrowMessage(response: RpcResponse): unknown {
if (!response.ok) {
throw new Error(response.error.message)
throw new Error(stripClaudePinnedLaunchMarker(response.error.message))
}
return response.result
}
Expand Down
18 changes: 18 additions & 0 deletions src/cli/cli-error-claude-pinned.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest'
import { formatCliError } from './cli-error'
import { RuntimeRpcFailureError } from './runtime-client'

it('prints a pinned Claude refusal without the renderer marker', () => {
const error = new RuntimeRpcFailureError({
id: 'request-1',
ok: false,
error: {
code: 'runtime_error',
message: 'That Claude account no longer exists. [claude_pinned:account-missing]'
},
_meta: { runtimeId: 'runtime-1' }
})
const output = formatCliError(error)
expect(output).toContain('That Claude account no longer exists.')
expect(output).not.toContain('claude_pinned')
})
5 changes: 4 additions & 1 deletion src/cli/cli-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
stripAutomationOwnerConflictCode
} from '../shared/automation-owner-conflict'
import { automationOwnerConflictRecovery } from './automation-owner-conflict-recovery'
import { stripClaudePinnedLaunchMarker } from '../shared/claude/claude-pinned-launch-error'
import { worktreeSelectorRecovery } from './worktree-selector-recovery'
import type { RuntimeRpcFailure } from './runtime-client'
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime/types'
Expand Down Expand Up @@ -35,7 +36,9 @@ function errorCode(error: unknown): string | undefined {
}

export function formatCliError(error: unknown, context: CliErrorContext = {}): string {
const message = error instanceof Error ? error.message : String(error)
const message = stripClaudePinnedLaunchMarker(
error instanceof Error ? error.message : String(error)
)
const selector = selectorRecovery(errorCode(error), context)
if (selector) {
return formatMessageWithNextSteps(
Expand Down
38 changes: 38 additions & 0 deletions src/cli/handlers/claude-launch-account-flag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getOptionalStringFlag } from '../flags'
import { RuntimeClientError, type RuntimeClient } from '../runtime-client'
import type { RuntimeStatus } from '../../shared/runtime-types'
import { CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY } from '../../shared/protocol-version'

/**
* `--account <id|email>`: the managed Claude account a launch runs on, resolved by the host.
*
* Why the capability check: an older host strips the unknown field and would quietly start the
* agent on its active account — exactly what the caller asked not to happen.
*/
export async function readClaudeLaunchAccountFlag(
flags: Map<string, string | boolean>,
client: RuntimeClient,
agent: string | undefined
): Promise<string | undefined> {
if (!flags.has('account')) {
return undefined
}
const account = getOptionalStringFlag(flags, 'account')?.trim()
if (!account) {
throw new RuntimeClientError(
'invalid_argument',
'--account requires a Claude account id or email.'
)
}
if (agent !== 'claude') {
throw new RuntimeClientError('invalid_argument', '--account requires --agent claude.')
}
const status = await client.call<RuntimeStatus>('status.get')
if (!status.result.capabilities?.includes(CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY)) {
throw new RuntimeClientError(
'incompatible_runtime',
'The connected Orca runtime does not support --account for Claude launches. Update or restart Orca and try again.'
)
}
return account
}
102 changes: 102 additions & 0 deletions src/cli/handlers/orchestration-worker-account-cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const callMock = vi.fn()

vi.mock('../format', () => ({ printResult: vi.fn() }))
vi.mock('../selectors', () => ({ getTerminalHandle: vi.fn() }))

import { ORCHESTRATION_HANDLERS } from './orchestration'
import { CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY } from '../../shared/protocol-version'

describe('orchestration worker-start --account', () => {
beforeEach(() => {
callMock.mockReset()
process.exitCode = undefined
})

const invokeWorkerStart = (flags: Map<string, string | boolean>) =>
ORCHESTRATION_HANDLERS['orchestration worker-start']({
flags,
client: { call: callMock },
cwd: '/tmp/repo',
json: true
} as never)

it('forwards --account to a host that resolves Claude launch accounts', async () => {
callMock
.mockResolvedValueOnce({
result: { capabilities: [CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY] }
})
.mockResolvedValueOnce({
result: { runId: 'run_1', taskId: 'task_1', dispatchId: 'ctx_1', state: 'ready' }
})

await invokeWorkerStart(
new Map<string, string | boolean>([
['task', 'task_1'],
['agent', 'claude'],
['account', 'pinned@example.com'],
['from', 'term_coord']
])
)

expect(callMock).toHaveBeenNthCalledWith(1, 'status.get')
expect(callMock).toHaveBeenNthCalledWith(
2,
'orchestration.workerStart',
expect.objectContaining({ agent: 'claude', account: 'pinned@example.com' })
)
})

it('fails before worker-start when --account would be stripped or misapplied', async () => {
callMock.mockResolvedValueOnce({ result: { capabilities: [] } })
const flags = (agent: string) =>
new Map<string, string | boolean>([
['task', 'task_1'],
['agent', agent],
['account', 'acct-b'],
['from', 'term_coord']
])

await expect(invokeWorkerStart(flags('claude'))).rejects.toMatchObject({
code: 'incompatible_runtime'
})
await expect(invokeWorkerStart(flags('codex'))).rejects.toThrow(
'--account requires --agent claude.'
)
expect(callMock).toHaveBeenCalledTimes(1)
})

it('never sends an account field when --account is absent', async () => {
callMock.mockResolvedValue({
result: { runId: 'run_1', taskId: 'task_1', dispatchId: 'ctx_1', state: 'ready' }
})
await invokeWorkerStart(
new Map<string, string | boolean>([
['task', 'task_1'],
['agent', 'claude'],
['from', 'term_coord']
])
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock.mock.calls[0]?.[1]).not.toHaveProperty('account')
})

it('refuses --account with --terminal before calling any host', async () => {
await expect(
invokeWorkerStart(
new Map<string, string | boolean>([
['task', 'task_1'],
['terminal', 'term_worker'],
['account', 'acct-b'],
['from', 'term_coord']
])
)
).rejects.toMatchObject({
code: 'invalid_argument',
message:
'--account cannot be combined with --terminal; the reused terminal keeps its own account.'
})
expect(callMock).not.toHaveBeenCalled()
})
})
13 changes: 13 additions & 0 deletions src/cli/handlers/orchestration/worker-launch-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isDevCliInvocation } from './runtime-compatibility'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
import { formatWorkerStart } from './worker-output'
import { renderResolvedOrchestrationCommand } from '../../orchestration-mutation-recovery'
import { readClaudeLaunchAccountFlag } from '../claude-launch-account-flag'

export const ORCHESTRATION_WORKER_LAUNCH_HANDLER: Record<string, CommandHandler> = {
'orchestration worker-start': async ({ flags, client, cwd, json }) => {
Expand All @@ -28,6 +29,17 @@ export const ORCHESTRATION_WORKER_LAUNCH_HANDLER: Record<string, CommandHandler>
)
}
}
if (flags.has('account') && flags.has('terminal')) {
throw new RuntimeClientError(
'invalid_argument',
'--account cannot be combined with --terminal; the reused terminal keeps its own account.'
)
}
const account = await readClaudeLaunchAccountFlag(
flags,
client,
getOptionalStringFlag(flags, 'agent')
)
const task = getOptionalStringFlag(flags, 'task')
const spec = getOptionalStringFlag(flags, 'spec')
const taskTitle = getOptionalStringFlag(flags, 'task-title')
Expand Down Expand Up @@ -62,6 +74,7 @@ export const ORCHESTRATION_WORKER_LAUNCH_HANDLER: Record<string, CommandHandler>
agent: getOptionalStringFlag(flags, 'agent'),
model,
effort,
...(account ? { account } : {}),
terminal: getOptionalStringFlag(flags, 'terminal'),
retryOf: getOptionalStringFlag(flags, 'retry-of'),
timeoutMs: getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms'),
Expand Down
5 changes: 4 additions & 1 deletion src/cli/handlers/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { isTuiAgent } from '../../shared/tui-agent-config'
import { isWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope'
import { printLineageSummary } from './worktree-lineage-summary'
import { readClaudeLaunchAccountFlag } from './claude-launch-account-flag'
import { projectWorktreePsTerminalVerdict } from '../worktree-ps-terminal-verdict'
import {
assertWorkspaceTargetFlagsCompatible,
Expand Down Expand Up @@ -185,6 +186,7 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
const explicitParentWorktree = explicitParent.parentWorktree
const explicitParentWorkspace = explicitParent.parentWorkspace
const startupAgent = getOptionalStartupAgent(flags)
const claudeAccount = await readClaudeLaunchAccountFlag(flags, client, startupAgent)
const setupDecision = getOptionalSetupDecision(flags)
const noParent = flags.get('no-parent') === true
const envParentWorkspace =
Expand Down Expand Up @@ -239,7 +241,8 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
? {
startupAgent,
startupPrompt: getPresentStringFlag(flags, 'prompt', { allowEmpty: true }) ?? '',
launchSource: 'cli'
launchSource: 'cli',
...(claudeAccount ? { startupClaudeAccount: claudeAccount } : {})
}
: {})
})
Expand Down
103 changes: 103 additions & 0 deletions src/cli/index-worktree-create-claude-account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it, vi } from 'vitest'
import { CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY } from '../shared/protocol-version'

const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))

vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})

vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
listEnvironments: listEnvironmentsMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))

vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})

import { main } from './index'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'

describe('orca worktree create --account', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})

const createArgs = ['worktree', 'create', '--repo', 'id:repo-1', '--name', 'pinned', '--json']

it('sends the account selector to a host that advertises support', async () => {
queueFixtures(
callMock,
okFixture('req_status', { capabilities: [CLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITY] }),
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/pinned', 'pinned', 'abc', 'repo-1')
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})

await main([...createArgs, '--agent', 'claude', '--account', 'pinned@example.com'], '/tmp/repo')

expect(callMock).toHaveBeenNthCalledWith(1, 'status.get')
expect(callMock).toHaveBeenNthCalledWith(
3,
'worktree.create',
expect.objectContaining({
startupAgent: 'claude',
startupPrompt: '',
startupClaudeAccount: 'pinned@example.com'
})
)
})

it('refuses an older host instead of launching on its active account', async () => {
queueFixtures(callMock, okFixture('req_status', { capabilities: [] }))
vi.spyOn(console, 'error').mockImplementation(() => {})

await main([...createArgs, '--agent', 'claude', '--account', 'acct-b'], '/tmp/repo')

expect(process.exitCode).toBe(1)
expect(callMock).not.toHaveBeenCalledWith('worktree.create', expect.anything())
})

it('requires --agent claude', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {})

await main([...createArgs, '--agent', 'codex', '--account', 'acct-b'], '/tmp/repo')

expect(process.exitCode).toBe(1)
expect(callMock).not.toHaveBeenCalled()
})
})
Loading