mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
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:
@@ -1,3 +1,4 @@
|
||||
setupAgentStartupPolicy: wait-for-setup
|
||||
scripts:
|
||||
setup: |
|
||||
node config/scripts/run-internal-dev-setup.mjs
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user