From cfc003452c303a805243ad047c3ebf3fdeda1043 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:53:19 -0700 Subject: [PATCH] Hide workspace parent flag from worktree create CLI (#5743) --- skills/orca-cli/SKILL.md | 20 +- src/cli/format.test.ts | 8 +- .../worktree-create-parent-selector.ts | 60 ++++ src/cli/handlers/worktree-lineage-summary.ts | 2 +- src/cli/handlers/worktree.ts | 39 +-- src/cli/help.ts | 14 +- src/cli/index.test.ts | 308 +++++++++++++++--- src/cli/specs/core.ts | 15 +- src/main/runtime/orca-runtime.test.ts | 2 +- src/main/runtime/orca-runtime.ts | 28 +- src/main/runtime/rpc/errors.test.ts | 4 +- .../runtime/rpc/methods/worktree-schemas.ts | 4 +- src/main/runtime/rpc/methods/worktree.test.ts | 4 +- 13 files changed, 381 insertions(+), 127 deletions(-) create mode 100644 src/cli/handlers/worktree-create-parent-selector.ts diff --git a/skills/orca-cli/SKILL.md b/skills/orca-cli/SKILL.md index 7c04adb37d3..96187c9bd84 100644 --- a/skills/orca-cli/SKILL.md +++ b/skills/orca-cli/SKILL.md @@ -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 --json orca worktree create --repo id: --name related-task --json +orca worktree create --repo id: --name related-task --parent-worktree active --json +orca worktree create --repo id: --name folder-child --parent-worktree folder: --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: --display-name "My Task" --json @@ -66,11 +68,13 @@ Selectors: - `id:`, `path:`, `branch:`, `issue:` - `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:`, `worktree:`, `id:folder:`, `id:worktree:` 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:` or `--parent-worktree worktree:` 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 --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 diff --git a/src/cli/format.test.ts b/src/cli/format.test.ts index cd614976015..07e1879f613 100644 --- a/src/cli/format.test.ts +++ b/src/cli/format.test.ts @@ -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:, worktree:, id:, branch:, issue:, 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:, worktree:, id:, branch:, issue:, path:, or active/current.', 'Next step: Retry with --no-parent to create without lineage.' ].join('\n') ) diff --git a/src/cli/handlers/worktree-create-parent-selector.ts b/src/cli/handlers/worktree-create-parent-selector.ts new file mode 100644 index 00000000000..86b1da40221 --- /dev/null +++ b/src/cli/handlers/worktree-create-parent-selector.ts @@ -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): 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, + cwd: string, + client: RuntimeClient +): Promise { + 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 + } +} diff --git a/src/cli/handlers/worktree-lineage-summary.ts b/src/cli/handlers/worktree-lineage-summary.ts index 79d1d70c742..4e3d187abe9 100644 --- a/src/cli/handlers/worktree-lineage-summary.ts +++ b/src/cli/handlers/worktree-lineage-summary.ts @@ -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' } diff --git a/src/cli/handlers/worktree.ts b/src/cli/handlers/worktree.ts index 884f53d81b4..b4547cfe69a 100644 --- a/src/cli/handlers/worktree.ts +++ b/src/cli/handlers/worktree.ts @@ -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): void { +function assertParentWorktreeFlagsCompatible(flags: Map): 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): 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 = { 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 = { printResult(result, json, formatWorktreeShow) }, 'worktree set': async ({ flags, client, cwd, json }) => { - assertParentFlagsCompatible(flags) + assertParentWorktreeFlagsCompatible(flags) const linearIssueLink = getOptionalLinearIssueLinkFlag(flags, 'linear-issue', { allowNull: true }) diff --git a/src/cli/help.ts b/src/cli/help.ts index 6c42337d2ed..8e3826f9322 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -194,7 +194,7 @@ Common Commands: orca environment show --environment [--json] orca environment rm --environment [--json] orca worktree list [--repo ] [--limit ] [--json] - orca worktree create --name [--repo |--project [--host ]|--project-host-setup ] [--agent ] [--prompt ] [--setup run|skip|inherit] [--base-branch ] [--issue ] [--linear-issue ] [--comment ] [--parent-workspace |--parent-worktree ] [--no-parent] [--run-hooks] [--activate] [--json] + orca worktree create --name [--repo |--project [--host ]|--project-host-setup ] [--agent ] [--prompt ] [--setup run|skip|inherit] [--base-branch ] [--issue ] [--linear-issue ] [--comment ] [--parent-worktree ] [--no-parent] [--run-hooks] [--activate] [--json] orca worktree show --worktree [--json] orca worktree current [--json] orca worktree set --worktree [--display-name ] [--issue ] [--linear-issue ] [--comment ] [--workspace-status ] [--parent-worktree |--no-parent] [--json] @@ -230,8 +230,7 @@ Selectors: --repo Registered repo selector such as id:, name:, or path: --worktree Worktree selector such as id:, branch:, issue:, path:, or active/current --terminal Runtime-issued terminal handle returned by \`orca terminal list --json\` - --parent-workspace Parent workspace selector such as folder: or worktree: - --parent-worktree Parent worktree selector; create infers a child of the caller/current worktree by default + --parent-worktree Parent worktree selector such as id:, branch:, issue:, 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 Parent selector such as active/current, id:, branch:, issue:, path:, folder:, or worktree:' + } if (flag === 'key' && command === 'computer hotkey') { return '--key 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 Number of scroll pages', - 'parent-workspace': - '--parent-workspace Parent workspace selector such as folder:', 'parent-worktree': - '--parent-worktree Parent selector; create infers the caller/current worktree by default', + '--parent-worktree Parent worktree selector such as id:, branch:, issue:, path:, or active/current', path: '--path Path argument for the command', prompt: '--prompt Prompt text for agent-backed commands', query: '--query Search text for matching refs', diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index e9e2592f660..832df7042f3 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -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 ] [--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:') + expect(createHelp).toContain('folder:') + expect(createHelp).toContain('worktree:') + + 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:') + expect(setHelp).not.toContain('worktree:') + 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:, worktree:, id:, branch:, issue:, 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, diff --git a/src/cli/specs/core.ts b/src/cli/specs/core.ts index 6ce508016c8..e8d6cf4bc6e 100644 --- a/src/cli/specs/core.ts +++ b/src/cli/specs/core.ts @@ -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 [--repo |--project [--host ]|--project-host-setup ] [--agent ] [--prompt ] [--setup run|skip|inherit] [--base-branch ] [--issue ] [--linear-issue ] [--comment ] [--parent-workspace |--parent-worktree ] [--no-parent] [--run-hooks] [--activate] [--json]', + 'orca worktree create --name [--repo |--project [--host ]|--project-host-setup ] [--agent ] [--prompt ] [--setup run|skip|inherit] [--base-branch ] [--issue ] [--linear-issue ] [--comment ] [--parent-worktree ] [--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: or worktree:, 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:, or worktree: 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: --name linear-task --linear-issue https://linear.app/stably/issue/STA-335/test-issue --json', 'orca worktree create --repo id: --name agent-task --agent codex --prompt "hi" --json', - 'orca worktree create --repo id: --name folder-child --parent-workspace folder: --json', + 'orca worktree create --repo id: --name folder-child --parent-worktree folder: --json', 'orca worktree create --repo id: --name related-task --parent-worktree active --json', 'orca worktree create --repo id: --name independent-task --no-parent --json' ] diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 995045068e7..c17547b2adc 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -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.' }) ]) }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 376d3462d31..aab4b825546 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -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: or worktree:.', + 'Pass a valid --parent-worktree selector such as folder:, worktree:, id:, branch:, issue:, 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:, worktree:, id:, branch:, issue:, 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, diff --git a/src/main/runtime/rpc/errors.test.ts b/src/main/runtime/rpc/errors.test.ts index e3a90d11384..a14dc809c54 100644 --- a/src/main/runtime/rpc/errors.test.ts +++ b/src/main/runtime/rpc/errors.test.ts @@ -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.'] } diff --git a/src/main/runtime/rpc/methods/worktree-schemas.ts b/src/main/runtime/rpc/methods/worktree-schemas.ts index c0e5e4fc83d..8382b2b268b 100644 --- a/src/main/runtime/rpc/methods/worktree-schemas.ts +++ b/src/main/runtime/rpc/methods/worktree-schemas.ts @@ -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) { diff --git a/src/main/runtime/rpc/methods/worktree.test.ts b/src/main/runtime/rpc/methods/worktree.test.ts index 6defbe29579..00f835cef34 100644 --- a/src/main/runtime/rpc/methods/worktree.test.ts +++ b/src/main/runtime/rpc/methods/worktree.test.ts @@ -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() })