diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 1fbd304426c..02703dc1adc 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -295,5 +295,6 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-13: Closed another request/cache ownership slice by routing right-sidebar search, create-review field generation, browser tab default profile selection, Smart Workspace GitHub/GitLab/Linear lookups, GitHub Project browse cache, Project slug comments, and standalone GitHub issue comments through explicit worktree/repo/source ownership instead of ambient focused runtime state. Verified with focused ownership/browser/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-13: Routed Quick Open file listing through the selected worktree runtime owner instead of ambient focused settings, matching File Explorer and sidebar search ownership. Verified with focused Quick Open and file-owner boundary tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-13: Routed typed GitHub issue/PR lookup entry points in Cmd-J, Smart Workspace cross-repo accept, and Composer submit fallback through source-aware local/SSH/runtime lookup helpers instead of direct preload calls. Verified with focused composer/palette/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. +- [x] 2026-06-13: Centralized Automation run identity so renderer dispatch, Automations UI, main-process run-target resolution, persistence backfill, and CLI formatting resolve `runRepoId` through explicit `runContext` with a named `legacyRepoId` fallback instead of each caller reading the persisted compatibility field directly. The checklist row remains partial because `Automation.projectId` still exists for storage compatibility. Verified with shared identity, automation run-context, automation service, and CLI format tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [~] 2026-06-13: Remaining source-error gap: unavailable hosts, version-skewed runtimes, and GitHub/GitLab missing auth/tooling are surfaced for desktop-owned and runtime-owned source hosts, but unsupported-provider still has no production emitter per source host. - [~] 2026-06-13: Remaining Automation gap: run-host/source-host availability and missing project-host setup are handled for manual/internal/external actions, but `Automation.projectId` remains the legacy repo-id compatibility field. diff --git a/src/cli/workspace-format.ts b/src/cli/workspace-format.ts index 95ad2cf4607..7be9ba302f0 100644 --- a/src/cli/workspace-format.ts +++ b/src/cli/workspace-format.ts @@ -1,4 +1,5 @@ import type { Automation, AutomationRun } from '../shared/automations-types' +import { getAutomationLegacyRepoId } from '../shared/automation-run-identity' import { formatAutomationPrecheckTimeout } from '../shared/automation-precheck' import { formatAutomationSchedule } from '../shared/automation-schedules' import type { PublicKnownRuntimeEnvironment } from '../shared/runtime-environments' @@ -186,9 +187,9 @@ export function formatAutomationShow(result: { automation: Automation }): string `projectHostSetupId: ${runContext.projectHostSetupId}`, `runRepoId: ${runContext.repoId}`, `runPath: ${runContext.path}`, - `legacyRepoId: ${automation.projectId}` + `legacyRepoId: ${getAutomationLegacyRepoId(automation)}` ] - : [`legacyRepoId: ${automation.projectId}`] + : [`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`] return [ `id: ${automation.id}`, `name: ${automation.name}`, diff --git a/src/main/automations/run-target-resolution.ts b/src/main/automations/run-target-resolution.ts index 06b303da57f..a72feba0931 100644 --- a/src/main/automations/run-target-resolution.ts +++ b/src/main/automations/run-target-resolution.ts @@ -1,5 +1,6 @@ import type { Store } from '../persistence' import type { Automation } from '../../shared/automations-types' +import { getAutomationLegacyRepoId } from '../../shared/automation-run-identity' import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host' import type { ProjectHostSetup, Repo } from '../../shared/types' import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' @@ -15,7 +16,7 @@ function getLegacyPrecheckCwd(store: Store, automation: Automation): string | nu : null return parsed?.worktreePath ?? null } - return store.getRepo(automation.projectId)?.path ?? null + return store.getRepo(getAutomationLegacyRepoId(automation))?.path ?? null } export function resolveAutomationRunTarget( @@ -24,7 +25,7 @@ export function resolveAutomationRunTarget( ): AutomationRunTargetResult { const context = automation.runContext ?? null if (!context) { - const repo = store.getRepo(automation.projectId) + const repo = store.getRepo(getAutomationLegacyRepoId(automation)) const cwd = getLegacyPrecheckCwd(store, automation) if (!repo || !cwd) { return { ok: false, error: 'Automation run target is no longer available.' } diff --git a/src/main/persistence.ts b/src/main/persistence.ts index d1f1470d190..550a38891a3 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -32,6 +32,7 @@ import { latestAutomationOccurrenceAtOrBefore, nextAutomationOccurrenceAfter } from '../shared/automation-schedules' +import { getAutomationLegacyRepoId } from '../shared/automation-run-identity' import { normalizeAutomationPrecheck } from '../shared/automation-precheck' import type { PersistedState, @@ -682,7 +683,7 @@ function backfillLegacyAutomationContexts( const contextsByAutomationId = new Map>() const automations = (state.automations ?? []).map((automation) => { const contexts = getAutomationContextsForRepo( - state.repos.find((repo) => repo.id === automation.projectId), + state.repos.find((repo) => repo.id === getAutomationLegacyRepoId(automation)), state.projectHostSetups ?? [] ) const next: Automation = { ...automation } diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index 5adebcaf971..d31ae459fdf 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -53,6 +53,7 @@ import type { AutomationRun, AutomationUpdateInput } from '../../../../shared/automations-types' +import { getAutomationRunRepoId } from '../../../../shared/automation-run-identity' import { parseExecutionHostId } from '../../../../shared/execution-host' import { TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' import type { PreflightStatus } from '../../../../preload/api-types' @@ -142,10 +143,6 @@ function getDefaultWorktree(worktrees: readonly Worktree[]): Worktree | null { return worktrees.find((worktree) => worktree.isMainWorktree) ?? worktrees[0] ?? null } -function getAutomationRunRepoId(automation: Automation): string { - return automation.runContext?.repoId ?? automation.projectId -} - function getRepoBackedAutomationSourceContext( automation: Automation ): RepoBackedAutomationSourceContext | null { diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.ts index 50173412559..2fc2eb3e744 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.ts @@ -11,6 +11,7 @@ import type { AutomationDispatchResult, AutomationPrecheckResult } from '../../../shared/automations-types' +import { getAutomationRunRepoId } from '../../../shared/automation-run-identity' import { didAutomationPrecheckPass, formatAutomationPrecheckFailure @@ -57,7 +58,7 @@ export function useAutomationDispatchEvents(): void { activeTabId: state.activeTabId, activeTabType: state.activeTabType } - const runRepoId = automation.runContext?.repoId ?? automation.projectId + const runRepoId = getAutomationRunRepoId(automation) const repo = state.repos.find((entry) => entry.id === runRepoId) const automationWorktree = automation.workspaceId ? state.allWorktrees().find((entry) => entry.id === automation.workspaceId) diff --git a/src/shared/automation-run-identity.test.ts b/src/shared/automation-run-identity.test.ts new file mode 100644 index 00000000000..1738cba3fce --- /dev/null +++ b/src/shared/automation-run-identity.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import type { Automation } from './automations-types' +import { + getAutomationLegacyRepoId, + getAutomationRunProjectId, + getAutomationRunRepoId +} from './automation-run-identity' + +function automation(overrides: Partial = {}): Automation { + return { + id: 'auto-1', + name: 'Automation', + prompt: 'Run this', + precheck: null, + agentId: 'claude', + runContext: null, + sourceContext: null, + projectId: 'legacy-repo', + executionTargetType: 'local', + executionTargetId: 'local', + schedulerOwner: 'local_host_service', + workspaceMode: 'new_per_run', + workspaceId: null, + baseBranch: null, + reuseSession: false, + timezone: 'UTC', + rrule: 'FREQ=DAILY', + dtstart: 1, + enabled: true, + nextRunAt: 1, + missedRunPolicy: 'run_once_within_grace', + missedRunGraceMinutes: 720, + createdAt: 1, + updatedAt: 1, + ...overrides + } +} + +describe('automation run identity', () => { + it('uses explicit run context identity when present', () => { + const value = automation({ + runContext: { + kind: 'workspace-run', + projectId: 'github:stablyai/orca', + hostId: 'ssh:builder', + projectHostSetupId: 'setup-builder', + repoId: 'remote-repo', + path: '/remote/orca' + } + }) + + expect(getAutomationLegacyRepoId(value)).toBe('legacy-repo') + expect(getAutomationRunRepoId(value)).toBe('remote-repo') + expect(getAutomationRunProjectId(value)).toBe('github:stablyai/orca') + }) + + it('falls back to the legacy repo id for pre-host-context automations', () => { + const value = automation() + + expect(getAutomationLegacyRepoId(value)).toBe('legacy-repo') + expect(getAutomationRunRepoId(value)).toBe('legacy-repo') + expect(getAutomationRunProjectId(value)).toBe('legacy-repo') + }) +}) diff --git a/src/shared/automation-run-identity.ts b/src/shared/automation-run-identity.ts new file mode 100644 index 00000000000..0497f300f89 --- /dev/null +++ b/src/shared/automation-run-identity.ts @@ -0,0 +1,15 @@ +import type { Automation } from './automations-types' + +type AutomationRunIdentityFields = Pick + +export function getAutomationLegacyRepoId(automation: Pick): string { + return automation.projectId +} + +export function getAutomationRunRepoId(automation: AutomationRunIdentityFields): string { + return automation.runContext?.repoId ?? getAutomationLegacyRepoId(automation) +} + +export function getAutomationRunProjectId(automation: AutomationRunIdentityFields): string { + return automation.runContext?.projectId ?? getAutomationLegacyRepoId(automation) +}