Fix setup-provisioned skills missing at agent startup (#17124)

* fix(setup): let repos gate agent startup

* test(setup): update runner call expectations
This commit is contained in:
Neil
2026-08-29 01:53:31 -07:00
committed by GitHub
parent 92ab618a11
commit 8d3e32a2ff
16 changed files with 97 additions and 21 deletions
+1
View File
@@ -1,3 +1,4 @@
setupAgentStartupPolicy: wait-for-setup
scripts:
setup: |
node config/scripts/run-internal-dev-setup.mjs
+13
View File
@@ -22,6 +22,19 @@ describe('parseOrcaYaml', () => {
})
})
it('parses a project requirement to finish setup before agent startup', () => {
const yaml = [
'setupAgentStartupPolicy: wait-for-setup',
'scripts:',
' setup: node install-project-skills.mjs'
].join('\n')
expect(parseOrcaYaml(yaml)).toEqual({
scripts: { setup: 'node install-project-skills.mjs' },
setupAgentStartupPolicy: 'wait-for-setup'
})
})
it('parses YAML with archive script only', () => {
const yaml = `scripts:\n archive: |\n echo "archiving"\n`
const result = parseOrcaYaml(yaml)
+21 -1
View File
@@ -302,7 +302,7 @@ describe('createSetupRunnerScript', () => {
}
})
it('omits waitForAgentStartup unless the repo explicitly waits for setup', async () => {
it('waits when either local or project policy requires completed setup', async () => {
gitExecFileSyncMock.mockReset()
gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n')
const { createSetupRunnerScript } = await import('./worktree-runner-script')
@@ -318,6 +318,26 @@ describe('createSetupRunnerScript', () => {
createSetupRunnerScript(makeRepo('wait-for-setup'), '/test/worktree', 'echo setup')
.waitForAgentStartup
).toBe(true)
expect(
createSetupRunnerScript(
makeRepo('start-immediately'),
'/test/worktree',
'echo setup',
undefined,
undefined,
'wait-for-setup'
).waitForAgentStartup
).toBe(true)
expect(
createSetupRunnerScript(
makeRepo('wait-for-setup'),
'/test/worktree',
'echo setup',
undefined,
undefined,
'start-immediately'
).waitForAgentStartup
).toBe(true)
})
it('marks setup-runner terminals for the always-on credential guard', async () => {
+1
View File
@@ -55,6 +55,7 @@ export function hasHooksFile(repoPath: string): boolean {
// Why: detect unrecognised keys so the UI can suggest an update instead of showing a "could not be parsed" error.
const RECOGNIZED_ORCA_YAML_KEYS = new Set([
'scripts',
'setupAgentStartupPolicy',
'issueCommand',
'defaultTabs',
'environmentRecipes',
+11 -5
View File
@@ -8,6 +8,7 @@ import { randomUUID } from 'node:crypto'
import type { Store } from '../persistence'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { Repo } from '../../shared/repo-types'
import type { SetupAgentStartupPolicy } from '../../shared/orca-yaml-hook-types'
import type {
LocalBaseRefRefreshResult,
LocalBaseRefUpdateSuggestion
@@ -1259,7 +1260,8 @@ async function createRemoteSetupRunnerScript(
worktreePath: string,
script: string,
gitProvider: SshGitProvider,
fsProvider: IFilesystemProvider
fsProvider: IFilesystemProvider,
projectStartupPolicy?: SetupAgentStartupPolicy
): Promise<CreateWorktreeResult['setup']> {
const useWindowsFormat = isWindowsAbsolutePathLike(worktreePath)
// Why: SSH terminals choose their shell on the remote host; local Windows
@@ -1281,7 +1283,10 @@ async function createRemoteSetupRunnerScript(
return {
runnerScriptPath,
envVars: getSetupRunnerEnvVars(repo, worktreePath),
...(shouldWaitForSetupBeforeAgentStartup(repo.hookSettings?.setupAgentStartupPolicy)
...(shouldWaitForSetupBeforeAgentStartup(
repo.hookSettings?.setupAgentStartupPolicy,
projectStartupPolicy
)
? { waitForAgentStartup: true }
: {})
}
@@ -2030,7 +2035,8 @@ export async function createRemoteWorktree(
created.path,
setupScript,
provider,
fsProvider
fsProvider,
yamlHooks?.setupAgentStartupPolicy
)
} catch (error) {
console.error(`[hooks] Failed to prepare setup runner for ${created.path}:`, error)
@@ -2721,13 +2727,13 @@ export async function createLocalWorktree(
try {
// Why: main only writes the runner script and must not execute setup itself, or we reintroduce the old hidden background-hook behavior.
// Why: worktree already exists, so a runner-gen failure degrades to "created without setup launch" rather than failing creation.
// Why: both trailing args are optional — the shell is undefined off Windows.
setup = createSetupRunnerScript(
repo,
worktreePath,
setupScript,
localWorktreeGitOptionArgs[0],
resolveSetupRunnerShell(settings)
resolveSetupRunnerShell(settings),
createdYamlHooks?.setupAgentStartupPolicy
)
} catch (error) {
console.error(`[hooks] Failed to prepare setup runner for ${worktreePath}:`, error)
@@ -514,10 +514,14 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: false
}
])
loadHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
loadHooksMock.mockReturnValue({
scripts: { setup: 'pnpm install' },
setupAgentStartupPolicy: 'wait-for-setup'
})
getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
shouldRunSetupForCreateMock.mockReturnValue(true)
expect(createSetupRunnerScriptMock).not.toHaveBeenCalled()
const result = (await handlers['worktrees:create'](null, {
repoId: 'repo-1',
@@ -538,6 +542,14 @@ describe('registerWorktreeHandlers', () => {
startupTerminal?: { spawned: boolean; surface?: string }
timing?: { phases: { phase: string }[] }
}
expect(createSetupRunnerScriptMock).toHaveBeenCalledWith(
expect.objectContaining({ id: 'repo-1' }),
'/workspace/improve-dashboard',
'pnpm install',
undefined,
undefined,
'wait-for-setup'
)
expect(runtimeStub.createTerminal).toHaveBeenNthCalledWith(
1,
@@ -118,6 +118,7 @@ describe('registerWorktreeHandlers', () => {
'/workspace/improve-dashboard',
'pnpm worktree:setup',
undefined,
undefined,
undefined
)
expect(result).toMatchObject({
@@ -169,6 +170,7 @@ describe('registerWorktreeHandlers', () => {
'/workspace/improve-dashboard',
'pnpm worktree:setup # worktree',
undefined,
undefined,
undefined
)
expect(result).toEqual(
@@ -137,7 +137,7 @@ describe('registerWorktreeHandlers', () => {
}
const fsProvider = {
readFile: vi.fn().mockResolvedValue({
content: 'scripts:\n setup: pnpm install\n',
content: 'setupAgentStartupPolicy: wait-for-setup\nscripts:\n setup: pnpm install\n',
isBinary: false
}),
createDir: vi.fn().mockResolvedValue(undefined),
@@ -153,7 +153,10 @@ describe('registerWorktreeHandlers', () => {
getSshFilesystemProviderMock.mockReturnValue(fsProvider)
getActiveMultiplexerMock.mockReturnValue(mux)
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
parseOrcaYamlMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
parseOrcaYamlMock.mockReturnValue({
scripts: { setup: 'pnpm install' },
setupAgentStartupPolicy: 'wait-for-setup'
})
getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
shouldRunSetupForCreateMock.mockReturnValue(true)
@@ -184,7 +187,8 @@ describe('registerWorktreeHandlers', () => {
envVars: expect.objectContaining({
ORCA_ROOT_PATH: '/remote/repo',
ORCA_WORKTREE_PATH: '/remote/repo-improve-dashboard'
})
}),
waitForAgentStartup: true
}
})
)
+2 -1
View File
@@ -467,7 +467,8 @@ describe('registerWorktreeHandlers Windows path handling', () => {
'C:\\workspaces\\improve-dashboard',
'pnpm install',
undefined,
setupShell
setupShell,
undefined
)
expect(result).toMatchObject({
setup: {
@@ -369,6 +369,7 @@ describe('registerWorktreeHandlers', () => {
'/workspace/improve-dashboard',
'pnpm worktree:setup',
{ wslDistro: 'Ubuntu' },
undefined,
undefined
)
expect(addWorktreeMock).toHaveBeenCalledWith(
+6 -2
View File
@@ -45042,6 +45042,7 @@ describe('OrcaRuntimeService', () => {
'/tmp/workspaces/runtime-hook-test',
'pnpm worktree:setup',
undefined,
undefined,
undefined
)
expect(runHook).not.toHaveBeenCalled()
@@ -45197,7 +45198,8 @@ describe('OrcaRuntimeService', () => {
'C:\\workspaces\\runtime-hook-activate',
'pnpm worktree:setup',
undefined,
{ family: 'posix' }
{ family: 'posix' },
undefined
)
expect(result.setup).toMatchObject({
runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh',
@@ -45277,6 +45279,7 @@ describe('OrcaRuntimeService', () => {
'/tmp/workspaces/runtime-hook-skip',
'pnpm worktree:setup',
undefined,
undefined,
undefined
)
expect(runHook).not.toHaveBeenCalled()
@@ -45480,7 +45483,8 @@ describe('OrcaRuntimeService', () => {
'C:\\workspaces\\runtime-hook-windowless',
'pnpm worktree:setup',
undefined,
{ family: 'posix' }
{ family: 'posix' },
undefined
)
expect(runHook).not.toHaveBeenCalled()
expect(result.setupReceipt).toMatchObject({ state: 'running' })
+2 -2
View File
@@ -27367,13 +27367,13 @@ export class OrcaRuntimeService {
// a renderer window, so the startup shell can wait on setup completion
// and windowless creates resolve the same Windows setup shell.
const runtimeTarget = this.getLocalGitExecutionOptionArgs(repo)[0]
// Why: both trailing args are optional — the shell is undefined off Windows.
setup = createSetupRunnerScript(
repo,
worktreePath,
hooks.scripts.setup,
runtimeTarget,
resolveSetupRunnerShell(settings)
resolveSetupRunnerShell(settings),
yamlHooks?.setupAgentStartupPolicy
)
} catch (error) {
// Why: the git worktree is already real at this point. If runner
+5 -2
View File
@@ -13,6 +13,7 @@ import { buildPosixRunnerScript, buildWindowsRunnerScript } from './setup-runner
import type { HookRuntimeTarget } from './hook-runtime-target'
import type { Repo } from '../shared/repo-types'
import type { WorktreeSetupLaunch } from '../shared/worktree/launch-types'
import type { SetupAgentStartupPolicy } from '../shared/orca-yaml-hook-types'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { SetupRunnerShell } from '../shared/setup-runner-command'
@@ -30,7 +31,8 @@ export function createSetupRunnerScript(
worktreePath: string,
script: string,
projectRuntime?: ProjectExecutionRuntimeResolution | HookRuntimeTarget,
setupShell?: SetupRunnerShell
setupShell?: SetupRunnerShell,
projectStartupPolicy?: SetupAgentStartupPolicy
): WorktreeSetupLaunch {
return createWorktreeRunnerScript({
repo,
@@ -39,7 +41,8 @@ export function createSetupRunnerScript(
runnerBaseName: 'setup-runner',
runtimeTarget: getHookRuntimeTarget(projectRuntime),
waitForAgentStartup: shouldWaitForSetupBeforeAgentStartup(
repo.hookSettings?.setupAgentStartupPolicy
repo.hookSettings?.setupAgentStartupPolicy,
projectStartupPolicy
),
setupShell
})
+1
View File
@@ -8,6 +8,7 @@ export type OrcaHooks = {
setup?: string // Runs after worktree is created
archive?: string // Runs before worktree is archived
}
setupAgentStartupPolicy?: SetupAgentStartupPolicy
issueCommand?: string // Shared default command for linked GitHub issues
defaultTabs?: OrcaDefaultTabTemplate[] // Terminal tabs to create once for a new worktree
environmentRecipes?: OrcaVmRecipe[] // Project-scoped per-workspace environment recipes
+7
View File
@@ -225,6 +225,11 @@ export function parseOrcaYaml(content: string): OrcaHooks | null {
const scriptsRecord = asRecord(record.scripts)
const setup = scriptsRecord ? asTrimmedString(scriptsRecord.setup) : undefined
const archive = scriptsRecord ? asTrimmedString(scriptsRecord.archive) : undefined
const setupAgentStartupPolicy =
record.setupAgentStartupPolicy === 'start-immediately' ||
record.setupAgentStartupPolicy === 'wait-for-setup'
? record.setupAgentStartupPolicy
: undefined
const issueCommand = asTrimmedString(record.issueCommand)
const defaultTabs = normalizeDefaultTabs(record.defaultTabs)
const environmentRecipeParse = normalizeVmRecipes(record.environmentRecipes)
@@ -239,6 +244,7 @@ export function parseOrcaYaml(content: string): OrcaHooks | null {
!setup &&
!archive &&
!issueCommand &&
!setupAgentStartupPolicy &&
defaultTabs.length === 0 &&
environmentRecipes.length === 0 &&
environmentRecipeDiagnostics.length === 0 &&
@@ -252,6 +258,7 @@ export function parseOrcaYaml(content: string): OrcaHooks | null {
...(setup ? { setup } : {}),
...(archive ? { archive } : {})
},
...(setupAgentStartupPolicy ? { setupAgentStartupPolicy } : {}),
...(issueCommand ? { issueCommand } : {}),
...(defaultTabs.length > 0 ? { defaultTabs } : {}),
...(environmentRecipes.length > 0 ? { environmentRecipes } : {}),
+4 -4
View File
@@ -1,11 +1,11 @@
import type { SetupAgentStartupPolicy } from './orca-yaml-hook-types'
// Why: existing repos should keep launching setup and agents side by side unless
// the user explicitly opts into waiting for setup completion.
// Why: existing repos keep launching setup and agents side by side unless the user or
// committed project config requires setup to finish first.
export const DEFAULT_SETUP_AGENT_STARTUP_POLICY: SetupAgentStartupPolicy = 'start-immediately'
export function shouldWaitForSetupBeforeAgentStartup(
policy: SetupAgentStartupPolicy | undefined
...policies: (SetupAgentStartupPolicy | undefined)[]
): boolean {
return policy === 'wait-for-setup'
return policies.includes('wait-for-setup')
}