From c622db5f23f76793fd9fd3c8d88bef3877ad90e8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 12 Jun 2026 21:41:01 -0700 Subject: [PATCH] Preserve automation CLI run context Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + src/cli/handlers/automations.ts | 49 +++++++++++---- src/cli/index.test.ts | 59 +++++++++++++++++++ src/cli/worktree-project-target.ts | 17 +++++- 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index aec0e9f337c..9981123bfad 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -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`. diff --git a/src/cli/handlers/automations.ts b/src/cli/handlers/automations.ts index 077ecc1eb1c..16f22583386 100644 --- a/src/cli/handlers/automations.ts +++ b/src/cli/handlers/automations.ts @@ -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 & { @@ -298,7 +302,7 @@ async function resolveDefaultTarget( flags: Map, cwd: string, client: Parameters[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, cwd: string, client: Parameters[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 = { 'automations list': async ({ client, json }) => { const result = await client.call<{ automations: Automation[] }>('automation.list') @@ -376,6 +403,7 @@ export const AUTOMATION_HANDLERS: Record = { 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 = { prompt: getOptionalStringFlag(flags, 'prompt'), precheck: getPrecheckFlag(flags), agentId: getOptionalProviderFlag(flags), + ...(target.runContext ? { runContext: target.runContext } : {}), repo: target.repo, workspace: target.workspace, workspaceMode: getWorkspaceModeFlag(flags), diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 2325180b61d..c974b9c6e41 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -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(() => {}) diff --git a/src/cli/worktree-project-target.ts b/src/cli/worktree-project-target.ts index 243dc0276c7..6626270655a 100644 --- a/src/cli/worktree-project-target.ts +++ b/src/cli/worktree-project-target.ts @@ -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, name: string @@ -40,6 +45,13 @@ export async function resolveProjectCreateRepoSelector( flags: Map, client: RuntimeClient ): Promise { + return (await resolveProjectCreateTarget(flags, client))?.repoSelector +} + +export async function resolveProjectCreateTarget( + flags: Map, + client: RuntimeClient +): Promise { 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 + } }