Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
144 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
a9ff232
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 17, 2026
e1b1b55
fix(claude-accounts): annotate test casts and merge a duplicate impor…
Ethan-Rivas Oct 17, 2026
17e76d2
fix(claude-accounts): recheck System default before naming the select…
Ethan-Rivas Oct 17, 2026
28d75c1
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 17, 2026
ac5dcdd
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 17, 2026
a3f5011
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 18, 2026
12ac739
refactor(tab-bar): move the terminal tab hover card's program line in…
Ethan-Rivas Oct 18, 2026
782f95f
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 18, 2026
076848d
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 18, 2026
9d59825
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
9ec51bb
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
14b0002
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
8780493
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
0d4a155
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
a74afdd
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
2926aec
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 19, 2026
48fb6b8
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 20, 2026
a6dff17
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 20, 2026
a31867b
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 20, 2026
e19c014
refactor(agent-launch): move the project Claude account lookup and th…
Ethan-Rivas Oct 20, 2026
a70c5c4
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
a4fa26a
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
5d5aeb5
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
3a25450
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
b71b6c2
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
66ccc89
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
2e4c2d9
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
740ebc5
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 21, 2026
b9eb73e
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
0b6448d
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
6609e2a
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
7515556
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
3c5d9d2
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
74ccc04
test(claude-accounts): mock the renamed runtime-owner helper in the n…
Ethan-Rivas Oct 22, 2026
e434869
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
05f896b
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 22, 2026
ea638c5
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 23, 2026
ea42a99
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 23, 2026
016fc1b
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 23, 2026
c018423
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 23, 2026
1251b95
refactor(terminal-pane): move the Start on active account button into…
Ethan-Rivas Oct 23, 2026
6767bc0
refactor(cli): read worktree create --account inside the agent launch…
Ethan-Rivas Oct 23, 2026
94ad568
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 23, 2026
3eb4a66
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 24, 2026
a952fb0
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 24, 2026
b1e313b
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 24, 2026
d60d891
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 24, 2026
fa5c092
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 24, 2026
934591c
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
7103353
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
658e0d2
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
cc82df2
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
d14583c
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
86fb000
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
f157ee4
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 25, 2026
7b0b18c
fix: skip pinned-account marking on refused resumes and pass startup …
Ethan-Rivas Oct 25, 2026
49fb75b
test: cover the startup reaching pinned Claude refusal recovery
Ethan-Rivas Oct 25, 2026
bc59542
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
8d902fd
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
28f8623
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
56e8b0d
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
3ab218d
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
5999066
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
2fc0675
fix(cli): refuse --account with --terminal before any runtime call
Ethan-Rivas Oct 26, 2026
a8a2019
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
e3b6a92
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
82bdc85
test: attach the executor stub host comment to its harness
Ethan-Rivas Oct 26, 2026
64b7b52
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 26, 2026
30487d4
Merge remote-tracking branch 'origin/main' into feat/per-project-agen…
Ethan-Rivas Oct 27, 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 @@ -24,6 +24,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/packages/process-host/src/__fixtures__/**/*",
"../src/main/gitlab/mappers.ts",
"../src/main/runtime/sidebar-inputs/sidebar-clock.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 @@ -162,3 +163,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'

// Keep acceptance policies distinct until every caller is proven compatible with any widened/narrowed reply set.

Expand Down Expand Up @@ -32,7 +33,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 { selectorAmbiguityRecovery } from './selector-ambiguity-recovery'
import type { RuntimeRpcFailure } from './runtime-client'
Expand Down Expand Up @@ -64,7 +65,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 = contextRecovery(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/account-runtime-capabilities'

/**
* `--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
}
103 changes: 103 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,103 @@
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/account-runtime-capabilities'

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

const invokeWorkerStart = (flags: Map<string, string | boolean>) =>
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worker-start reads only flags, client.call, cwd and json.
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,10 +10,17 @@ 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 }) => {
const terminal = getPresentStringFlag(flags, 'terminal')
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 model = getOptionalStringFlag(flags, 'model')
const effort = getOptionalStringFlag(flags, 'effort')
if (model || effort) {
Expand All @@ -29,6 +36,11 @@ export const ORCHESTRATION_WORKER_LAUNCH_HANDLER: Record<string, CommandHandler>
)
}
}
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 @@ -63,6 +75,7 @@ export const ORCHESTRATION_WORKER_LAUNCH_HANDLER: Record<string, CommandHandler>
agent: getOptionalStringFlag(flags, 'agent'),
model,
effort,
...(account ? { account } : {}),
terminal,
retryOf: getOptionalStringFlag(flags, 'retry-of'),
timeoutMs: getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms'),
Expand Down
15 changes: 13 additions & 2 deletions src/cli/handlers/worktree-create-agent-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { printLineageSummary } from './worktree-lineage-summary'
import { printHookWarning } from './worktree-removal-warnings'
import { withSetupDecisionRecovery } from './worktree-setup-decision-recovery'
import { withCliAgentLaunchRecovery } from '../agent-launch-recovery'
import { readClaudeLaunchAccountFlag } from './claude-launch-account-flag'

type WorktreeAgentLaunchResult = Pick<
RuntimeWorktreeCreateResult,
Expand All @@ -39,8 +40,17 @@ export async function createWorktreeWithAgent(args: {
rawArgs?: readonly string[]
}): Promise<void> {
const { client, flags, create, launch, json, rawArgs } = args
const capabilities = await getAgentLaunchCapabilities(client)
// Why: only the create's startup terminal can run Claude on a non-active account.
const claudeAccount = await readClaudeLaunchAccountFlag(flags, client, launch.agent)
if (claudeAccount && (launch.operationId || launch.surface || hasLaunchModelFlags(flags))) {
throw new RuntimeClientError(
'invalid_argument',
'--account cannot be combined with --model, --effort, --surface or --retry-launch.'
)
}
const capabilities = claudeAccount ? undefined : await getAgentLaunchCapabilities(client)
if (
claudeAccount ||
!capabilities?.includes(AGENT_LAUNCH_LOCAL_PRESENTATION_RUNTIME_CAPABILITY) ||
(launch.prompt && !capabilities.includes(AGENT_LAUNCH_PROMPT_CARRY_RUNTIME_CAPABILITY))
) {
Expand All @@ -63,7 +73,8 @@ export async function createWorktreeWithAgent(args: {
...create,
startupAgent: launch.agent,
startupPrompt: launch.prompt?.text ?? '',
launchSource: 'cli'
launchSource: 'cli',
...(claudeAccount ? { startupClaudeAccount: claudeAccount } : {})
}
const created = await withSetupDecisionRecovery(
withCliAgentLaunchRecovery(
Expand Down
4 changes: 2 additions & 2 deletions src/cli/handlers/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ import {
function getOptionalStartupAgent(flags: Map<string, string | boolean>): TuiAgent | undefined {
const agent = getPresentStringFlag(flags, 'agent')
if (agent === undefined) {
const needsAgent = ['prompt', 'model', 'effort', 'surface', 'retry-launch'].find((name) =>
flags.has(name)
const needsAgent = ['prompt', 'model', 'effort', 'surface', 'retry-launch', 'account'].find(
(name) => flags.has(name)
)
if (needsAgent) {
throw new RuntimeClientError('invalid_argument', `--${needsAgent} requires --agent`)
Expand Down
Loading