feat: prepare issue workspaces from connected repositories

This commit is contained in:
-Puter
2026-07-24 02:19:35 +05:30
parent 7974bd5f3e
commit 7caad057db
12 changed files with 794 additions and 25 deletions

View File

@@ -1,5 +1,12 @@
import { env } from "@code/env/convex";
import {
makeIssueWorkspacePlan,
transitionWorkspaceRun,
WorkspaceStateError,
WorkspaceValidationError,
} from "@code/primitives/project-workspace";
import { ConvexError, v } from "convex/values";
import { Effect } from "effect";
import { mutation, query } from "./_generated/server";
import { artifactPath } from "./artifactModel";
@@ -17,6 +24,37 @@ const agentStatus = v.union(
v.literal("failed")
);
const workspacePlanFor = (input: {
readonly issueBody: string;
readonly issueId: string;
readonly issueNumber: number;
readonly issueTitle: string;
readonly sourceUrl: string | undefined;
readonly defaultBranch: string | undefined;
}) => {
try {
return Effect.runSync(
makeIssueWorkspacePlan({
artifacts: [],
branchName: undefined,
checkoutPath: undefined,
contextFiles: [],
defaultBranch: input.defaultBranch,
issueBody: input.issueBody,
issueId: input.issueId,
issueNumber: input.issueNumber,
issueTitle: input.issueTitle,
sourceUrl: input.sourceUrl,
})
);
} catch (error) {
if (error instanceof WorkspaceValidationError) {
throw new ConvexError(error.message);
}
throw error;
}
};
export const get = query({
args: { issueId: v.id("projectIssues"), token: v.string() },
handler: async (ctx, args) => {
@@ -33,7 +71,26 @@ export const get = query({
.query("projectArtifacts")
.withIndex("by_project", (q) => q.eq("projectId", project._id))
.take(25);
return { artifacts, issue, project };
const [source] = await ctx.db
.query("projectSources")
.withIndex("by_project", (q) => q.eq("projectId", project._id))
.take(1);
const contextDocuments = await ctx.db
.query("projectContextDocuments")
.withIndex("by_project", (q) => q.eq("projectId", project._id))
.take(25);
const run = await ctx.db
.query("projectWorkRuns")
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
.unique();
return {
artifacts,
contextDocuments,
issue,
project,
run: run ?? null,
source: source ?? null,
};
},
});
@@ -49,12 +106,38 @@ export const ensureRun = mutation({
if (!issue) {
throw new ConvexError("Issue not found");
}
const [source] = await ctx.db
.query("projectSources")
.withIndex("by_project", (q) => q.eq("projectId", issue.projectId))
.take(1);
const plan = workspacePlanFor({
defaultBranch: source?.defaultBranch,
issueBody: issue.body,
issueId: String(issue._id),
issueNumber: issue.number,
issueTitle: issue.title,
sourceUrl: source?.url,
});
const existing = await ctx.db
.query("projectWorkRuns")
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
.unique();
if (existing) {
return existing;
if (
existing.baseBranch !== plan.baseBranch ||
existing.branchName !== plan.branchName ||
existing.checkoutPath !== plan.checkoutPath ||
existing.sourceUrl !== plan.sourceUrl
) {
await ctx.db.patch("projectWorkRuns", existing._id, {
baseBranch: plan.baseBranch,
branchName: plan.branchName,
checkoutPath: plan.checkoutPath,
sourceUrl: plan.sourceUrl,
updatedAt: Date.now(),
});
}
return await ctx.db.get("projectWorkRuns", existing._id);
}
const timestamp = Date.now();
const actorKey = [
@@ -66,16 +149,26 @@ export const ensureRun = mutation({
const runId = await ctx.db.insert("projectWorkRuns", {
actorKey,
agentId: String(issue._id),
baseBranch: plan.baseBranch,
branchName: plan.branchName,
checkoutPath: plan.checkoutPath,
createdAt: timestamp,
daemonId: args.daemonId,
issueId: issue._id,
projectId: issue.projectId,
sourceUrl: plan.sourceUrl,
status: "queued",
updatedAt: timestamp,
});
await ctx.db.insert("projectEvents", {
createdAt: timestamp,
data: { actorKey, daemonId: args.daemonId },
data: {
actorKey,
branchName: plan.branchName,
checkoutPath: plan.checkoutPath,
daemonId: args.daemonId,
sourceUrl: plan.sourceUrl,
},
issueId: issue._id,
kind: "agent.workspace.created",
projectId: issue.projectId,
@@ -147,6 +240,19 @@ export const setStatus = mutation({
if (!run) {
throw new ConvexError("Agent work run not found");
}
try {
Effect.runSync(
transitionWorkspaceRun({
from: run.status,
to: args.status,
})
);
} catch (error) {
if (error instanceof WorkspaceStateError) {
throw new ConvexError(error.message);
}
throw error;
}
const timestamp = Date.now();
const terminal = args.status === "completed" || args.status === "failed";
await ctx.db.patch("projectIssues", issue._id, {