Apply the bracketed Convex API module references required by the kebab-case module rename
This commit is contained in:
13
packages/backend/CLAUDE.md
Normal file
13
packages/backend/CLAUDE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- convex-ai-start -->
|
||||
|
||||
This project uses [Convex](https://convex.dev) as its backend.
|
||||
|
||||
When working on Convex code, **always read
|
||||
`convex/_generated/ai/guidelines.md` first** for important guidelines on
|
||||
how to correctly use Convex APIs and patterns. The file contains rules that
|
||||
override what you may have learned about Convex from training data.
|
||||
|
||||
Convex agent skills for common tasks can be installed by running
|
||||
`npx convex ai-files install`.
|
||||
|
||||
<!-- convex-ai-end -->
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"guidelinesHash": "e72f83c02fca6a3a20f4d53731bd803a6c22ae4f9507ef575407aff86f35aa06",
|
||||
"guidelinesHash": "f730e6620e882fef21a3e00c5539cc0b472ef26688efd92bf3a42f0711de6888",
|
||||
"agentsMdSectionHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
||||
"claudeMdHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
||||
"agentSkillsSha": "ec1e6baae7d86c7843c22938c75979c016f5c6e9"
|
||||
|
||||
@@ -67,8 +67,8 @@ export default defineSchema({
|
||||
```
|
||||
|
||||
- Here are the valid Convex types along with their respective validators:
|
||||
Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|
||||
| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|
||||
| ----------- | ----------- | -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Id | string | `doc._id` | `v.id(tableName)` | |
|
||||
| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |
|
||||
| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |
|
||||
@@ -77,8 +77,9 @@ export default defineSchema({
|
||||
| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
|
||||
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
|
||||
| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |
|
||||
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
|
||||
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". |
|
||||
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "\_". |
|
||||
|
||||
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". |
|
||||
|
||||
### Function registration
|
||||
|
||||
@@ -317,6 +318,9 @@ export default app;
|
||||
- Component reads and writes participate in the calling mutation's transaction. When a component mirrors state from one of your tables (like an aggregate over a table), update the component in the SAME mutation as every insert, patch, replace, or delete of that table - never from a separate function - so the two can never drift.
|
||||
- To author a LOCAL component: a directory under `convex/` with its own `convex.config.ts` (`export default defineComponent("myName");` - the argument is the name string), its own `schema.ts`, and functions built from that directory's own `_generated/server`. Mount it from the root config (`app.use(myName)` - no options), and reference its functions through the generated `components` object INCLUDING the module segment: a function in `convex/myName/index.ts` is `components.myName.index.myFunction`, never `components.myName.myFunction`.
|
||||
- For per-key quotas, cooldowns, or throttling (N operations per period, retry-after), use the `@convex-dev/rate-limiter` component - hand-rolled counter or window-scan implementations admit races under concurrency and lose quota when a mutation fails.
|
||||
- For chat or assistant features where an LLM replies inside a durable conversation - per-user resumable histories, recorded tool-call steps, several assistants sharing one conversation - use the `@convex-dev/agent` component: mount it, create one component thread per conversation, and generate/read through it (`createThread(ctx, components.agent, ...)`, `new Agent(components.agent, { name, languageModel, tools }).generateText(ctx, { threadId }, { prompt })`, `listMessages`). Do not hand-roll a messages table or call an LLM SDK directly from your functions for these.
|
||||
- For async Convex functions needing bounded parallelism, serialized mutation work, or completion callbacks, use `@convex-dev/workpool`; retry only idempotent actions.
|
||||
- For ephemeral presence - who is online/viewing/typing in a room, tracked by client heartbeats with session tokens, multi-session aggregation (one entry per user across tabs), and timeout-to-offline - use the `@convex-dev/presence` component - hand-rolled lastSeen tables need wall-clock query filters that go stale, and per-session rows break the one-entry-per-user contract.
|
||||
- Calling a component mutation is a subtransaction: if it throws and the caller catches the error, the component's writes roll back while the calling mutation continues and can still commit its own writes.
|
||||
- To pass a function across a component boundary, mint a handle in the app: `const handle = await createFunctionHandle(internal.index.myCallback);` (from `convex/server`; async, takes only the function reference - `getFunctionHandle` and `getFunctionName` are not this API). Send it as a string; the receiver casts it back and invokes it: `await ctx.runMutation(args.handle as FunctionHandle<"mutation">, callbackArgs);`.
|
||||
|
||||
|
||||
12
packages/backend/convex/_generated/api.d.ts
vendored
12
packages/backend/convex/_generated/api.d.ts
vendored
@@ -8,7 +8,10 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as agentWorkspace from "../agentWorkspace.js";
|
||||
import type * as artifactModel from "../artifactModel.js";
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as authz from "../authz.js";
|
||||
import type * as daemonCommands from "../daemonCommands.js";
|
||||
import type * as daemonRuntime from "../daemonRuntime.js";
|
||||
import type * as daemons from "../daemons.js";
|
||||
@@ -16,6 +19,9 @@ import type * as fluePersistence from "../fluePersistence.js";
|
||||
import type * as healthCheck from "../healthCheck.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as privateData from "../privateData.js";
|
||||
import type * as projectArtifacts from "../projectArtifacts.js";
|
||||
import type * as projectIssues from "../projectIssues.js";
|
||||
import type * as projects from "../projects.js";
|
||||
import type * as todos from "../todos.js";
|
||||
|
||||
import type {
|
||||
@@ -25,7 +31,10 @@ import type {
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
agentWorkspace: typeof agentWorkspace;
|
||||
artifactModel: typeof artifactModel;
|
||||
auth: typeof auth;
|
||||
authz: typeof authz;
|
||||
daemonCommands: typeof daemonCommands;
|
||||
daemonRuntime: typeof daemonRuntime;
|
||||
daemons: typeof daemons;
|
||||
@@ -33,6 +42,9 @@ declare const fullApi: ApiFromModules<{
|
||||
healthCheck: typeof healthCheck;
|
||||
http: typeof http;
|
||||
privateData: typeof privateData;
|
||||
projectArtifacts: typeof projectArtifacts;
|
||||
projectIssues: typeof projectIssues;
|
||||
projects: typeof projects;
|
||||
todos: typeof todos;
|
||||
}>;
|
||||
|
||||
|
||||
174
packages/backend/convex/agent-workspace.ts
Normal file
174
packages/backend/convex/agent-workspace.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { env } from "@code/env/convex";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { artifactPath } from "./artifactModel";
|
||||
|
||||
const requireAgent = (token: string) => {
|
||||
if (token !== env.FLUE_DB_TOKEN) {
|
||||
throw new ConvexError("Invalid agent control token");
|
||||
}
|
||||
};
|
||||
|
||||
const agentStatus = v.union(
|
||||
v.literal("working"),
|
||||
v.literal("needs-input"),
|
||||
v.literal("completed"),
|
||||
v.literal("failed")
|
||||
);
|
||||
|
||||
export const get = query({
|
||||
args: { issueId: v.id("projectIssues"), token: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
requireAgent(args.token);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
const project = await ctx.db.get("projects", issue.projectId);
|
||||
if (!project) {
|
||||
throw new ConvexError("Project not found");
|
||||
}
|
||||
const artifacts = await ctx.db
|
||||
.query("projectArtifacts")
|
||||
.withIndex("by_project", (q) => q.eq("projectId", project._id))
|
||||
.take(25);
|
||||
return { artifacts, issue, project };
|
||||
},
|
||||
});
|
||||
|
||||
export const ensureRun = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
issueId: v.id("projectIssues"),
|
||||
token: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
requireAgent(args.token);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
const existing = await ctx.db
|
||||
.query("projectWorkRuns")
|
||||
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
|
||||
.unique();
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const actorKey = [
|
||||
"project",
|
||||
String(issue.projectId),
|
||||
"issue",
|
||||
String(issue._id),
|
||||
];
|
||||
const runId = await ctx.db.insert("projectWorkRuns", {
|
||||
actorKey,
|
||||
agentId: String(issue._id),
|
||||
createdAt: timestamp,
|
||||
daemonId: args.daemonId,
|
||||
issueId: issue._id,
|
||||
projectId: issue.projectId,
|
||||
status: "queued",
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
await ctx.db.insert("projectSignals", {
|
||||
createdAt: timestamp,
|
||||
data: { actorKey, daemonId: args.daemonId },
|
||||
issueId: issue._id,
|
||||
kind: "agent.workspace.created",
|
||||
projectId: issue.projectId,
|
||||
runId,
|
||||
});
|
||||
return await ctx.db.get("projectWorkRuns", runId);
|
||||
},
|
||||
});
|
||||
|
||||
export const updateArtifact = mutation({
|
||||
args: {
|
||||
content: v.string(),
|
||||
issueId: v.id("projectIssues"),
|
||||
path: artifactPath,
|
||||
token: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
requireAgent(args.token);
|
||||
if (args.content.length > 200_000) {
|
||||
throw new ConvexError("Artifact content exceeds 200000 characters");
|
||||
}
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
const artifact = await ctx.db
|
||||
.query("projectArtifacts")
|
||||
.withIndex("by_project_and_path", (q) =>
|
||||
q.eq("projectId", issue.projectId).eq("path", args.path)
|
||||
)
|
||||
.unique();
|
||||
if (!artifact) {
|
||||
throw new ConvexError(`Artifact ${args.path} not found`);
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("projectArtifacts", artifact._id, {
|
||||
content: args.content,
|
||||
revision: artifact.revision + 1,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
await ctx.db.insert("projectSignals", {
|
||||
createdAt: timestamp,
|
||||
data: { path: args.path, revision: artifact.revision + 1 },
|
||||
issueId: issue._id,
|
||||
kind: "artifact.updated",
|
||||
projectId: issue.projectId,
|
||||
});
|
||||
return artifact.revision + 1;
|
||||
},
|
||||
});
|
||||
|
||||
export const setStatus = mutation({
|
||||
args: {
|
||||
issueId: v.id("projectIssues"),
|
||||
status: agentStatus,
|
||||
summary: v.optional(v.string()),
|
||||
token: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
requireAgent(args.token);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
const run = await ctx.db
|
||||
.query("projectWorkRuns")
|
||||
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
|
||||
.unique();
|
||||
if (!run) {
|
||||
throw new ConvexError("Agent work run not found");
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const terminal = args.status === "completed" || args.status === "failed";
|
||||
await ctx.db.patch("projectIssues", issue._id, {
|
||||
status: args.status,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
await ctx.db.patch("projectWorkRuns", run._id, {
|
||||
status: args.status,
|
||||
summary: args.summary?.slice(0, 4000),
|
||||
updatedAt: timestamp,
|
||||
...(args.status === "working" && run.startedAt === undefined
|
||||
? { startedAt: timestamp }
|
||||
: {}),
|
||||
...(terminal ? { completedAt: timestamp } : {}),
|
||||
});
|
||||
await ctx.db.insert("projectSignals", {
|
||||
createdAt: timestamp,
|
||||
data: { summary: args.summary?.slice(0, 1000) },
|
||||
issueId: issue._id,
|
||||
kind: `agent.${args.status}`,
|
||||
projectId: issue.projectId,
|
||||
runId: run._id,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -30,65 +30,71 @@ export const artifactPath = v.union(
|
||||
v.literal("card.md")
|
||||
);
|
||||
|
||||
type ProjectSeed = {
|
||||
interface ProjectSeed {
|
||||
readonly defaultBranch: string;
|
||||
readonly description?: string;
|
||||
readonly name: string;
|
||||
readonly repoName: string;
|
||||
readonly repoOwner: string;
|
||||
readonly repoUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const createInitialArtifacts = (
|
||||
project: ProjectSeed
|
||||
): ReadonlyArray<{ path: ArtifactPath; content: string }> => {
|
||||
): readonly { path: ArtifactPath; content: string }[] => {
|
||||
const repository = `${project.repoOwner}/${project.repoName}`;
|
||||
const purpose = project.description ?? `Maintain and improve ${repository}.`;
|
||||
|
||||
return [
|
||||
{
|
||||
path: "project.md",
|
||||
content: `# ${project.name}\n\nRepository: [${repository}](${project.repoUrl})\n\n## Project knowledge\n\n- [Business](business.md)\n- [Design](design.md)\n- [Agent](agent.md)\n\n## Delivery\n\n- [Work](work.md)\n- [Steps](steps.md)\n- [Artifacts](artifacts.md)\n`,
|
||||
path: "project.md",
|
||||
},
|
||||
{
|
||||
path: "business.md",
|
||||
content: `# Business\n\n## Purpose\n\n${purpose}\n\n## Source of truth\n\nThe connected GitHub repository and its project issues define the current product work.\n`,
|
||||
path: "business.md",
|
||||
},
|
||||
{
|
||||
path: "design.md",
|
||||
content: `# Design\n\n## Repository boundary\n\nWork targets \`${repository}\` on \`${project.defaultBranch}\`. Preserve its established architecture and conventions before introducing new patterns.\n\n## Decision record\n\nRecord issue-specific architecture decisions here when they affect later work.\n`,
|
||||
path: "design.md",
|
||||
},
|
||||
{
|
||||
path: "agent.md",
|
||||
content: `# Agent\n\n## Role\n\nOwn one project issue at a time. Read the project artifacts before editing code, ask a focused question when blocked, and support completion claims with command output.\n\n## Guardrails\n\n- Keep issue work isolated in its AgentOS workspace.\n- Update work.md, steps.md, artifacts.md, and context.md as facts change.\n- Never mark work complete without a relevant runtime check.\n`,
|
||||
path: "agent.md",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Work\n\nNo issue has entered the agent queue yet. New issues are appended here by the control plane.\n",
|
||||
path: "work.md",
|
||||
content: "# Work\n\nNo issue has entered the agent queue yet. New issues are appended here by the control plane.\n",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Steps\n\n1. Read the issue and project artifacts.\n2. Inspect the repository context.\n3. Ask for missing decisions only when work cannot continue safely.\n4. Implement the smallest complete change.\n5. Run the relevant command or scenario.\n6. Publish changed artifacts and the final signal.\n",
|
||||
path: "steps.md",
|
||||
content: "# Steps\n\n1. Read the issue and project artifacts.\n2. Inspect the repository context.\n3. Ask for missing decisions only when work cannot continue safely.\n4. Implement the smallest complete change.\n5. Run the relevant command or scenario.\n6. Publish changed artifacts and the final signal.\n",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Artifacts\n\nThis file indexes concrete outputs from issue runs, including changed paths, command results, and preview links.\n",
|
||||
path: "artifacts.md",
|
||||
content: "# Artifacts\n\nThis file indexes concrete outputs from issue runs, including changed paths, command results, and preview links.\n",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Signals\n\nThe agent manager emits `issue.queued`, `agent.started`, `agent.needs-input`, `agent.completed`, and `agent.failed`. Signals are durable project events and drive the web status.\n",
|
||||
path: "signals.md",
|
||||
content: "# Signals\n\nThe agent manager emits `issue.queued`, `agent.started`, `agent.needs-input`, `agent.completed`, and `agent.failed`. Signals are durable project events and drive the web status.\n",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Agent manager\n\n## State machine\n\n`open -> queued -> working -> completed`\n\nA blocked run moves from `working` to `needs-input`; a resumed run returns to `queued`. An unrecoverable run moves to `failed`.\n\nEach issue owns one Flue agent identity and one AgentOS actor key, so conversation and workspace state remain isolated.\n",
|
||||
path: "agent-manager.md",
|
||||
content: "# Agent manager\n\n## State machine\n\n`open -> queued -> working -> completed`\n\nA blocked run moves from `working` to `needs-input`; a resumed run returns to `queued`. An unrecoverable run moves to `failed`.\n\nEach issue owns one Flue agent identity and one AgentOS actor key, so conversation and workspace state remain isolated.\n",
|
||||
},
|
||||
{
|
||||
path: "context.md",
|
||||
content: `# Context\n\n- Provider: GitHub\n- Repository: ${repository}\n- URL: ${project.repoUrl}\n- Default branch: ${project.defaultBranch}\n\nIssue-specific facts belong below this repository context.\n`,
|
||||
path: "context.md",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"# Project card\n\nThe web project card is the interface between UI and packages. It reads `projects.list`, `project-artifacts.list`, and `project-issues.list`; it writes through `projects.connectGitHub`, `project-issues.create`, and `project-issues.begin`. The issue-scoped Flue agent reads `agent-workspace.get`, publishes with `agent-workspace.updateArtifact`, and reports state with `agent-workspace.setStatus`. Filesystem and shell operations are executed by the AgentOS sandbox adapter through durable daemon commands.\n",
|
||||
path: "card.md",
|
||||
content: "# Project card\n\nThe web project card is the interface between UI and packages. It reads `projects.list`, `projectArtifacts.list`, and `projectIssues.list`; it writes through `projects.connectGitHub`, `projectIssues.create`, and `projectIssues.begin`. The issue-scoped Flue agent reads `agentWorkspace.get`, publishes with `agentWorkspace.updateArtifact`, and reports state with `agentWorkspace.setStatus`. Filesystem and shell operations are executed by the AgentOS sandbox adapter through durable daemon commands.\n",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ConvexError } from "convex/values";
|
||||
|
||||
export type AuthContext = {
|
||||
export interface AuthContext {
|
||||
readonly auth: {
|
||||
readonly getUserIdentity: () => Promise<{
|
||||
readonly tokenIdentifier: string;
|
||||
} | null>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const requireOwnerId = async (ctx: AuthContext): Promise<string> => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
|
||||
@@ -52,6 +52,11 @@ export const list = query({
|
||||
.take(100);
|
||||
},
|
||||
});
|
||||
export const get = query({
|
||||
args: { commandId: v.id("daemonCommands") },
|
||||
handler: async (ctx, args) =>
|
||||
await ctx.db.get("daemonCommands", args.commandId),
|
||||
});
|
||||
|
||||
export const available = query({
|
||||
args: { daemonId: v.string() },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ConvexError, v } from "convex/values";
|
||||
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import type { MutationCtx } from "./_generated/server";
|
||||
import { requireOwnerId } from "./authz";
|
||||
|
||||
const requireProjectOwner = async (
|
||||
ctx: Parameters<Parameters<typeof mutation>[0]["handler"]>[0],
|
||||
projectId: Parameters<typeof ctx.db.get<"projects">>[1],
|
||||
ctx: MutationCtx,
|
||||
projectId: Id<"projects">,
|
||||
ownerId: string
|
||||
) => {
|
||||
const project = await ctx.db.get("projects", projectId);
|
||||
@@ -17,9 +19,9 @@ const requireProjectOwner = async (
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
body: v.string(),
|
||||
projectId: v.id("projects"),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireOwnerId(ctx);
|
||||
@@ -30,7 +32,9 @@ export const create = mutation({
|
||||
throw new ConvexError("Issue title must be between 3 and 160 characters");
|
||||
}
|
||||
if (body.length < 10 || body.length > 10_000) {
|
||||
throw new ConvexError("Issue description must be between 10 and 10000 characters");
|
||||
throw new ConvexError(
|
||||
"Issue description must be between 10 and 10000 characters"
|
||||
);
|
||||
}
|
||||
const latest = await ctx.db
|
||||
.query("projectIssues")
|
||||
@@ -42,12 +46,12 @@ export const create = mutation({
|
||||
const timestamp = Date.now();
|
||||
const number = (latest?.number ?? 0) + 1;
|
||||
const issueId = await ctx.db.insert("projectIssues", {
|
||||
projectId: args.projectId,
|
||||
number,
|
||||
title,
|
||||
body,
|
||||
status: "open",
|
||||
createdAt: timestamp,
|
||||
number,
|
||||
projectId: args.projectId,
|
||||
status: "open",
|
||||
title,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
const workArtifact = await ctx.db
|
||||
@@ -64,11 +68,11 @@ export const create = mutation({
|
||||
});
|
||||
}
|
||||
await ctx.db.insert("projectSignals", {
|
||||
projectId: args.projectId,
|
||||
createdAt: timestamp,
|
||||
data: { number, title },
|
||||
issueId,
|
||||
kind: "issue.created",
|
||||
data: { number, title },
|
||||
createdAt: timestamp,
|
||||
projectId: args.projectId,
|
||||
});
|
||||
return issueId;
|
||||
},
|
||||
@@ -92,18 +96,18 @@ export const begin = mutation({
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
await ctx.db.insert("projectSignals", {
|
||||
projectId: issue.projectId,
|
||||
createdAt: timestamp,
|
||||
data: { number: issue.number },
|
||||
issueId: issue._id,
|
||||
kind: "issue.queued",
|
||||
data: { number: issue.number },
|
||||
createdAt: timestamp,
|
||||
projectId: issue.projectId,
|
||||
});
|
||||
return "queued" as const;
|
||||
},
|
||||
});
|
||||
|
||||
export const markDispatchFailed = mutation({
|
||||
args: { issueId: v.id("projectIssues"), error: v.string() },
|
||||
args: { error: v.string(), issueId: v.id("projectIssues") },
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireOwnerId(ctx);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
@@ -117,11 +121,11 @@ export const markDispatchFailed = mutation({
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
await ctx.db.insert("projectSignals", {
|
||||
projectId: issue.projectId,
|
||||
createdAt: timestamp,
|
||||
data: { error: args.error.slice(0, 1000), phase: "dispatch" },
|
||||
issueId: issue._id,
|
||||
kind: "agent.failed",
|
||||
data: { error: args.error.slice(0, 1000), phase: "dispatch" },
|
||||
createdAt: timestamp,
|
||||
projectId: issue.projectId,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import { z } from "zod";
|
||||
|
||||
import { internal } from "./_generated/api";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
@@ -6,52 +7,40 @@ import { action, internalMutation, query } from "./_generated/server";
|
||||
import { createInitialArtifacts } from "./artifactModel";
|
||||
import { requireOwnerId } from "./authz";
|
||||
|
||||
type GitHubRepository = {
|
||||
readonly defaultBranch: string;
|
||||
readonly description?: string;
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly owner: string;
|
||||
readonly url: string;
|
||||
};
|
||||
const gitHubRepositorySchema = z.object({
|
||||
default_branch: z.string(),
|
||||
description: z.string().nullable(),
|
||||
html_url: z.url(),
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
owner: z.object({ login: z.string() }),
|
||||
});
|
||||
|
||||
const parseRepository = (value: string): { owner: string; repo: string } => {
|
||||
const normalized = value.trim().replace(/\.git$/u, "");
|
||||
const match = normalized.match(
|
||||
/^(?:https?:\/\/github\.com\/)?([^/\s]+)\/([^/\s]+)$/iu
|
||||
/^(?:https?:\/\/github\.com\/)?(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)$/iu
|
||||
);
|
||||
if (!match?.[1] || !match[2]) {
|
||||
if (!match?.groups?.owner || !match.groups.repo) {
|
||||
throw new ConvexError("Use a GitHub repository in owner/name format");
|
||||
}
|
||||
return { owner: match[1], repo: match[2] };
|
||||
return { owner: match.groups.owner, repo: match.groups.repo };
|
||||
};
|
||||
|
||||
const readGitHubRepository = (value: unknown): GitHubRepository => {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
throw new ConvexError("GitHub returned an invalid repository response");
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const owner = record.owner;
|
||||
if (
|
||||
typeof record.id !== "number" ||
|
||||
typeof record.name !== "string" ||
|
||||
typeof record.html_url !== "string" ||
|
||||
typeof record.default_branch !== "string" ||
|
||||
typeof owner !== "object" ||
|
||||
owner === null ||
|
||||
typeof (owner as Record<string, unknown>).login !== "string"
|
||||
) {
|
||||
const readGitHubRepository = (value: unknown) => {
|
||||
const result = gitHubRepositorySchema.safeParse(value);
|
||||
if (!result.success) {
|
||||
throw new ConvexError("GitHub returned incomplete repository metadata");
|
||||
}
|
||||
return {
|
||||
defaultBranch: record.default_branch,
|
||||
...(typeof record.description === "string"
|
||||
? { description: record.description }
|
||||
: {}),
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
owner: (owner as { login: string }).login,
|
||||
url: record.html_url,
|
||||
defaultBranch: result.data.default_branch,
|
||||
...(result.data.description === null
|
||||
? {}
|
||||
: { description: result.data.description }),
|
||||
id: result.data.id,
|
||||
name: result.data.name,
|
||||
owner: result.data.owner.login,
|
||||
url: result.data.html_url,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,13 +49,16 @@ export const connectGitHub = action({
|
||||
handler: async (ctx, args): Promise<Id<"projects">> => {
|
||||
const ownerId = await requireOwnerId(ctx);
|
||||
const { owner, repo } = parseRepository(args.repository);
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "zopu-code",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "zopu-code",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new ConvexError(
|
||||
@@ -120,16 +112,16 @@ export const storeGitHubProject = internalMutation({
|
||||
}
|
||||
|
||||
const projectId = await ctx.db.insert("projects", {
|
||||
createdAt: timestamp,
|
||||
defaultBranch: args.repository.defaultBranch,
|
||||
description: args.repository.description,
|
||||
name: args.repository.name,
|
||||
ownerId: args.ownerId,
|
||||
provider: "github",
|
||||
providerRepoId: args.repository.id,
|
||||
name: args.repository.name,
|
||||
description: args.repository.description,
|
||||
repoOwner: args.repository.owner,
|
||||
repoName: args.repository.name,
|
||||
repoOwner: args.repository.owner,
|
||||
repoUrl: args.repository.url,
|
||||
defaultBranch: args.repository.defaultBranch,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
const artifacts = createInitialArtifacts({
|
||||
@@ -143,20 +135,20 @@ export const storeGitHubProject = internalMutation({
|
||||
await Promise.all(
|
||||
artifacts.map(({ content, path }) =>
|
||||
ctx.db.insert("projectArtifacts", {
|
||||
projectId,
|
||||
path,
|
||||
content,
|
||||
revision: 1,
|
||||
createdAt: timestamp,
|
||||
path,
|
||||
projectId,
|
||||
revision: 1,
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
)
|
||||
);
|
||||
await ctx.db.insert("projectSignals", {
|
||||
projectId,
|
||||
kind: "project.connected",
|
||||
data: { provider: "github", repository: args.repository.url },
|
||||
createdAt: timestamp,
|
||||
data: { provider: "github", repository: args.repository.url },
|
||||
kind: "project.connected",
|
||||
projectId,
|
||||
});
|
||||
return projectId;
|
||||
},
|
||||
|
||||
@@ -197,11 +197,7 @@ export default defineSchema({
|
||||
settlementRecordId: v.optional(v.string()),
|
||||
settlementRecordJson: v.optional(v.string()),
|
||||
settledOutcome: v.optional(
|
||||
v.union(
|
||||
v.literal("completed"),
|
||||
v.literal("failed"),
|
||||
v.literal("aborted")
|
||||
)
|
||||
v.union(v.literal("completed"), v.literal("failed"), v.literal("aborted"))
|
||||
),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
@@ -235,8 +231,7 @@ export default defineSchema({
|
||||
nextOffset: v.number(),
|
||||
closed: v.boolean(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_path", ["path"]),
|
||||
}).index("by_path", ["path"]),
|
||||
|
||||
// Conversation-stream batches: one row per appended batch. Offset is
|
||||
// 0-based; `seq` is the row's position in the stream.
|
||||
@@ -252,10 +247,12 @@ export default defineSchema({
|
||||
appendedAt: v.number(),
|
||||
})
|
||||
.index("by_path_and_seq", ["path", "seq"])
|
||||
.index(
|
||||
"by_path_producer_epoch_producerSequence",
|
||||
["path", "producerId", "producerEpoch", "producerSequence"]
|
||||
),
|
||||
.index("by_path_producer_epoch_producerSequence", [
|
||||
"path",
|
||||
"producerId",
|
||||
"producerEpoch",
|
||||
"producerSequence",
|
||||
]),
|
||||
|
||||
// Event-stream metadata: one row per stream path.
|
||||
flueEventStreams: defineTable({
|
||||
@@ -329,5 +326,5 @@ export default defineSchema({
|
||||
"streamPath",
|
||||
"conversationId",
|
||||
"attachmentId",
|
||||
])
|
||||
]),
|
||||
});
|
||||
|
||||
41
packages/backend/skills-lock.json
Normal file
41
packages/backend/skills-lock.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"convex": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex/SKILL.md",
|
||||
"computedHash": "c5f3622c64ef550aac27d1dbc041f0c7c40d9119863c9fb8bac180b0498ee8ed"
|
||||
},
|
||||
"convex-create-component": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex-create-component/SKILL.md",
|
||||
"computedHash": "012acb639fccc22a47e89ef69941689f9328ac9ff5b872d77af6328407ec8876"
|
||||
},
|
||||
"convex-migration-helper": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex-migration-helper/SKILL.md",
|
||||
"computedHash": "1ed5ef230d484e9884d881ddbd15158f0070382f8c6097364cb8ed2ec458decd"
|
||||
},
|
||||
"convex-performance-audit": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex-performance-audit/SKILL.md",
|
||||
"computedHash": "2eaf22b20f74ab39f336b41845b834b8796b6d2b380bc078a1ab54769c3f233c"
|
||||
},
|
||||
"convex-quickstart": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex-quickstart/SKILL.md",
|
||||
"computedHash": "54e5271d5d613cc4c0068baa250613b5abb6e4533df1e67adc26d2e0c7d68ded"
|
||||
},
|
||||
"convex-setup-auth": {
|
||||
"source": "get-convex/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/convex-setup-auth/SKILL.md",
|
||||
"computedHash": "b1a940758751c5b2fdc6ced105b19927a1655f0c1d4bd2fd5536dc3264202c00"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user