Preserve automation CLI run context

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 21:41:01 -07:00
co-authored by Orca
parent 0fcd896dd1
commit c622db5f23
4 changed files with 115 additions and 11 deletions
@@ -243,3 +243,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Guarded remaining GitLab path-only fallbacks by making `workItemByPath` use the same repoId/source-context selector as the rest of GitLab IPC, passing repoId from URL picker and right-sidebar GitLab review actions, and verifying source-host routing with GitLab IPC regressions plus focused oxlint and full `pnpm run typecheck`.
- [x] 2026-06-12: Guarded GitHub label and assignable-user metadata lookups with the same repo/source-context selector as task mutations, so drawer edit metadata cannot bypass source-host validation; verified with GitHub IPC regressions (17 tests), focused oxlint, and full `pnpm run typecheck`.
- [x] 2026-06-12: Continued the Host copy audit through onboarding, remote folder-add, attachment import, Settings SSH copy, and remote file browser catalog text; extracted `RepoStepNestedImportPanel` to keep the touched onboarding component under max-lines, and verified with stale-copy scans, focused onboarding/repos tests, focused oxlint, full `pnpm run typecheck`, and `git diff --check`.
- [x] 2026-06-12: Preserved explicit automation run-host context in CLI project selectors: `orca automations create/edit --project/--host` and `--project-host-setup` now send `WorkspaceRunContext` alongside the legacy repo selector, so scheduled runs retain logical project, host, setup, repo, and path identity. Verified with focused CLI regressions (108 tests), targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
+39 -10
View File
@@ -7,7 +7,11 @@ import type {
AutomationSchedulePreset,
AutomationUpdateInput
} from '../../shared/automations-types'
import type { TuiAgent } from '../../shared/types'
import {
buildWorkspaceRunContext,
type WorkspaceRunContext
} from '../../shared/task-source-context'
import type { ProjectHostSetup, TuiAgent } from '../../shared/types'
import {
DEFAULT_AUTOMATION_PRECHECK_TIMEOUT_SECONDS,
MAX_AUTOMATION_PRECHECK_TIMEOUT_SECONDS
@@ -33,7 +37,7 @@ import { getOptionalWorktreeSelector, resolveCurrentWorktreeSelector } from '../
import {
assertWorkspaceTargetFlagsCompatible,
hasWorkspaceProjectTarget,
resolveProjectCreateRepoSelector
resolveProjectCreateTarget
} from '../worktree-project-target'
type AutomationCreateParams = Omit<AutomationCreateInput, 'projectId' | 'timezone'> & {
@@ -298,7 +302,7 @@ async function resolveDefaultTarget(
flags: Map<string, string | boolean>,
cwd: string,
client: Parameters<CommandHandler>[0]['client']
): Promise<{ repo?: string; workspace?: string }> {
): Promise<{ repo?: string; workspace?: string; runContext?: WorkspaceRunContext }> {
assertWorkspaceTargetFlagsCompatible(flags)
const repo = getOptionalStringFlag(flags, 'repo')
if (repo && getOptionalStringFlag(flags, 'workspace')) {
@@ -310,9 +314,12 @@ async function resolveDefaultTarget(
'Use either --workspace or project target flags, not both.'
)
}
const projectRepo = await resolveProjectCreateRepoSelector(flags, client)
if (projectRepo) {
return { repo: projectRepo }
const projectTarget = await resolveProjectCreateTarget(flags, client)
if (projectTarget) {
return {
repo: projectTarget.repoSelector,
runContext: buildAutomationRunContextFromSetup(projectTarget.setup)
}
}
const workspace = await getOptionalWorktreeSelector(flags, 'workspace', cwd, client)
if (repo || workspace) {
@@ -332,7 +339,7 @@ async function getExplicitTarget(
flags: Map<string, string | boolean>,
cwd: string,
client: Parameters<CommandHandler>[0]['client']
): Promise<{ repo?: string; workspace?: string }> {
): Promise<{ repo?: string; workspace?: string; runContext?: WorkspaceRunContext }> {
assertWorkspaceTargetFlagsCompatible(flags)
const repo = getOptionalStringFlag(flags, 'repo')
if (repo && getOptionalStringFlag(flags, 'workspace')) {
@@ -344,14 +351,34 @@ async function getExplicitTarget(
'Use either --workspace or project target flags, not both.'
)
}
const projectRepo = await resolveProjectCreateRepoSelector(flags, client)
if (projectRepo) {
return { repo: projectRepo }
const projectTarget = await resolveProjectCreateTarget(flags, client)
if (projectTarget) {
return {
repo: projectTarget.repoSelector,
runContext: buildAutomationRunContextFromSetup(projectTarget.setup)
}
}
const workspace = await getOptionalWorktreeSelector(flags, 'workspace', cwd, client)
return { repo, workspace }
}
function buildAutomationRunContextFromSetup(setup: ProjectHostSetup): WorkspaceRunContext {
const runContext = buildWorkspaceRunContext({
projectId: setup.projectId,
hostId: setup.hostId,
projectHostSetupId: setup.id,
repoId: setup.repoId,
path: setup.path
})
if (!runContext) {
throw new RuntimeClientError(
'invalid_argument',
`Project host setup is missing automation run context fields: ${setup.id}`
)
}
return runContext
}
export const AUTOMATION_HANDLERS: Record<string, CommandHandler> = {
'automations list': async ({ client, json }) => {
const result = await client.call<{ automations: Automation[] }>('automation.list')
@@ -376,6 +403,7 @@ export const AUTOMATION_HANDLERS: Record<string, CommandHandler> = {
prompt: getRequiredStringFlag(flags, 'prompt'),
precheck: getPrecheckFlag(flags),
agentId: getProviderFlag(flags),
...(target.runContext ? { runContext: target.runContext } : {}),
repo: target.repo,
workspace: target.workspace,
workspaceMode,
@@ -398,6 +426,7 @@ export const AUTOMATION_HANDLERS: Record<string, CommandHandler> = {
prompt: getOptionalStringFlag(flags, 'prompt'),
precheck: getPrecheckFlag(flags),
agentId: getOptionalProviderFlag(flags),
...(target.runContext ? { runContext: target.runContext } : {}),
repo: target.repo,
workspace: target.workspace,
workspaceMode: getWorkspaceModeFlag(flags),
+59
View File
@@ -2710,12 +2710,71 @@ describe('orca cli worktree awareness', () => {
'automation.create',
expect.objectContaining({
repo: 'id:repo-gpu',
runContext: {
kind: 'workspace-run',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
projectHostSetupId: 'setup-gpu',
repoId: 'repo-gpu',
path: '/srv/orca'
},
workspace: undefined,
workspaceMode: 'new_per_run'
})
)
})
it('resolves project-host-setup flags for automation edit with explicit run context', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-gpu',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
}),
okFixture('req_edit', {
automation: { id: 'auto-1', name: 'GPU review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['automations', 'edit', 'auto-1', '--project-host-setup', 'setup-gpu', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'projectHostSetup.list')
expect(callMock).toHaveBeenNthCalledWith(
2,
'automation.update',
expect.objectContaining({
id: 'auto-1',
updates: expect.objectContaining({
repo: 'id:repo-gpu',
runContext: {
kind: 'workspace-run',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
projectHostSetupId: 'setup-gpu',
repoId: 'repo-gpu',
path: '/srv/orca'
}
})
})
)
})
it('rejects invalid automation --day values before calling the runtime', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+16 -1
View File
@@ -2,6 +2,11 @@ import type { ProjectHostSetup } from '../shared/types'
import type { RuntimeClient } from './runtime-client'
import { RuntimeClientError } from './runtime-client'
export type ProjectCreateTarget = {
repoSelector: string
setup: ProjectHostSetup
}
function getPresentStringFlag(
flags: Map<string, string | boolean>,
name: string
@@ -40,6 +45,13 @@ export async function resolveProjectCreateRepoSelector(
flags: Map<string, string | boolean>,
client: RuntimeClient
): Promise<string | undefined> {
return (await resolveProjectCreateTarget(flags, client))?.repoSelector
}
export async function resolveProjectCreateTarget(
flags: Map<string, string | boolean>,
client: RuntimeClient
): Promise<ProjectCreateTarget | undefined> {
const projectHostSetupId = getPresentStringFlag(flags, 'project-host-setup')
const projectId = getPresentStringFlag(flags, 'project')
const hostId = getPresentStringFlag(flags, 'host')
@@ -66,5 +78,8 @@ export async function resolveProjectCreateRepoSelector(
: `Project is not set up on the selected host: ${projectId}${hostId ? ` on ${hostId}` : ''}`
)
}
return `id:${setup.repoId}`
return {
repoSelector: `id:${setup.repoId}`,
setup
}
}