Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/site/content/docs/cli/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ orca worktree set --worktree active --comment "reproduced bug" --json
orca worktree set --worktree active --gitlab-issue 42 --gitlab-mr 77 --json
orca worktree set --worktree active --pr null --json
orca worktree rm --worktree id:<id> --force --json
orca worktree sleep --worktree active --json
orca worktree wake --worktree id:<id> --json
```

For selectors, setup flags, parent/child worktrees, and the broader command map, see [Orca CLI reference](/docs/cli/reference).
Expand Down
8 changes: 6 additions & 2 deletions docs/site/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,12 @@ orca worktree create --name child-task --agent codex --prompt "Investigate the f
orca worktree set --worktree active --comment "reproduced failure; testing token refresh fix" --json
orca worktree set --worktree active --workspace-status in-review --unread --json
orca worktree rm --worktree id:<worktreeId> --force --json
orca worktree sleep --worktree active --json
orca worktree wake --worktree id:<worktreeId> --json
```

`worktree wake` resumes the agents a workspace sleep left running and those Agent sleep put to sleep, without switching what the desktop shows, so a script can send them work without taking over your view. Finished agents in a workspace slept with `worktree sleep` or the Sleep action resume when you open their tab instead.

When `worktree create` runs from inside an Orca-managed worktree, Orca records the new worktree as a child when it can infer the relationship. Pass `--parent-worktree active` to be explicit, or `--no-parent` when the new work is independent.

`worktree set --unread` puts the unread dot on the workspace in the sidebar, the same one Orca shows when an agent finishes; `--read` clears it.
Expand Down Expand Up @@ -184,8 +188,8 @@ Omit `--terminal` to target the active terminal in the current worktree. Read be

Close stops the process as part of removing the terminal. Use `--terminal <handle>` for one
terminal, or `--worktree <selector> --all` to durably close every terminal in exactly that
workspace, including its saved layouts and agent-resume records. Use workspace Sleep when those
terminals should resume later. If the execution host cannot confirm that every PTY stopped, bulk
workspace, including its saved layouts and agent-resume records. Use workspace Sleep
(`orca worktree sleep`) when those terminals should resume later. If the execution host cannot confirm that every PTY stopped, bulk
close returns a failing `unverifiable` result rather than claiming the processes exited.
`terminal stop` remains only for compatibility with older tooling.

Expand Down
5 changes: 4 additions & 1 deletion skill-guides/orca-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ ORCA worktree create --repo id:<repoId> --name review-task --pr 123 --json
ORCA worktree set --worktree active --gitlab-issue '#42' --gitlab-mr '!77' --json
ORCA worktree set --worktree active --pr null --gitlab-mr null --json
ORCA worktree rm --worktree id:<repoId>::<worktreePath> --force --json
ORCA worktree sleep --worktree active --json
ORCA worktree wake --worktree id:<repoId>::<worktreePath> --json
```

Use `repo set --external-worktree-visibility show` to show a repo's non-Orca worktrees.
Expand Down Expand Up @@ -217,7 +219,8 @@ Terminal rules:
- `--terminal` is optional for most commands; omitted means the active terminal in the current worktree.
- Use `terminal close --terminal <handle>` to close one terminal. Use `terminal close --worktree <selector> --all` to stop every terminal process in exactly that workspace and durably remove its terminal tabs, layouts, and agent-resume records.
- A bulk close fails when the execution host cannot confirm every PTY stopped. Treat that as `unverifiable`; do not report the processes as exited or retry against another host.
- Use workspace Sleep, not close, when the terminals and agent sessions should resume later. `terminal stop` is legacy compatibility plumbing and should not be used in new agent workflows.
- Use workspace Sleep (`worktree sleep`), not close, when the terminals and agent sessions should resume later. `terminal stop` is legacy compatibility plumbing and should not be used in new agent workflows.
- To send work to an agent that Agent sleep put to sleep, run `worktree wake` first: it resumes the agents in the background and does not switch the user's desktop view, unlike `file open --focus` or `terminal switch`.
- `terminal list --json` omits `visualLayouts` to keep the common agent payload bounded. Add `--include-visual-layouts` only when tab and pane topology is required.
- A paired server has no window. `terminal switch --environment <name>` moves the desktop app on this machine to that server's terminal, and reports `navigated: false` when no desktop here shows that server.
- Use `terminal read` before `terminal send` unless the next input is obvious.
Expand Down
4 changes: 2 additions & 2 deletions src/cli/bundled-skill-guides.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/cli/handler-group-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ export const HANDLER_GROUPS: readonly HandlerGroup[] = [
'worktree current',
'worktree create',
'worktree set',
'worktree rm'
'worktree rm',
'worktree sleep',
'worktree wake'
],
load: async () => (await import('./handlers/worktree.js')).WORKTREE_HANDLERS
},
Expand Down
30 changes: 30 additions & 0 deletions src/cli/handlers/worktree-sleep-wake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { CommandHandler } from '../dispatch'
import { printResult } from '../format'
import { getRequiredWorktreeSelector } from '../selectors'

export const WORKTREE_SLEEP_WAKE_HANDLERS: Record<string, CommandHandler> = {
'worktree sleep': async ({ flags, client, cwd, json }) => {
const result = await client.call<{
worktreeId: string
chatFailed?: boolean
vmSuspendFailure?: string
}>('worktree.sleep', {
worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client)
})
// Why: the sleep has finished when this returns; a terminal stop it could not prove fails the call.
printResult(result, json, (value) =>
[
`slept: ${value.worktreeId}`,
...(value.chatFailed ? ['warning: a host could not confirm its chats stopped'] : []),
...(value.vmSuspendFailure ? [`warning: VM still running: ${value.vmSuspendFailure}`] : [])
].join('\n')
)
},
'worktree wake': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ worktreeId: string; sleepingAgentWake: string }>(
'worktree.wake',
{ worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client) }
)
printResult(result, json, (value) => `sleepingAgentWake: ${value.sleepingAgentWake}`)
}
}
4 changes: 3 additions & 1 deletion src/cli/handlers/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from '../../shared/runtime-types'
import type { CommandHandler } from '../dispatch'
import { printHookWarning, printPreservedBranchWarning } from './worktree-removal-warnings'
import { WORKTREE_SLEEP_WAKE_HANDLERS } from './worktree-sleep-wake'
import { formatWorktreeList, formatWorktreePs, formatWorktreeShow, printResult } from '../format'
import {
annotateOmittedHostScope,
Expand Down Expand Up @@ -291,5 +292,6 @@ export const WORKTREE_HANDLERS: Record<string, CommandHandler> = {
? `removed: ${value.removed}\nOrca is still deleting the checkout in the background.`
: `removed: ${value.removed}`
)
}
},
...WORKTREE_SLEEP_WAKE_HANDLERS
}
14 changes: 14 additions & 0 deletions src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ describe('command aliases dispatch to the canonical handler', () => {
)
})

it('sends worktree sleep and wake to their runtime methods with the resolved selector', async () => {
queueFixtures(
callMock,
okFixture('req_sleep', { worktreeId: 'wt-1' }),
okFixture('req_wake', { worktreeId: 'wt-1', sleepingAgentWake: 'requested' })
)

await main(['worktree', 'sleep', '--worktree', 'id:wt-1', '--json'], '/tmp/repo')
await main(['worktree', 'wake', '--worktree', 'id:wt-1', '--json'], '/tmp/repo')

expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.sleep', { worktree: 'id:wt-1' })
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.wake', { worktree: 'id:wt-1' })
})

it('still runs `terminal focus` after the handler de-duplication', async () => {
queueFixtures(callMock, okFixture('req', { focus: { ok: true } }))

Expand Down
2 changes: 2 additions & 0 deletions src/cli/root-help-text-primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export const ROOT_HELP_TEXT_PRIMARY = [
' worktree create Create a new Orca-managed worktree',
' worktree set Update Orca metadata for a worktree',
' worktree rm Remove a worktree from Orca and git',
' worktree sleep Sleep a workspace, keeping its agent sessions to resume later',
" worktree wake Resume a workspace's slept agents without switching the desktop view",
' worktree ps Show a compact orchestration summary across worktrees',
'',
'Agents:',
Expand Down
2 changes: 2 additions & 0 deletions src/cli/root-help-text-secondary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const ROOT_HELP_TEXT_SECONDARY = [
' orca worktree current [--json]',
' orca worktree set --worktree <selector> [--display-name <name>] [--issue <number|null>] [--pr <number|null>] [--linear-issue <identifier-or-url|null>] [--gitlab-issue <number-or-url|null>] [--gitlab-mr <number-or-url|null>] [--comment <text>] [--workspace-status <id>] [--unread|--read] [--parent-worktree <selector>|--no-parent] [--json]',
' orca worktree rm --worktree <selector> [--force] [--run-hooks] [--allow-failed-archive-hook] [--json]',
' orca worktree sleep --worktree <selector> [--json]',
' orca worktree wake --worktree <selector> [--json]',
' orca worktree ps [--limit <n>] [--json]',
' orca file open <path> [--line <n> [--column <n>]] [--worktree <selector>] [--focus] [--json]',
' orca file diff <path> [--staged] [--worktree <selector>] [--focus] [--json]',
Expand Down
21 changes: 21 additions & 0 deletions src/cli/specs/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,27 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
'For Git worktrees, removal also attempts to delete the checked-out local branch, with or without --force. Orca retains branches it knows predated the worktree and any branch whose changes it cannot prove are already merged.'
]
},
{
path: ['worktree', 'sleep'],
summary: 'Sleep a workspace, keeping its agent sessions to resume later',
usage: 'orca worktree sleep --worktree <selector> [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'worktree'],
notes: [
'Same as the workspace Sleep action: stops its terminals and chats and records each agent session for resume. Returns once the sleep finished; fails when a terminal stop cannot be proven.'
]
},
{
path: ['worktree', 'wake'],
summary: "Resume a workspace's slept agents without switching the desktop view",
usage: 'orca worktree wake --worktree <selector> [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'worktree'],
notes: [
'Resumes agents a workspace sleep left running and agents slept by Agent sleep, in hidden panes; the desktop keeps showing what it showed. Returns once the wake is queued, behind any sleep still running.',
'Finished agents in a workspace slept with `orca worktree sleep` or the Sleep action resume when their tab is opened, not here.',
'result.sleepingAgentWake is requested, not-applicable, or unsupported-headless (the slept agents only a desktop window can wake, and headless `orca serve` has none).'
],
examples: ['orca worktree wake --worktree active --json']
},
{
path: ['worktree', 'ps'],
summary: 'Show a compact orchestration summary across worktrees',
Expand Down
2 changes: 1 addition & 1 deletion src/cli/specs/terminal-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const TERMINAL_CLOSE_COMMAND_SPEC: CommandSpec = {
notes: [
'Without --all, closes one terminal pane/session; add --tab to close its whole tab.',
'With --worktree <selector> --all, stops every terminal process owned by that workspace and durably removes its terminal tabs, layouts, and resume records.',
'Use workspace Sleep when the terminals and agent sessions should resume later.'
'Use workspace Sleep (`orca worktree sleep`) when the terminals and agent sessions should resume later.'
],
examples: [
'orca terminal close --terminal term_abc123',
Expand Down
125 changes: 125 additions & 0 deletions src/main/runtime/orca-runtime-tests/worktree-wake.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, it, vi } from 'vitest'
import {
OrcaRuntimeService,
electronMocks,
getDefaultWorkspaceSession
} from '../orca-runtime-test-mocks.spec'
import type { SleepingAgentSessionRecord } from '../orca-runtime-test-mocks.spec'
import {
TEST_REPO_ID,
TEST_WINDOW_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
store
} from '../orca-runtime-test-fixtures.spec'

function makeRecord(overrides: Partial<SleepingAgentSessionRecord> = {}): SleepingAgentSessionRecord {
return {
paneKey: 'tab-1:leaf-1',
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
agent: 'claude',
providerSession: { key: 'session_id', id: 'session-1' },
prompt: 'test',
state: 'done',
capturedAt: 1,
updatedAt: 1,
...overrides
}
}

function startRuntime(opts: {
window: boolean
record: SleepingAgentSessionRecord
ownerHostId?: 'local' | 'ssh:ssh-1'
}): {
runtime: OrcaRuntimeService
notifier: { activateWorktree: ReturnType<typeof vi.fn>; resumeSleepingAgents: ReturnType<typeof vi.fn> }
readHostIds: () => (string | undefined)[]
} {
const ownerHostId = opts.ownerHostId ?? 'local'
const repo = { ...store.getRepos()[0]!, executionHostId: ownerHostId }
// Only the owning host's partition holds the record, so a wrong partition read misses it.
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
{
...getDefaultWorkspaceSession(),
sleepingAgentSessionsByPaneKey: { [opts.record.paneKey]: opts.record }
},
ownerHostId
)
const getWorkspaceSession = vi.fn((hostId?: string) =>
hostId === undefined ? getDefaultWorkspaceSession() : runtimeStore.getWorkspaceSession(hostId)
)
const runtime = new OrcaRuntimeService({
...runtimeStore,
getRepos: () => [repo],
getRepo: (id: string) => (id === TEST_REPO_ID ? repo : undefined),
getWorkspaceSession
})
const notifier = {
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
resumeSleepingAgents: vi.fn()
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements every notifier member the runtime calls during attach and wake.
runtime.setNotifier(notifier as never)
electronMocks.BrowserWindow.fromId.mockReturnValue(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: getAvailableAuthoritativeWindow only checks isDestroyed on the found window.
(opts.window ? { isDestroyed: () => false } : null) as never
)
runtime.attachWindow(TEST_WINDOW_ID)
runtime.markGraphReady(TEST_WINDOW_ID)
return {
runtime,
notifier,
readHostIds: () => getWorkspaceSession.mock.calls.map(([hostId]) => hostId)
}
}

describe('OrcaRuntimeService.wakeManagedWorktree', () => {
it('queues the wake of slept agents without navigating the desktop', async () => {
const { runtime, notifier } = startRuntime({ window: true, record: makeRecord() })

const result = await runtime.wakeManagedWorktree(`id:${TEST_WORKTREE_ID}`)

expect(result).toEqual({ worktreeId: TEST_WORKTREE_ID, sleepingAgentWake: 'requested' })
await vi.waitFor(() =>
expect(notifier.resumeSleepingAgents).toHaveBeenCalledWith(TEST_WORKTREE_ID)
)
expect(notifier.activateWorktree).not.toHaveBeenCalled()
})

it('reports unsupported-headless for a hibernated agent only a window wakes', async () => {
const { runtime } = startRuntime({ window: false, record: makeRecord() })

const result = await runtime.wakeManagedWorktree(`id:${TEST_WORKTREE_ID}`)

expect(result.sleepingAgentWake).toBe('unsupported-headless')
})

// Main resumes a workspace sleep's running agent itself, so a headless host wakes it too.
it('reports a requested wake headless for a workspace sleep record', async () => {
const { runtime } = startRuntime({
window: false,
record: makeRecord({ origin: 'worktree-sleep', state: 'working' })
})

const result = await runtime.wakeManagedWorktree(`id:${TEST_WORKTREE_ID}`)

expect(result.sleepingAgentWake).toBe('requested')
})

it("reads the sleeping records of the worktree's host", async () => {
const { runtime, readHostIds } = startRuntime({
window: false,
record: makeRecord(),
ownerHostId: 'ssh:ssh-1'
})

const result = await runtime.wakeManagedWorktree(`id:${TEST_WORKTREE_ID}`)

expect(readHostIds()).toContain('ssh:ssh-1')
expect(result.sleepingAgentWake).toBe('unsupported-headless')
})
})
1 change: 1 addition & 0 deletions src/main/runtime/orca-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec')
await import('./orca-runtime-tests/mobile-session-tabs-cold-serve-hydrate.spec')
await import('./orca-runtime-tests/mobile-session-tabs-empty-headless-snapshot.spec')
await import('./orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec')
await import('./orca-runtime-tests/worktree-wake.spec')
await import('./orca-runtime-tests/mobile-session-tabs-headless-chat-rebuild.spec')
await import('./orca-runtime-tests/mobile-session-tabs-shell-selection.spec')
await import('./orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec')
Expand Down
49 changes: 49 additions & 0 deletions src/main/runtime/rpc/methods/worktree-sleep-wake-methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import '../unused-default-rpc-methods.test-fixture'
import { describe, expect, it, vi } from 'vitest'
import { RpcDispatcher } from '../dispatcher'
import type { RpcRequest } from '../core'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { WORKTREE_METHODS } from './worktree'

function makeRequest(method: string, params?: unknown): RpcRequest {
return { id: 'req-1', authToken: 'tok', method, params }
}

describe('worktree.wake', () => {
it('wakes slept agents for a local CLI caller, which has no mobile clientKind', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worktree.wake reads only the runtime members stubbed here.
const runtime = {
getRuntimeId: () => 'test-runtime',
wakeManagedWorktree: vi
.fn()
.mockResolvedValue({ worktreeId: 'wt-1', sleepingAgentWake: 'requested' }),
activateManagedWorktree: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })

const response = await dispatcher.dispatch(
makeRequest('worktree.wake', { worktree: 'id:wt-1' })
)

expect(response).toMatchObject({
ok: true,
result: { worktreeId: 'wt-1', sleepingAgentWake: 'requested' }
})
expect(runtime.wakeManagedWorktree).toHaveBeenCalledWith('id:wt-1')
expect(runtime.activateManagedWorktree).not.toHaveBeenCalled()
})

it('rejects a call without a worktree selector', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worktree.wake reads only the runtime members stubbed here.
const runtime = {
getRuntimeId: () => 'test-runtime',
wakeManagedWorktree: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })

const response = await dispatcher.dispatch(makeRequest('worktree.wake', {}))

expect(response).toMatchObject({ ok: false })
expect(runtime.wakeManagedWorktree).not.toHaveBeenCalled()
})
})
15 changes: 15 additions & 0 deletions src/main/runtime/rpc/methods/worktree-sleep-wake-methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { WorktreeSelector } from '../../../../shared/rpc-contract/worktree-params'
import { defineMethod } from '../core'
import { WORKTREE_SLEEP_METHOD } from './worktree-sleep-method'

export const WORKTREE_SLEEP_WAKE_METHODS = [
WORKTREE_SLEEP_METHOD,
// Why: the background wake mobile activation uses, opened to every caller so a
// CLI can resume slept agents without moving the desktop to the workspace.
defineMethod({
name: 'worktree.wake',
permission: 'workspace',
params: WorktreeSelector,
handler: async (params, { runtime }) => runtime.wakeManagedWorktree(params.worktree)
})
]
Loading