Hide workspace parent flag from worktree create CLI (#5743)

This commit is contained in:
Brennan Benson 2026-06-18 17:53:19 -07:00 committed by GitHub
parent 0659ba0aa8
commit cfc003452c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 381 additions and 127 deletions

View File

@ -1,12 +1,12 @@
---
name: orca-cli
description: >-
Use the public `orca` CLI to operate Orca-managed worktrees/workspaces,
Use the public `orca` CLI to operate Orca-managed worktrees, folder contexts,
terminals, repos, automations, worktree comments, and the browser embedded
inside the Orca app. Use when the user says "$orca-cli", "use orca cli",
"Orca worktree/workspace", "child workspace", "spawn codex/claude in a
workspace", "read/wait/send Orca terminal", "terminal send", "Orca browser", or "control the
browser inside Orca". Prefer this over raw `git worktree`, ad hoc PTYs,
"Orca worktree", "child worktree", "spawn codex/claude in a worktree",
"read/wait/send Orca terminal", "terminal send", "Orca browser", or "control
the browser inside Orca". Prefer this over raw `git worktree`, ad hoc PTYs,
Playwright, or Computer Use when the task touches Orca-managed state. Use
Computer Use for browser windows, webviews, or desktop UI outside Orca's
embedded browser.
@ -40,7 +40,7 @@ Prefer `--json` for agent-driven calls. If the CLI is missing, say so explicitly
## Worktrees
An Orca worktree/workspace is Orca's tracked view of a repo checkout, its metadata, terminals, browser tabs, and UI state.
An Orca worktree is Orca's tracked view of a repo checkout, its metadata, terminals, browser tabs, and UI state.
Common commands:
@ -55,6 +55,8 @@ orca worktree ps --json
orca worktree current --json
orca worktree show --worktree <selector> --json
orca worktree create --repo id:<repoId> --name related-task --json
orca worktree create --repo id:<repoId> --name related-task --parent-worktree active --json
orca worktree create --repo id:<repoId> --name folder-child --parent-worktree folder:<folderId> --json
orca worktree create --name child-task --agent codex --prompt "hi" --json
orca worktree create --name independent-task --no-parent --json
orca worktree set --worktree id:<worktreeId> --display-name "My Task" --json
@ -66,11 +68,13 @@ Selectors:
- `id:<worktreeId>`, `path:<absolutePath>`, `branch:<branchName>`, `issue:<number>`
- `active` / `current` for the enclosing Orca-managed worktree from the shell cwd
- For `worktree create --parent-worktree` only, folder/worktree parent context keys are also valid: `folder:<folderId>`, `worktree:<worktreeId>`, `id:folder:<folderId>`, `id:worktree:<worktreeId>`
Lineage rules:
- When creating from inside an Orca-managed worktree, Orca infers the current workspace as the parent when it can.
- Use `--parent-worktree active` when the child relationship should be explicit.
- When creating from inside an Orca-managed worktree or folder context, Orca infers the current parent context when it can.
- Use `--parent-worktree active` when the child worktree relationship should be explicit.
- Use `--parent-worktree folder:<folderId>` or `--parent-worktree worktree:<worktreeId>` when a folder or worktree parent context should be explicit.
- Use `--no-parent` only when the new work is independent.
- If `--repo` is omitted, Orca infers the repo from the current Orca worktree when possible.
@ -89,7 +93,7 @@ orca worktree create --name task --run-hooks --json
- `--agent`, `--activate`, and `--run-hooks` reveal the new worktree. Plain create stays in the background.
- Let Orca choose setup terminal placement from repo settings, including tab vs split behavior. Do not manually create extra setup terminals.
- If an older installed CLI rejects `--agent`, `--prompt`, or `--setup`, create the worktree normally, then run `orca terminal create --worktree <selector> --command "codex"` and `orca terminal send` if a prompt is needed.
- `worktree create` creates a new checkout/workspace. For a fresh agent in the current checkout, use `orca terminal create --worktree active --command "codex" --json`.
- `worktree create` creates a new checkout. For a fresh agent in the current checkout, use `orca terminal create --worktree active --command "codex" --json`.
## Worktree Comments

View File

@ -95,10 +95,10 @@ describe('formatCliError', () => {
ok: false,
error: {
code: 'LINEAGE_PARENT_NOT_FOUND',
message: 'Parent workspace was not found.',
message: 'Parent selector was not found.',
data: {
nextSteps: [
'Run `orca worktree list` and pass a valid --parent-worktree selector.',
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<id>, id:<worktreeId>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.',
123
]
@ -109,8 +109,8 @@ describe('formatCliError', () => {
expect(formatCliError(error)).toBe(
[
'Parent workspace was not found.',
'Next step: Run `orca worktree list` and pass a valid --parent-worktree selector.',
'Parent selector was not found.',
'Next step: Pass a valid --parent-worktree selector such as folder:<id>, worktree:<id>, id:<worktreeId>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Next step: Retry with --no-parent to create without lineage.'
].join('\n')
)

View File

@ -0,0 +1,60 @@
import { isWorkspaceKey } from '../../shared/workspace-scope'
import { getOptionalStringFlag } from '../flags'
import { RuntimeClientError, type RuntimeClient } from '../runtime-client'
import { getOptionalWorktreeSelector } from '../selectors'
export type CreateParentSelector = {
parentWorktree?: string
parentWorkspace?: string
}
const CREATE_PARENT_CONFLICT_MESSAGE = 'Choose either one parent selector or --no-parent.'
export function assertCreateParentFlagsCompatible(flags: Map<string, string | boolean>): void {
if (flags.has('parent-worktree') && flags.get('no-parent') === true) {
throw new RuntimeClientError('invalid_argument', CREATE_PARENT_CONFLICT_MESSAGE)
}
const parentWorktree = flags.get('parent-worktree')
if (
flags.has('parent-worktree') &&
(typeof parentWorktree !== 'string' || parentWorktree === '')
) {
throw new RuntimeClientError('invalid_argument', 'Missing required --parent-worktree')
}
}
function getWorkspaceKeyParentSelector(selector: string): string | undefined {
const rawSelector = selector.startsWith('id:') ? selector.slice('id:'.length) : selector
return isWorkspaceKey(rawSelector) ? rawSelector : undefined
}
export async function resolveCreateParentSelector(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<CreateParentSelector> {
const rawParentWorktree = getOptionalStringFlag(flags, 'parent-worktree')
if (!rawParentWorktree) {
return {}
}
const parentWorkspace = getWorkspaceKeyParentSelector(rawParentWorktree)
if (parentWorkspace) {
// Why: create exposes one public parent flag, while the runtime still needs
// workspace keys to preserve folder/worktree lineage accurately.
return { parentWorkspace }
}
const parentWorktree = await getOptionalWorktreeSelector(flags, 'parent-worktree', cwd, client)
const resolvedParentWorkspace = parentWorktree
? getWorkspaceKeyParentSelector(parentWorktree)
: undefined
if (resolvedParentWorkspace) {
// Why: active/current may resolve to a folder workspace pseudo-worktree id.
return { parentWorkspace: resolvedParentWorkspace }
}
return {
parentWorktree
}
}

View File

@ -13,7 +13,7 @@ function getLineageSourceLabel(source: string): string {
case 'explicit-cli-flag':
return 'explicit flag'
case 'active-workspace':
return 'active workspace'
return 'active context'
default:
return 'manual action'
}

View File

@ -28,6 +28,10 @@ import {
hasWorkspaceProjectTarget,
resolveProjectCreateRepoSelector
} from '../worktree-project-target'
import {
assertCreateParentFlagsCompatible,
resolveCreateParentSelector
} from './worktree-create-parent-selector'
import { getOptionalLinearIssueLinkFlag } from './worktree-linear-issue-link'
type HookWarningResult = {
@ -54,25 +58,13 @@ function printPreservedBranchWarning(result: PreservedBranchResult, json: boolea
}
}
function assertParentFlagsCompatible(flags: Map<string, string | boolean>): void {
function assertParentWorktreeFlagsCompatible(flags: Map<string, string | boolean>): void {
if (flags.has('parent-worktree') && flags.get('no-parent') === true) {
throw new RuntimeClientError(
'invalid_argument',
'Choose either --parent-worktree or --no-parent, not both.'
)
}
if (flags.has('parent-workspace') && flags.get('no-parent') === true) {
throw new RuntimeClientError(
'invalid_argument',
'Choose either --parent-workspace or --no-parent, not both.'
)
}
if (flags.has('parent-workspace') && flags.has('parent-worktree')) {
throw new RuntimeClientError(
'invalid_argument',
'Choose either --parent-workspace or --parent-worktree, not both.'
)
}
const parentWorktree = flags.get('parent-worktree')
if (
flags.has('parent-worktree') &&
@ -80,13 +72,6 @@ function assertParentFlagsCompatible(flags: Map<string, string | boolean>): void
) {
throw new RuntimeClientError('invalid_argument', 'Missing required --parent-worktree')
}
const parentWorkspace = flags.get('parent-workspace')
if (
flags.has('parent-workspace') &&
(typeof parentWorkspace !== 'string' || parentWorkspace === '')
) {
throw new RuntimeClientError('invalid_argument', 'Missing required --parent-workspace')
}
}
function getEnvParentWorkspace(): string | undefined {
@ -211,20 +196,16 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
printResult(result, json, formatWorktreeShow)
},
'worktree create': async ({ flags, client, cwd, json }) => {
assertParentFlagsCompatible(flags)
assertCreateParentFlagsCompatible(flags)
assertWorkspaceTargetFlagsCompatible(flags)
const callerTerminalHandle =
typeof process.env.ORCA_TERMINAL_HANDLE === 'string' &&
process.env.ORCA_TERMINAL_HANDLE.length > 0
? process.env.ORCA_TERMINAL_HANDLE
: undefined
const explicitParentWorktree = await getOptionalWorktreeSelector(
flags,
'parent-worktree',
cwd,
client
)
const explicitParentWorkspace = getPresentStringFlag(flags, 'parent-workspace')
const explicitParent = await resolveCreateParentSelector(flags, cwd, client)
const explicitParentWorktree = explicitParent.parentWorktree
const explicitParentWorkspace = explicitParent.parentWorkspace
const startupAgent = getOptionalStartupAgent(flags)
const setupDecision = getOptionalSetupDecision(flags)
const noParent = flags.get('no-parent') === true
@ -277,7 +258,7 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
printResult(result, json, formatWorktreeShow)
},
'worktree set': async ({ flags, client, cwd, json }) => {
assertParentFlagsCompatible(flags)
assertParentWorktreeFlagsCompatible(flags)
const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue', {
allowNull: true
})

View File

@ -194,7 +194,7 @@ 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>|--project <id> [--host <host-id>]|--project-host-setup <id>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--linear-issue <identifier-or-url>] [--comment <text>] [--parent-workspace <selector>|--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]
orca worktree create --name <name> [--repo <selector>|--project <id> [--host <host-id>]|--project-host-setup <id>] [--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>] [--linear-issue <identifier-or-url|null>] [--comment <text>] [--workspace-status <id>] [--parent-worktree <selector>|--no-parent] [--json]
@ -230,8 +230,7 @@ Selectors:
--repo <selector> Registered repo selector such as id:<id>, name:<name>, or path:<path>
--worktree <selector> Worktree selector such as id:<id>, branch:<branch>, issue:<number>, path:<path>, or active/current
--terminal <handle> Runtime-issued terminal handle returned by \`orca terminal list --json\`
--parent-workspace <selector> Parent workspace selector such as folder:<id> or worktree:<id>
--parent-worktree <selector> Parent worktree selector; create infers a child of the caller/current worktree by default
--parent-worktree <selector> Parent worktree selector such as id:<id>, branch:<branch>, issue:<number>, path:<path>, or active/current
--no-parent Force no parent lineage for unrelated worktree creation/update
Terminal Send Options:
@ -255,7 +254,7 @@ Behavior:
Use selectors for discovery and handles for repeated live terminal operations.
Agent Sessions And Worktrees:
\`worktree create --agent\` creates a new checkout/workspace with an agent.
\`worktree create --agent\` creates a new checkout with an agent.
To start a fresh agent in the current worktree, use:
orca terminal create --worktree active --command "codex"
@ -433,6 +432,9 @@ function formatCommandFlagHelp(flag: string, commandPath: string[]): string {
if (command === 'linear create' && flag === 'parent-current') {
return '--parent-current Use the current linked issue as parent'
}
if (command === 'worktree create' && flag === 'parent-worktree') {
return '--parent-worktree <selector> Parent selector such as active/current, id:<id>, branch:<branch>, issue:<number>, path:<path>, folder:<id>, or worktree:<id>'
}
if (flag === 'key' && command === 'computer hotkey') {
return '--key <key-combo> Modifier chord with one key, e.g. CmdOrCtrl+A'
}
@ -479,10 +481,8 @@ export function formatFlagHelp(flag: string): string {
'no-parent': '--no-parent Force no parent lineage for unrelated work',
'no-screenshot': '--no-screenshot Skip screenshot capture after the operation',
pages: '--pages <n> Number of scroll pages',
'parent-workspace':
'--parent-workspace <selector> Parent workspace selector such as folder:<id>',
'parent-worktree':
'--parent-worktree <selector> Parent selector; create infers the caller/current worktree by default',
'--parent-worktree <selector> Parent worktree selector such as id:<id>, branch:<branch>, issue:<number>, path:<path>, or active/current',
path: '--path <path> Path argument for the command',
prompt: '--prompt <text> Prompt text for agent-backed commands',
query: '--query <text> Search text for matching refs',

View File

@ -99,6 +99,7 @@ import {
normalizeWorktreeSelector
} from './index'
import { GLOBAL_FLAGS } from './args'
import { RuntimeRpcFailureError } from './runtime-client'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
describe('COMMAND_SPECS collision check', () => {
@ -155,7 +156,7 @@ describe('orca root help', () => {
)
expect(logSpy.mock.calls[0][0]).toContain('Agent Sessions And Worktrees:')
expect(logSpy.mock.calls[0][0]).toContain(
'`worktree create --agent` creates a new checkout/workspace with an agent.'
'`worktree create --agent` creates a new checkout with an agent.'
)
expect(logSpy.mock.calls[0][0]).toContain(
'orca terminal create --worktree active --command "codex"'
@ -221,13 +222,47 @@ describe('orca root help', () => {
expect(callMock).not.toHaveBeenCalled()
})
it('hides removed parent-workspace help and scopes create parent selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['--help'], '/tmp/repo')
const rootHelp = String(logSpy.mock.calls[0][0])
expect(rootHelp).not.toContain('--parent-workspace')
expect(rootHelp).toContain('[--parent-worktree <selector>] [--no-parent]')
logSpy.mockClear()
await main(['worktree', 'create', '--help'], '/tmp/repo')
const createHelp = String(logSpy.mock.calls[0][0])
expect(createHelp).not.toContain('--parent-workspace')
expect(createHelp).not.toContain('checkout/workspace')
expect(createHelp).not.toContain('caller workspace')
expect(createHelp).not.toContain('current workspace')
expect(createHelp).not.toContain('active Orca workspace')
expect(createHelp).not.toContain('folderWorkspaceId')
expect(createHelp).toContain('folder:<id>')
expect(createHelp).toContain('folder:<folderId>')
expect(createHelp).toContain('worktree:<id>')
logSpy.mockClear()
await main(['worktree', 'set', '--help'], '/tmp/repo')
const setHelp = String(logSpy.mock.calls[0][0])
expect(setHelp).not.toContain('--parent-workspace')
expect(setHelp).not.toContain('folder:<id>')
expect(setHelp).not.toContain('worktree:<id>')
expect(callMock).not.toHaveBeenCalled()
})
it('distinguishes new worktrees from fresh agent terminals in command 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('This creates a new checkout/workspace')
expect(String(logSpy.mock.calls[0][0])).toContain('This creates a new checkout.')
expect(String(logSpy.mock.calls[0][0])).toContain(
'orca terminal create --worktree active --command "codex"'
)
@ -1251,32 +1286,15 @@ describe('orca cli worktree awareness', () => {
})
})
it('passes an explicit parent workspace through worktree.create without cwd inference', async () => {
it('routes traditional parent-worktree selectors through parentWorktree', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: {
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
}
parentWorktreeId: 'repo-1::/tmp/repo/parent'
},
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
@ -1291,8 +1309,8 @@ describe('orca cli worktree awareness', () => {
'id:repo-1',
'--name',
'child',
'--parent-workspace',
'folder:folder-1',
'--parent-worktree',
'branch:feature/parent',
'--json'
],
'/tmp/repo/parent/src'
@ -1307,13 +1325,80 @@ describe('orca cli worktree awareness', () => {
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:folder-1',
parentWorktree: 'branch:feature/parent',
noParent: false,
callerTerminalHandle: undefined
})
})
it('routes workspace-key parent-worktree selectors through parentWorkspace', async () => {
const cases = [
{ selector: 'folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
},
{ selector: 'id:folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'id:worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
}
]
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const testCase of cases) {
callMock.mockReset()
queueFixtures(
callMock,
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: testCase.parentWorkspace,
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
testCase.selector,
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: testCase.parentWorkspace,
noParent: false,
callerTerminalHandle: undefined
})
}
})
it('passes folder workspace environment lineage through worktree.create', async () => {
process.env.ORCA_WORKSPACE_ID = 'folder:folder-1'
queueFixtures(
@ -1399,6 +1484,68 @@ describe('orca cli worktree awareness', () => {
})
})
it('routes active/current folder workspace parent selectors through parentWorkspace on create', async () => {
const folderWorkspace = {
...buildWorktree('/tmp/folder', '', '', 'folder-workspace:group-1'),
id: 'folder:folder-1',
repoId: 'folder-workspace:group-1',
displayName: 'Folder'
}
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const parentSelector of ['current', 'active']) {
callMock.mockReset()
queueFixtures(
callMock,
worktreeListFixture([folderWorkspace]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
parentSelector,
'--json'
],
'/tmp/folder/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:folder-1',
noParent: false,
callerTerminalHandle: undefined
})
}
})
it('rejects contradictory parent flags on worktree.create before resolving selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
@ -1422,40 +1569,44 @@ describe('orca cli worktree awareness', () => {
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either --parent-worktree or --no-parent, not both.'
'Choose either one parent selector or --no-parent.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects contradictory parent workspace flags on worktree.create', async () => {
it('rejects removed parent-workspace on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
const outputModes = [[], ['--json']]
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-workspace',
'folder:folder-1',
'--parent-worktree',
'current',
'--json'
],
'/tmp/not-managed'
)
for (const outputArgs of outputModes) {
logSpy.mockClear()
errSpy.mockClear()
process.exitCode = priorExitCode
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either --parent-workspace or --parent-worktree, not both.'
)
expect(process.exitCode).toBe(1)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-workspace',
'folder:folder-1',
...outputArgs
],
'/tmp/repo'
)
const output = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(output).toContain('Unknown flag --parent-workspace for command: worktree create')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
}
process.exitCode = priorExitCode
})
@ -1488,6 +1639,67 @@ describe('orca cli worktree awareness', () => {
process.exitCode = priorExitCode
})
it('reports runtime parent selector failures without hidden flag guidance', async () => {
callMock.mockRejectedValueOnce(
new RuntimeRpcFailureError({
id: 'req_create',
ok: false,
error: {
code: 'LINEAGE_PARENT_NOT_FOUND',
message: 'Parent selector was not found.',
data: {
nextSteps: [
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<id>, id:<worktreeId>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.'
]
}
},
_meta: { runtimeId: 'runtime-1' }
})
)
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',
'child',
'--parent-worktree',
'folder:missing',
'--json'
],
'/tmp/repo'
)
const output = String(logSpy.mock.calls[0][0])
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:missing',
noParent: false,
callerTerminalHandle: undefined
})
expect(output).toContain('"ok": false')
expect(output).toContain('Parent selector was not found.')
expect(output).toContain('--parent-worktree selector')
expect(output).not.toContain('--parent-workspace')
expect(errSpy).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes no-parent through worktree.create and skips cwd inference', 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>|--project <id> [--host <host-id>]|--project-host-setup <id>] [--agent <id>] [--prompt <text>] [--setup run|skip|inherit] [--base-branch <ref>] [--issue <number>] [--linear-issue <identifier-or-url>] [--comment <text>] [--parent-workspace <selector>|--parent-worktree <selector>] [--no-parent] [--run-hooks] [--activate] [--json]',
'orca worktree create --name <name> [--repo <selector>|--project <id> [--host <host-id>]|--project-host-setup <id>] [--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',
@ -117,20 +117,19 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
'linear-issue',
'comment',
'setup',
'parent-workspace',
'parent-worktree',
'no-parent',
'run-hooks',
'activate'
],
notes: [
'This creates a new checkout/workspace. For a fresh agent in an existing worktree, use `orca terminal create --worktree active --command "codex"` instead.',
'By default, Orca records the new worktree as a child of the caller workspace when it can infer one from the Orca terminal or current directory.',
'This creates a new checkout. For a fresh agent in an existing worktree, use `orca terminal create --worktree active --command "codex"` instead.',
'By default, Orca records the new worktree as a child of the caller context when it can infer one from the Orca terminal or current directory.',
'If --repo is omitted, Orca infers the repo from the current Orca-managed worktree.',
'Use --project with --host to create on a ready project host setup without spelling the backing repo id.',
'For related work, use the inferred parent or pass --parent-workspace folder:<id> or worktree:<id>, or --parent-worktree active, to make the relationship explicit.',
'Use --no-parent when the new worktree should be independent of the current workspace.',
'By default this creates the worktree and its first terminal without switching the active Orca workspace.',
'For related work, use the inferred parent or pass --parent-worktree active, folder:<id>, or worktree:<id> to make the relationship explicit.',
'Use --no-parent when the new worktree should be independent of the current context.',
'By default this creates the worktree and its first terminal without switching the active Orca view.',
'Pass --agent to launch an agent in the first terminal; --prompt sends initial work to that agent.',
'Repo-defined setup hooks follow the repository setup policy; pass --setup run to force them.',
'Pass --activate when the CLI caller intentionally wants to reveal the new worktree in the app.',
@ -142,7 +141,7 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
'orca worktree create --project github:stablyai/orca --host runtime:gpu --name benchmark --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 folder-child --parent-workspace folder:<folderWorkspaceId> --json',
'orca worktree create --repo id:<repoId> --name folder-child --parent-worktree folder:<folderId> --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'
]

View File

@ -14393,7 +14393,7 @@ describe('OrcaRuntimeService', () => {
expect.objectContaining({
code: 'LINEAGE_PARENT_CONTEXT_MISSING',
message:
'Worktree created, but Orca could not validate the current directory as a parent workspace.'
'Worktree created, but Orca could not validate the current directory as a parent context.'
})
])
})

View File

@ -11911,13 +11911,13 @@ export class OrcaRuntimeService {
if (!worktree.instanceId || !parent.instanceId) {
throw new RuntimeLineageError(
'LINEAGE_PARENT_CONTEXT_MISSING',
'Workspace instance identity was unavailable.'
'Worktree instance identity was unavailable.'
)
}
if (!this.store.setWorktreeLineage) {
throw new RuntimeLineageError(
'LINEAGE_PARENT_CONTEXT_MISSING',
'Workspace lineage storage was unavailable.'
'Worktree lineage storage was unavailable.'
)
}
const createdAt = Date.now()
@ -14096,7 +14096,7 @@ export class OrcaRuntimeService {
const childWorktreeId = child.id
const parentWorktreeId = parent.id
if (childWorktreeId === parentWorktreeId) {
throw new RuntimeLineageError('LINEAGE_PARENT_CYCLE', 'A workspace cannot parent itself.')
throw new RuntimeLineageError('LINEAGE_PARENT_CYCLE', 'A worktree cannot parent itself.')
}
const instanceByWorktreeId = new Map(
this.resolvedWorktreeCache?.worktrees.map((worktree) => [
@ -14113,7 +14113,7 @@ export class OrcaRuntimeService {
if (visited.has(cursor)) {
throw new RuntimeLineageError(
'LINEAGE_PARENT_CYCLE',
'Parent workspace would create a lineage cycle.'
'Parent selector would create a lineage cycle.'
)
}
visited.add(cursor)
@ -14143,13 +14143,13 @@ export class OrcaRuntimeService {
if (input.noParent === true && (input.parentWorkspace || input.parentWorktree)) {
throw new RuntimeLineageError(
'LINEAGE_PARENT_CONTEXT_CONFLICT',
'Choose either a parent workspace flag or --no-parent, not both.'
'Choose either one parent selector or --no-parent.'
)
}
if (input.parentWorkspace && input.parentWorktree) {
throw new RuntimeLineageError(
'LINEAGE_PARENT_CONTEXT_CONFLICT',
'Choose either --parent-workspace or --parent-worktree, not both.'
'Choose either one parent selector or --no-parent.'
)
}
@ -14168,10 +14168,10 @@ export class OrcaRuntimeService {
} catch {
throw new RuntimeLineageError(
'LINEAGE_PARENT_NOT_FOUND',
'Parent workspace was not found.',
'Parent selector was not found.',
{
nextSteps: [
'Pass a valid --parent-workspace selector such as folder:<id> or worktree:<id>.',
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<id>, id:<worktreeId>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.'
]
}
@ -14196,10 +14196,10 @@ export class OrcaRuntimeService {
} catch {
throw new RuntimeLineageError(
'LINEAGE_PARENT_NOT_FOUND',
'Parent workspace was not found.',
'Parent selector was not found.',
{
nextSteps: [
'Run `orca worktree list` and pass a valid --parent-worktree selector.',
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<id>, id:<worktreeId>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.'
]
}
@ -14222,7 +14222,7 @@ export class OrcaRuntimeService {
warnings.push({
code: 'LINEAGE_PARENT_CONTEXT_MISSING',
message:
'Worktree created, but Orca could not validate the environment parent workspace.',
'Worktree created, but Orca could not validate the environment parent context.',
details: { envParentWorkspace: input.envParentWorkspace }
})
}
@ -14291,7 +14291,7 @@ export class OrcaRuntimeService {
warnings.push({
code: 'LINEAGE_PARENT_CONTEXT_MISSING',
message:
'Worktree created, but Orca could not validate the caller terminal as a parent workspace.',
'Worktree created, but Orca could not validate the caller terminal as a parent context.',
details: { callerTerminalHandle: input.callerTerminalHandle }
})
}
@ -14307,7 +14307,7 @@ export class OrcaRuntimeService {
warnings.push({
code: 'LINEAGE_PARENT_CONTEXT_MISSING',
message:
'Worktree created, but Orca could not validate the current directory as a parent workspace.',
'Worktree created, but Orca could not validate the current directory as a parent context.',
details: { cwdParentWorktree: input.cwdParentWorktree }
})
}
@ -14331,7 +14331,7 @@ export class OrcaRuntimeService {
warnings: [
{
code: 'LINEAGE_PARENT_CONTEXT_CONFLICT',
message: 'Worktree created, but Orca could not prove which parent workspace caused it.',
message: 'Worktree created, but Orca could not prove which parent context caused it.',
details: {
terminalParentWorkspaceKey: candidates.find((c) => c.source === 'terminal-context')
?.parent.workspaceKey,

View File

@ -108,7 +108,7 @@ describe('mapRuntimeError', () => {
const response = mapRuntimeError(
'req_1',
{ runtimeId: 'runtime-1' },
new LineageError('Parent workspace was not found.')
new LineageError('Parent selector was not found.')
)
expect(response).toEqual({
@ -116,7 +116,7 @@ describe('mapRuntimeError', () => {
ok: false,
error: {
code: 'LINEAGE_PARENT_NOT_FOUND',
message: 'Parent workspace was not found.',
message: 'Parent selector was not found.',
data: {
nextSteps: ['Run `orca worktree list`.', 'Retry with --no-parent.']
}

View File

@ -143,13 +143,13 @@ export const WorktreeCreate = z
if ((params.parentWorkspace || params.parentWorktree) && params.noParent === true) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Choose either a parent workspace flag or --no-parent, not both.'
message: 'Choose either one parent selector or --no-parent.'
})
}
if (params.parentWorkspace && params.parentWorktree) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Choose either --parent-workspace or --parent-worktree, not both.'
message: 'Choose either one parent selector or --no-parent.'
})
}
if (params.startupPrompt !== undefined && params.startupAgent === undefined) {

View File

@ -529,9 +529,7 @@ describe('worktree RPC methods', () => {
)
expect(response).toMatchObject({ ok: false })
expect(JSON.stringify(response)).toContain(
'Choose either a parent workspace flag or --no-parent'
)
expect(JSON.stringify(response)).toContain('Choose either one parent selector or --no-parent')
expect(runtime.createManagedWorktree).not.toHaveBeenCalled()
})