Files
orca/src/shared/worktree-runtime-capabilities.ts
T
Brennan Benson fb5a8a0860 Run setup and archive scripts for folder projects (#40034)
* feat(workspaces): run setup and archive scripts for folder projects

Folder projects get the setup and archive scripts git projects have:
creating a workspace runs setup in a Setup tab, and removing one runs
archive first. The script sees the workspace's name and id, so
`orca worktree create` inside it attaches each new worktree to that
workspace, however the workspace was created.

- Folder creates (desktop IPC and runtime) prepare setup through the
  shared setup prep; runtime creates start it with the worktree terminal
  startup, so Setup-before-agent ordering matches.
- Runner files live in <folder>/.orca, one fresh file per launch, removed
  with the workspace.
- Only the host whose catalog holds the folder reads its orca.yaml or runs
  its scripts; a setup problem becomes a warning, never a failed create.
- Shared orca.yaml setup with no approval from an older client is skipped;
  ask/inherit never refuses a folder create.
- No orca.yaml default tabs for folders, since every workspace shares the
  directory.
- New capability worktree.folder-setup.v1 gates the Settings section on
  runtime hosts; the composer, Settings and trust prompt reuse the
  existing setup surfaces.

* i18n: add folder workspace hooks strings

* chore: drop the runner's sync-fs baseline and an array cast

* fix(workspaces): close review gaps in folder project setup

- Whole-project trust sends an explicit run for folder creates the
  policy would run, since it carries no content hash.
- Folder setup skips file access for a skipped setup and never starts a
  stopped WSL distro for a create nobody started; removal does the same.
- Runtime folder creates return the trust mismatch, so the window re-asks.
- The composer hides folder setup controls on hosts without
  worktree.folder-setup.v1 and keeps their old explicit skip.
- Hook checks read a folder only on the host that owns it.
- Git archive hooks get the removed workspace's id; WSL hooks blank
  inherited pane identity too.
- Runner files are written through the drive path WSL needs for its
  drive mounts, and kept out of git status with .orca/.gitignore.
- The CLI's older-host warning covers --agent creates and --json output.

* fix(workspaces): wait for the folder setup capability and read orca.yaml once

Create now waits for a runtime host's folder-setup answer instead of
sending Skip while the probe is pending, and runtime setup reuses the
parsed orca.yaml when merging effective hooks.

* fix(workspaces): keep a failed folder setup probe unknown and retry it on create

* fix(workspaces): share folder setup capability answers with the composer

* fix(workspaces): re-ask the host for folder setup support on every create

* fix(settings): read orca.yaml for folder projects and use workspace copy in hook rows

QA found Settings reporting no orca.yaml for folder projects while the
composer ran that script, and the inner hook rows still said worktree.

* fix(folder-workspaces): open one Setup tab for an instant folder create

* fix(workspaces): run setup when a created workspace has only editor tabs

* perf(folder-workspaces): per-workspace runner dir, bounded setup answers, one archive config read

* fix(folder-workspaces): retain setup capability for active consumers

* fix(folder-workspaces): match hook search to workspace descriptions

* fix(i18n): drop obsolete hook search descriptions

* test(folder-workspaces): follow relocated setup trust modules
2026-11-10 10:12:12 -08:00

145 lines
7.7 KiB
TypeScript

import { WORKSPACE_ATTACHMENT_RUNTIME_CAPABILITIES } from './workspace-attachments/workspace-attachment-capabilities'
import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from './sidebar-inputs-switch'
export const WORKTREE_RENAME_ERROR_DISMISS_RUNTIME_CAPABILITY =
'worktree.rename-error-dismiss.v1' as const
export const WORKTREE_OPEN_EXISTING_BRANCH_RUNTIME_CAPABILITY =
'worktree.open-existing-branch.v1' as const
// Older hosts advertised V2 while still accepting the V1 wire value.
export const WORKTREE_OPEN_EXISTING_BRANCH_V2_RUNTIME_CAPABILITY =
'worktree.open-existing-branch.v2' as const
export const WORKTREE_OPEN_EXISTING_BRANCH_V2_REQUEST_RUNTIME_CAPABILITY =
'worktree.open-existing-branch.v2-request.v1' as const
export const PROJECT_HOST_SETUP_RUNTIME_CAPABILITY = 'project-host-setup.v1' as const
// Why: older hosts strip `hostId` from setup update/delete and act on the first same-id twin.
export const PROJECT_HOST_SETUP_HOST_TARGET_RUNTIME_CAPABILITY =
'project-host-setup.host-target.v1' as const
export const WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY = 'workspace-run-context.v1' as const
export const WORKTREE_PROVISIONED_ROOT_RUNTIME_CAPABILITY = 'worktree.provisioned-root.v1' as const
export const WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY =
'worktree.linked-work-item-context.v1' as const
export const WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY =
'worktree.github-pr-suppression.v1' as const
// Legacy clients cannot safely derive a branch name from a qualified ref selector.
export const REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY =
'repo.search-qualified-refs.v1' as const
export const FOLDER_WORKSPACE_PATH_STATUS_RUNTIME_CAPABILITY =
'folder-workspace.path-status.v1' as const
// Hosts whose folderWorkspace.update/delete honor `requireUniqueId`; older hosts edit the first twin and delete every row with the id.
export const FOLDER_WORKSPACE_UNIQUE_ID_MUTATION_RUNTIME_CAPABILITY =
'folder-workspace.unique-id-mutation.v1' as const
// Replay ambiguous creates only with advertised idempotency support; older hosts strip clientMutationId. status.worktreeCreateIdempotency supplies optional retention policy.
export const WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY =
'worktree.create-idempotency.v1' as const
export const WORKTREE_REMOVE_BRANCH_POLICY_RUNTIME_CAPABILITY =
'worktree.remove-branch-policy.v1' as const
// A failed running archive hook blocks deletion. This does not prove a hook was found: unreadable SSH orca.yaml is indistinguishable from absence (#20196).
// Advertise the gate so callers need not risk their checkout by probing a failure (#19334).
export const WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY =
'worktree.archive-failure-blocking.v1' as const
export const WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY =
'worktree.visibility-defaults.v1' as const
export const WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY =
'worktree.visibility-source-defaults.v1' as const
// Capable clients wait for deletion and display removing rows. Legacy clients receive acceptance early, so hosts hide those rows to avoid showing them as normal workspaces.
export const WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY =
'worktree.background-removal.v1' as const
// Hosts that record another owner's verified automation run on the workspaces it creates there.
export const WORKTREE_CLIENT_AUTOMATION_PROVENANCE_RUNTIME_CAPABILITY =
'worktree.create.client-automation-provenance.v1' as const
export const WORKTREE_CREATE_AGENT_OPTIONS_RUNTIME_CAPABILITY =
'worktree.create-agent-options.v1' as const
// Hosts that save a parent from another Orca store; older hosts strip `crossStoreParent` and write no link.
export const WORKTREE_CROSS_STORE_LINEAGE_RUNTIME_CAPABILITY =
'worktree.lineage.crossMachine' as const
export function supportsCrossStoreLineage(capabilities: readonly string[] | undefined): boolean {
return capabilities?.includes(WORKTREE_CROSS_STORE_LINEAGE_RUNTIME_CAPABILITY) === true
}
/** Refuses before any request, so an older host never applies the rest of the change without its parent. */
export function assertCrossStoreLineageSupported(
capabilities: readonly string[] | undefined,
hostLabel: string
): void {
if (!supportsCrossStoreLineage(capabilities)) {
throw new Error(
`Update Orca on ${hostLabel} to nest workspaces under a parent on another machine. Nothing was changed.`
)
}
}
// Hosts that accept a folder workspace as `worktree.set`'s parent; older hosts refuse it as not found.
export const WORKTREE_SET_FOLDER_PARENT_RUNTIME_CAPABILITY =
'worktree.set.folder-parent.v1' as const
export function supportsFolderParent(capabilities: readonly string[] | undefined): boolean {
return capabilities?.includes(WORKTREE_SET_FOLDER_PARENT_RUNTIME_CAPABILITY) === true
}
/** Refuses before any request, so an older host's "not found" never reads as a mistyped folder. */
export function assertFolderParentSetSupported(
capabilities: readonly string[] | undefined,
hostLabel: string
): void {
if (!supportsFolderParent(capabilities)) {
throw new Error(
`Update Orca on ${hostLabel} to attach a worktree to a folder workspace. Nothing was changed.`
)
}
}
// Hosts whose selector RPCs accept `matchFolderWorkspaces`, letting path: and name: also match folder workspaces.
export const WORKSPACE_SELECTOR_FOLDER_MATCH_RUNTIME_CAPABILITY =
'workspace-selector.folder-match.v1' as const
// Hosts that run an existing worktree's setup once the user approves that worktree's content.
export const WORKTREE_RUN_APPROVED_SETUP_RUNTIME_CAPABILITY =
'worktree.run-approved-setup.v1' as const
// Hosts that run a folder project's setup and archive scripts when its workspaces are created and removed.
export const WORKTREE_FOLDER_SETUP_RUNTIME_CAPABILITY = 'worktree.folder-setup.v1' as const
// Hosts that honor branchIntent 'create-exact': the unsuffixed new branch, or a coded refusal.
export const WORKTREE_CREATE_EXACT_NAME_RUNTIME_CAPABILITY =
'worktree.create.exact-name.v1' as const
// Hosts that publish worktreeRowsChanged (only while the sidebar feed is off), so a phone's workspace list polls worktree.ps only as a backstop.
export const WORKTREE_ROWS_CHANGED_EVENT_RUNTIME_CAPABILITY =
'worktree.rows-changed-event.v1' as const
export const WORKTREE_RUNTIME_CAPABILITIES = [
WORKTREE_CREATE_AGENT_OPTIONS_RUNTIME_CAPABILITY,
WORKTREE_RENAME_ERROR_DISMISS_RUNTIME_CAPABILITY,
WORKTREE_OPEN_EXISTING_BRANCH_RUNTIME_CAPABILITY,
WORKTREE_OPEN_EXISTING_BRANCH_V2_RUNTIME_CAPABILITY,
WORKTREE_OPEN_EXISTING_BRANCH_V2_REQUEST_RUNTIME_CAPABILITY,
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
PROJECT_HOST_SETUP_HOST_TARGET_RUNTIME_CAPABILITY,
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY,
WORKTREE_PROVISIONED_ROOT_RUNTIME_CAPABILITY,
WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY,
...WORKSPACE_ATTACHMENT_RUNTIME_CAPABILITIES,
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY,
FOLDER_WORKSPACE_PATH_STATUS_RUNTIME_CAPABILITY,
FOLDER_WORKSPACE_UNIQUE_ID_MUTATION_RUNTIME_CAPABILITY,
WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY,
WORKTREE_REMOVE_BRANCH_POLICY_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_CLIENT_AUTOMATION_PROVENANCE_RUNTIME_CAPABILITY,
WORKTREE_CROSS_STORE_LINEAGE_RUNTIME_CAPABILITY,
WORKTREE_SET_FOLDER_PARENT_RUNTIME_CAPABILITY,
WORKSPACE_SELECTOR_FOLDER_MATCH_RUNTIME_CAPABILITY,
WORKTREE_RUN_APPROVED_SETUP_RUNTIME_CAPABILITY,
WORKTREE_FOLDER_SETUP_RUNTIME_CAPABILITY,
WORKTREE_CREATE_EXACT_NAME_RUNTIME_CAPABILITY,
...(ORCA_SIDEBAR_INPUTS_IS_MAIN ? [] : [WORKTREE_ROWS_CHANGED_EVENT_RUNTIME_CAPABILITY])
] as const