Implement execution workspaces and work products

This commit is contained in:
Dotta
2026-03-13 17:12:25 -05:00
parent 9da5358bb3
commit 920bc4c70f
45 changed files with 9157 additions and 140 deletions

View File

@@ -7,6 +7,7 @@ import {
companyMemberships,
goals,
heartbeatRuns,
executionWorkspaces,
issueAttachments,
issueLabels,
issueComments,
@@ -353,6 +354,40 @@ export function issueService(db: Db) {
}
}
async function assertValidProjectWorkspace(companyId: string, projectId: string | null | undefined, projectWorkspaceId: string) {
const workspace = await db
.select({
id: projectWorkspaces.id,
companyId: projectWorkspaces.companyId,
projectId: projectWorkspaces.projectId,
})
.from(projectWorkspaces)
.where(eq(projectWorkspaces.id, projectWorkspaceId))
.then((rows) => rows[0] ?? null);
if (!workspace) throw notFound("Project workspace not found");
if (workspace.companyId !== companyId) throw unprocessable("Project workspace must belong to same company");
if (projectId && workspace.projectId !== projectId) {
throw unprocessable("Project workspace must belong to the selected project");
}
}
async function assertValidExecutionWorkspace(companyId: string, projectId: string | null | undefined, executionWorkspaceId: string) {
const workspace = await db
.select({
id: executionWorkspaces.id,
companyId: executionWorkspaces.companyId,
projectId: executionWorkspaces.projectId,
})
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, executionWorkspaceId))
.then((rows) => rows[0] ?? null);
if (!workspace) throw notFound("Execution workspace not found");
if (workspace.companyId !== companyId) throw unprocessable("Execution workspace must belong to same company");
if (projectId && workspace.projectId !== projectId) {
throw unprocessable("Execution workspace must belong to the selected project");
}
}
async function assertValidLabelIds(companyId: string, labelIds: string[], dbOrTx: any = db) {
if (labelIds.length === 0) return;
const existing = await dbOrTx
@@ -647,6 +682,12 @@ export function issueService(db: Db) {
if (data.assigneeUserId) {
await assertAssignableUser(companyId, data.assigneeUserId);
}
if (data.projectWorkspaceId) {
await assertValidProjectWorkspace(companyId, data.projectId, data.projectWorkspaceId);
}
if (data.executionWorkspaceId) {
await assertValidExecutionWorkspace(companyId, data.projectId, data.executionWorkspaceId);
}
if (data.status === "in_progress" && !data.assigneeAgentId && !data.assigneeUserId) {
throw unprocessable("in_progress issues require an assignee");
}
@@ -665,6 +706,26 @@ export function issueService(db: Db) {
parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy),
) as Record<string, unknown> | null;
}
let projectWorkspaceId = issueData.projectWorkspaceId ?? null;
if (!projectWorkspaceId && issueData.projectId) {
const project = await tx
.select({
executionWorkspacePolicy: projects.executionWorkspacePolicy,
})
.from(projects)
.where(and(eq(projects.id, issueData.projectId), eq(projects.companyId, companyId)))
.then((rows) => rows[0] ?? null);
const projectPolicy = parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy);
projectWorkspaceId = projectPolicy?.defaultProjectWorkspaceId ?? null;
if (!projectWorkspaceId) {
projectWorkspaceId = await tx
.select({ id: projectWorkspaces.id })
.from(projectWorkspaces)
.where(and(eq(projectWorkspaces.projectId, issueData.projectId), eq(projectWorkspaces.companyId, companyId)))
.orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
.then((rows) => rows[0]?.id ?? null);
}
}
const [company] = await tx
.update(companies)
.set({ issueCounter: sql`${companies.issueCounter} + 1` })
@@ -681,6 +742,7 @@ export function issueService(db: Db) {
goalId: issueData.goalId,
defaultGoalId: defaultCompanyGoal?.id ?? null,
}),
...(projectWorkspaceId ? { projectWorkspaceId } : {}),
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
companyId,
issueNumber,
@@ -741,6 +803,17 @@ export function issueService(db: Db) {
if (issueData.assigneeUserId) {
await assertAssignableUser(existing.companyId, issueData.assigneeUserId);
}
const nextProjectId = issueData.projectId !== undefined ? issueData.projectId : existing.projectId;
const nextProjectWorkspaceId =
issueData.projectWorkspaceId !== undefined ? issueData.projectWorkspaceId : existing.projectWorkspaceId;
const nextExecutionWorkspaceId =
issueData.executionWorkspaceId !== undefined ? issueData.executionWorkspaceId : existing.executionWorkspaceId;
if (nextProjectWorkspaceId) {
await assertValidProjectWorkspace(existing.companyId, nextProjectId, nextProjectWorkspaceId);
}
if (nextExecutionWorkspaceId) {
await assertValidExecutionWorkspace(existing.companyId, nextProjectId, nextExecutionWorkspaceId);
}
applyStatusSideEffects(issueData.status, patch);
if (issueData.status && issueData.status !== "done") {