diff --git a/server/src/__tests__/execution-workspace-policy.test.ts b/server/src/__tests__/execution-workspace-policy.test.ts index 8d15dd54dc..c4c37e65be 100644 --- a/server/src/__tests__/execution-workspace-policy.test.ts +++ b/server/src/__tests__/execution-workspace-policy.test.ts @@ -274,10 +274,23 @@ describe("execution workspace policy helpers", () => { expect( parseIssueExecutionWorkspaceSettings({ mode: "project_primary", + environmentId: "11111111-1111-4111-8111-111111111111", }), ).toEqual({ mode: "shared_workspace", }); + expect( + parseIssueExecutionWorkspaceSettings( + { + mode: "project_primary", + environmentId: "11111111-1111-4111-8111-111111111111", + }, + { includeEnvironmentId: true }, + ), + ).toEqual({ + mode: "shared_workspace", + environmentId: "11111111-1111-4111-8111-111111111111", + }); }); it("prefers the agent default environment", () => { diff --git a/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts b/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts index 7fcc8f49a3..5ae4be084a 100644 --- a/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts +++ b/server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts @@ -23,6 +23,7 @@ import { issueComments, issueDocuments, issuePlanDecompositions, + issueThreadInteractions, issues, projects, projectWorkspaces, @@ -34,6 +35,7 @@ import { } from "./helpers/embedded-postgres.js"; import { heartbeatService } from "../services/heartbeat.ts"; import { instanceSettingsService } from "../services/instance-settings.ts"; +import { issueService } from "../services/issues.ts"; const execFileAsync = promisify(execFile); @@ -72,17 +74,33 @@ if (!embeddedPostgresSupport.supported) { ); } +async function runGit(cwd: string, args: string[]) { + const result = await execFileAsync("git", args, { cwd }); + return result.stdout.trim(); +} + async function createGitRepo() { const repoRoot = await mkdtemp(path.join(os.tmpdir(), "paperclip-accepted-plan-repo-")); - await execFileAsync("git", ["init"], { cwd: repoRoot }); - await execFileAsync("git", ["config", "user.email", "paperclip-test@example.com"], { cwd: repoRoot }); - await execFileAsync("git", ["config", "user.name", "Paperclip Test"], { cwd: repoRoot }); + await runGit(repoRoot, ["init"]); + await runGit(repoRoot, ["checkout", "-B", "master"]); + await runGit(repoRoot, ["config", "user.email", "paperclip-test@example.com"]); + await runGit(repoRoot, ["config", "user.name", "Paperclip Test"]); await writeFile(path.join(repoRoot, "README.md"), "accepted plan workspace refresh\n"); - await execFileAsync("git", ["add", "README.md"], { cwd: repoRoot }); - await execFileAsync("git", ["commit", "-m", "initial"], { cwd: repoRoot }); + await runGit(repoRoot, ["add", "README.md"]); + await runGit(repoRoot, ["commit", "-m", "initial"]); return repoRoot; } +async function createGitRepoWithOrigin() { + const repoRoot = await createGitRepo(); + const originRoot = await mkdtemp(path.join(os.tmpdir(), "paperclip-accepted-plan-origin-")); + await runGit(originRoot, ["init", "--bare"]); + await runGit(repoRoot, ["remote", "add", "origin", originRoot]); + await runGit(repoRoot, ["push", "-u", "origin", "master"]); + await runGit(repoRoot, ["fetch", "origin", "master"]); + return { repoRoot, originRoot }; +} + describeEmbeddedPostgres("accepted plan workspace refresh", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; @@ -114,6 +132,7 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => { if (root) await rm(root, { recursive: true, force: true }).catch(() => undefined); } await db.delete(issuePlanDecompositions); + await db.delete(issueThreadInteractions); await db.delete(issueDocuments); await db.delete(documentRevisions); await db.delete(documents); @@ -198,6 +217,73 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => { }); } + async function seedAcceptedPlanAcceptance(args: { + companyId: string; + issueId: string; + ownerAgentId: string; + }) { + const documentId = randomUUID(); + const revisionId = randomUUID(); + const interactionId = randomUUID(); + + await db.insert(documents).values({ + id: documentId, + companyId: args.companyId, + title: "Plan", + format: "markdown", + latestBody: "Plan body", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: args.ownerAgentId, + updatedByAgentId: args.ownerAgentId, + }); + await db.insert(documentRevisions).values({ + id: revisionId, + companyId: args.companyId, + documentId, + revisionNumber: 1, + title: "Plan", + format: "markdown", + body: "Plan body", + createdByAgentId: args.ownerAgentId, + }); + await db.insert(issueDocuments).values({ + companyId: args.companyId, + issueId: args.issueId, + documentId, + key: "plan", + }); + await db.insert(issueThreadInteractions).values({ + id: interactionId, + companyId: args.companyId, + issueId: args.issueId, + kind: "request_confirmation", + status: "accepted", + continuationPolicy: "wake_assignee", + payload: { + version: 1, + prompt: "Approve this plan?", + target: { + type: "issue_document", + issueId: args.issueId, + documentId, + key: "plan", + revisionId, + revisionNumber: 1, + }, + }, + result: { + version: 1, + outcome: "accepted", + }, + resolvedAt: new Date(), + createdByUserId: "local-board", + resolvedByUserId: "local-board", + }); + + return revisionId; + } + it("realizes an isolated workspace and drops stale shared task-session params before executing", async () => { const companyId = randomUUID(); const projectId = randomUUID(); @@ -373,6 +459,232 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => { expect(isolatedRows[0]?.cwd).not.toBe(repoRoot); }, 20_000); + it("keeps accepted-plan children strategy-only until first realization after the base ref moves", async () => { + const companyId = randomUUID(); + const projectId = randomUUID(); + const projectWorkspaceId = randomUUID(); + const sourceIssueId = randomUUID(); + const agentId = randomUUID(); + const { repoRoot, originRoot } = await createGitRepoWithOrigin(); + tempRoots.push(repoRoot, originRoot); + + await instanceSettingsService(db).updateExperimental({ + enableIsolatedWorkspaces: true, + }); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + status: "active", + defaultResponsibleUserId: "responsible-user", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Accepted Plan Branch Freshness", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(projectWorkspaces).values({ + id: projectWorkspaceId, + companyId, + projectId, + name: "Primary", + cwd: repoRoot, + isPrimary: true, + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: { + heartbeat: { + wakeOnDemand: true, + maxConcurrentRuns: 1, + }, + }, + permissions: {}, + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(issues).values({ + id: sourceIssueId, + companyId, + projectId, + projectWorkspaceId, + title: "Planning source issue", + status: "in_progress", + workMode: "planning", + priority: "medium", + responsibleUserId: "responsible-user", + assigneeAgentId: agentId, + identifier: "PAP-1584", + executionWorkspaceSettings: { + mode: "isolated_workspace", + workspaceStrategy: { + type: "git_worktree", + baseRef: "origin/master", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const heartbeat = heartbeatService(db); + adapterExecute.mockImplementationOnce(async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + sessionParams: { sessionId: "planning-source-session" }, + sessionDisplayId: "planning-source-session", + summary: "Realized the planning source workspace.", + provider: "test", + model: "test-model", + })); + + const sourceRun = await heartbeat.wakeup(agentId, { + source: "automation", + triggerDetail: "system", + reason: "issue_commented", + contextSnapshot: { + issueId: sourceIssueId, + taskId: sourceIssueId, + wakeReason: "issue_commented", + skipIssueComment: true, + }, + }); + expect(sourceRun).not.toBeNull(); + await vi.waitFor(async () => { + const latest = await heartbeat.getRun(sourceRun!.id); + expect(latest?.status).toBe("succeeded"); + }, { timeout: 10_000 }); + + const sourceWorkspace = await db + .select() + .from(executionWorkspaces) + .where(eq(executionWorkspaces.sourceIssueId, sourceIssueId)) + .then((rows) => rows[0] ?? null); + expect(sourceWorkspace).toMatchObject({ + mode: "isolated_workspace", + strategyType: "git_worktree", + baseRef: "origin/master", + }); + expect(sourceWorkspace?.branchName).toBeTruthy(); + + await writeFile(path.join(repoRoot, "base-moved.txt"), "base moved after planning\n"); + await runGit(repoRoot, ["add", "base-moved.txt"]); + await runGit(repoRoot, ["commit", "-m", "Move base after planning"]); + const movedBaseSha = await runGit(repoRoot, ["rev-parse", "HEAD"]); + await runGit(repoRoot, ["push", "origin", "HEAD:master"]); + await runGit(repoRoot, ["fetch", "origin", "master"]); + + const acceptedPlanRevisionId = await seedAcceptedPlanAcceptance({ + companyId, + issueId: sourceIssueId, + ownerAgentId: agentId, + }); + const decomposition = await issueService(db).decomposeAcceptedPlan(sourceIssueId, { + acceptedPlanRevisionId, + children: [ + { + title: "Implement approved child after base move", + status: "todo", + workMode: "standard", + priority: "medium", + assigneeAgentId: agentId, + }, + ], + actorAgentId: agentId, + }); + const childIssueId = decomposition.childIssueIds[0]; + expect(childIssueId).toBeTruthy(); + + const childBeforeRun = await db + .select({ + executionWorkspaceId: issues.executionWorkspaceId, + executionWorkspacePreference: issues.executionWorkspacePreference, + executionWorkspaceSettings: issues.executionWorkspaceSettings, + }) + .from(issues) + .where(eq(issues.id, childIssueId!)) + .then((rows) => rows[0] ?? null); + expect(childBeforeRun?.executionWorkspaceId).toBeNull(); + expect(childBeforeRun?.executionWorkspacePreference).toBeNull(); + expect(childBeforeRun?.executionWorkspaceSettings).toMatchObject({ + mode: "isolated_workspace", + workspaceStrategy: { + type: "git_worktree", + baseRef: "origin/master", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }); + + let childRunWorkspace: + | { cwd: string; branchName: string; executionWorkspaceId: string } + | null = null; + adapterExecute.mockImplementationOnce(async (input) => { + const context = (input as { context?: Record }).context ?? {}; + const workspace = context.paperclipWorkspace as Record | undefined; + const cwd = typeof workspace?.cwd === "string" ? workspace.cwd : null; + const branchName = typeof workspace?.branchName === "string" ? workspace.branchName : null; + const executionWorkspaceId = + typeof context.executionWorkspaceId === "string" ? context.executionWorkspaceId : null; + if (!cwd || !branchName || !executionWorkspaceId) { + throw new Error("Accepted-plan child run did not receive a realized workspace"); + } + childRunWorkspace = { cwd, branchName, executionWorkspaceId }; + expect(branchName).not.toBe(sourceWorkspace?.branchName); + await expect(runGit(cwd, ["rev-parse", "HEAD"])).resolves.toBe(movedBaseSha); + await db.update(issues).set({ status: "done", updatedAt: new Date() }).where(eq(issues.id, childIssueId!)); + return { + exitCode: 0, + signal: null, + timedOut: false, + sessionParams: { sessionId: "child-session" }, + sessionDisplayId: "child-session", + summary: "Child realized from the moved base.", + provider: "test", + model: "test-model", + }; + }); + + const childRun = await heartbeat.wakeup(agentId, { + source: "automation", + triggerDetail: "system", + reason: "issue_assigned", + contextSnapshot: { + issueId: childIssueId, + taskId: childIssueId, + wakeReason: "issue_assigned", + skipIssueComment: true, + }, + }); + expect(childRun).not.toBeNull(); + await vi.waitFor(async () => { + const latest = await heartbeat.getRun(childRun!.id); + expect(latest?.status).toBe("succeeded"); + }, { timeout: 10_000 }); + + expect(childRunWorkspace).not.toBeNull(); + expect(childRunWorkspace?.executionWorkspaceId).not.toBe(sourceWorkspace?.id); + const childAfterRun = await db + .select({ executionWorkspaceId: issues.executionWorkspaceId }) + .from(issues) + .where(eq(issues.id, childIssueId!)) + .then((rows) => rows[0] ?? null); + expect(childAfterRun?.executionWorkspaceId).toBe(childRunWorkspace?.executionWorkspaceId); + }, 20_000); + it("forces a fresh session and suppresses accepted-plan continuation when another issue owns the in-flight claim", async () => { const companyId = randomUUID(); const projectId = randomUUID(); diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 109f857113..0d6675133e 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3087,6 +3087,78 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { ]); }); + it("createChild preserves strategy-only workspace intent without realizing the parent workspace", async () => { + const companyId = randomUUID(); + const projectId = randomUUID(); + const parentIssueId = randomUUID(); + const projectWorkspaceId = randomUUID(); + const environmentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true }); + + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Workspace project", + status: "in_progress", + }); + + await db.insert(projectWorkspaces).values({ + id: projectWorkspaceId, + companyId, + projectId, + name: "Primary workspace", + isPrimary: true, + }); + + await db.insert(issues).values({ + id: parentIssueId, + companyId, + projectId, + projectWorkspaceId, + title: "Accepted plan parent", + status: "in_progress", + priority: "medium", + executionWorkspaceSettings: { + mode: "isolated_workspace", + environmentId, + workspaceStrategy: { + type: "git_worktree", + baseRef: "origin/master", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }, + }); + + const { issue: child } = await svc.createChild(parentIssueId, { + title: "Accepted plan child", + status: "todo", + priority: "medium", + executionWorkspaceInheritanceMode: "strategy_only", + }); + + expect(child.parentId).toBe(parentIssueId); + expect(child.projectId).toBe(projectId); + expect(child.projectWorkspaceId).toBe(projectWorkspaceId); + expect(child.executionWorkspaceId).toBeNull(); + expect(child.executionWorkspacePreference).toBeNull(); + expect(child.executionWorkspaceSettings).toEqual({ + mode: "isolated_workspace", + environmentId, + workspaceStrategy: { + type: "git_worktree", + baseRef: "origin/master", + branchTemplate: "{{issue.identifier}}-{{slug}}", + }, + }); + }); + it("clamps helper-created child requestDepth to the safe maximum", async () => { const companyId = randomUUID(); const projectId = randomUUID(); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 4809a71c73..4954b16142 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -571,7 +571,7 @@ function summarizeIssueWorkspaceForActivity( issue: IssueWorkspaceAuditInput, names: WorkspaceNameMaps, ) { - const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings); + const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings, { includeEnvironmentId: true }); const mode = settings?.mode ?? issue.executionWorkspacePreference ?? null; const executionWorkspaceId = issue.executionWorkspaceId ?? null; const projectWorkspaceId = issue.projectWorkspaceId ?? null; diff --git a/server/src/services/execution-workspace-policy.ts b/server/src/services/execution-workspace-policy.ts index 62dec3d707..f9221a3488 100644 --- a/server/src/services/execution-workspace-policy.ts +++ b/server/src/services/execution-workspace-policy.ts @@ -155,7 +155,14 @@ export function gateProjectExecutionWorkspacePolicy( return projectPolicy; } -export function parseIssueExecutionWorkspaceSettings(raw: unknown): IssueExecutionWorkspaceSettings | null { +type ParseIssueExecutionWorkspaceSettingsOptions = { + includeEnvironmentId?: boolean; +}; + +export function parseIssueExecutionWorkspaceSettings( + raw: unknown, + options: ParseIssueExecutionWorkspaceSettingsOptions = {}, +): IssueExecutionWorkspaceSettings | null { const parsed = parseObject(raw); if (Object.keys(parsed).length === 0) return null; const workspaceStrategy = parseExecutionWorkspaceStrategy(parsed.workspaceStrategy); @@ -179,6 +186,9 @@ export function parseIssueExecutionWorkspaceSettings(raw: unknown): IssueExecuti ...(normalizedMode ? { mode: normalizedMode as IssueExecutionWorkspaceSettings["mode"] } : {}), + ...(options.includeEnvironmentId && (typeof parsed.environmentId === "string" || parsed.environmentId === null) + ? { environmentId: parsed.environmentId } + : {}), ...(workspaceStrategy ? { workspaceStrategy } : {}), ...(parsed.workspaceRuntime && typeof parsed.workspaceRuntime === "object" && !Array.isArray(parsed.workspaceRuntime) ? { workspaceRuntime: { ...(parsed.workspaceRuntime as Record) } } diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 620874d712..213a3dc044 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -286,6 +286,32 @@ function buildReusedExecutionWorkspaceConfigPatchFromIssueSettings( }; } +// Accepted-plan children are not realized yet, so carry only unresolved +// workspace intent and let the first child run render/persist its own branch. +function buildPreRealizationExecutionWorkspaceSettings(raw: unknown): Record | null { + const settings = parseIssueExecutionWorkspaceSettings(raw, { includeEnvironmentId: true }); + if (!settings) return null; + const mode = + settings.mode && settings.mode !== "inherit" && settings.mode !== "reuse_existing" + ? settings.mode + : null; + const next: Record = {}; + if (mode) next.mode = mode; + if (settings.environmentId !== undefined) next.environmentId = settings.environmentId; + if (settings.workspaceRuntime) next.workspaceRuntime = settings.workspaceRuntime; + if (settings.workspaceStrategy) { + next.workspaceStrategy = { + type: settings.workspaceStrategy.type, + ...(settings.workspaceStrategy.baseRef ? { baseRef: settings.workspaceStrategy.baseRef } : {}), + ...(settings.workspaceStrategy.branchTemplate ? { branchTemplate: settings.workspaceStrategy.branchTemplate } : {}), + ...(settings.workspaceStrategy.worktreeParentDir ? { worktreeParentDir: settings.workspaceStrategy.worktreeParentDir } : {}), + ...(settings.workspaceStrategy.provisionCommand ? { provisionCommand: settings.workspaceStrategy.provisionCommand } : {}), + ...(settings.workspaceStrategy.teardownCommand ? { teardownCommand: settings.workspaceStrategy.teardownCommand } : {}), + }; + } + return Object.keys(next).length > 0 ? next : null; +} + function toTimestampMs(value: Date | string | null | undefined) { if (!value) return null; const date = value instanceof Date ? value : new Date(value); @@ -542,6 +568,7 @@ type IssueCreateInput = Omit & { labelIds?: string[]; blockedByIssueIds?: string[]; inheritExecutionWorkspaceFromIssueId?: string | null; + skipExecutionWorkspaceInheritance?: boolean; watchdog?: { agentId: string; instructions?: string | null } | null; watchdogActorRunId?: string | null; actorRunId?: string | null; @@ -551,6 +578,7 @@ type IssueCreateInput = Omit & { type IssueChildCreateInput = IssueCreateInput & { acceptanceCriteria?: string[]; blockParentUntilDone?: boolean; + executionWorkspaceInheritanceMode?: "linkage" | "strategy_only"; actorAgentId?: string | null; actorUserId?: string | null; }; @@ -741,6 +769,8 @@ const ACCEPTED_PLAN_DECOMPOSITION_FINGERPRINT_CHILD_METADATA_KEYS = new Set([ "updatedByUserId", "actorAgentId", "actorUserId", + "executionWorkspaceInheritanceMode", + "skipExecutionWorkspaceInheritance", ]); function normalizeAcceptedPlanDecompositionFingerprintChild(child: IssueChildCreateInput) { @@ -5606,14 +5636,25 @@ export function issueService(db: Db) { const { acceptanceCriteria, blockParentUntilDone, + executionWorkspaceInheritanceMode = "linkage", actorAgentId, actorUserId, ...issueData } = data; + const inheritStrategyOnly = executionWorkspaceInheritanceMode === "strategy_only"; + const hasExplicitExecutionWorkspaceOverride = + issueData.executionWorkspaceId !== undefined || + issueData.executionWorkspacePreference !== undefined || + issueData.executionWorkspaceSettings !== undefined; + const inheritedPreRealizationWorkspaceSettings = + inheritStrategyOnly && !hasExplicitExecutionWorkspaceOverride + ? buildPreRealizationExecutionWorkspaceSettings(parent.executionWorkspaceSettings) + : null; let child = await issueService(db).create(parent.companyId, { ...issueData, parentId: parent.id, projectId: issueData.projectId ?? parent.projectId, + projectWorkspaceId: issueData.projectWorkspaceId ?? (inheritStrategyOnly ? parent.projectWorkspaceId : undefined), goalId: issueData.goalId ?? parent.goalId, actorResponsibleUserId: issueData.actorResponsibleUserId ?? null, trustExplicitResponsibleUserId: issueData.trustExplicitResponsibleUserId === true, @@ -5621,7 +5662,12 @@ export function issueService(db: Db) { Math.max(clampIssueRequestDepth(parent.requestDepth) + 1, issueData.requestDepth ?? 0), ), description: appendAcceptanceCriteriaToDescription(issueData.description, acceptanceCriteria), - inheritExecutionWorkspaceFromIssueId: parent.id, + ...(inheritedPreRealizationWorkspaceSettings + ? { executionWorkspaceSettings: inheritedPreRealizationWorkspaceSettings } + : {}), + ...(inheritStrategyOnly + ? { skipExecutionWorkspaceInheritance: true } + : { inheritExecutionWorkspaceFromIssueId: parent.id }), }); if (blockParentUntilDone) { @@ -5796,7 +5842,10 @@ export function issueService(db: Db) { throw new Error("Accepted-plan decomposition child cursor moved past the requested children"); } - const createdChild = await issueService(tx as unknown as Db).createChild(sourceIssue.id, nextChildInput); + const createdChild = await issueService(tx as unknown as Db).createChild(sourceIssue.id, { + ...nextChildInput, + executionWorkspaceInheritanceMode: "strategy_only", + }); const nextIds = [...existingChildIssueIds, createdChild.issue.id]; const now = new Date(); const nextStatus = nextIds.length === data.children.length ? "completed" : "in_flight"; @@ -5924,6 +5973,7 @@ export function issueService(db: Db) { labelIds: inputLabelIds, blockedByIssueIds, inheritExecutionWorkspaceFromIssueId, + skipExecutionWorkspaceInheritance, watchdog, watchdogActorRunId, actorRunId, @@ -5956,7 +6006,9 @@ export function issueService(db: Db) { let executionWorkspacePreference = issueData.executionWorkspacePreference ?? null; let executionWorkspaceSettings = (issueData.executionWorkspaceSettings as Record | null | undefined) ?? null; - const workspaceInheritanceIssueId = inheritExecutionWorkspaceFromIssueId ?? issueData.parentId ?? null; + const workspaceInheritanceIssueId = skipExecutionWorkspaceInheritance + ? null + : inheritExecutionWorkspaceFromIssueId ?? issueData.parentId ?? null; const hasExplicitExecutionWorkspaceOverride = issueData.executionWorkspaceId !== undefined || issueData.executionWorkspacePreference !== undefined || @@ -6423,7 +6475,10 @@ export function issueService(db: Db) { let cleared = 0; for (const row of rows) { - const settings = parseIssueExecutionWorkspaceSettings(row.executionWorkspaceSettings); + const settings = parseIssueExecutionWorkspaceSettings( + row.executionWorkspaceSettings, + { includeEnvironmentId: true }, + ); if (settings?.environmentId !== environmentId) continue; await db