Hide workspace parent flag from worktree create CLI (#5743)
This commit is contained in:
parent
0659ba0aa8
commit
cfc003452c
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
})
|
||||
])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue