From f3ad1fc301f5c897182b05cee66f7b8e439328b6 Mon Sep 17 00:00:00 2001 From: lempkey Date: Mon, 6 Apr 2026 16:19:41 +0100 Subject: [PATCH 01/18] fix: use prefix-aware Link for export/import on Company Settings page The Export and Import buttons in CompanySettings used plain anchors which bypass the router's company-prefix wrapper. The links resolved to /company/export and /company/import instead of /:prefix/company/export, showing a 'Company not found' error. Replace both elements with from @/lib/router, which calls applyCompanyPrefix under the hood and correctly resolves to /:prefix/company/{export,import} regardless of which company is active. Fixes: #2910 --- ui/src/lib/company-routes.test.ts | 25 +++++++++++++++++++++++++ ui/src/pages/CompanySettings.tsx | 9 +++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/ui/src/lib/company-routes.test.ts b/ui/src/lib/company-routes.test.ts index d6dc2668..ced25744 100644 --- a/ui/src/lib/company-routes.test.ts +++ b/ui/src/lib/company-routes.test.ts @@ -20,4 +20,29 @@ describe("company routes", () => { "/execution-workspaces/workspace-123", ); }); + + /** + * Regression tests for https://github.com/paperclipai/paperclip/issues/2910 + * + * The Export and Import links on the Company Settings page used plain + * `` anchors which bypass the router's Link + * wrapper. Without the wrapper, the company prefix is never applied and + * the links resolve to `/company/export` instead of `/:prefix/company/export`, + * producing a "Company not found" error. + * + * The fix replaces the `` elements with the prefix-aware `` from + * `@/lib/router`. These tests assert that the underlying `applyCompanyPrefix` + * utility (used by that Link) correctly rewrites the export/import paths. + */ + it("applies company prefix to /company/export", () => { + expect(applyCompanyPrefix("/company/export", "PAP")).toBe("/PAP/company/export"); + }); + + it("applies company prefix to /company/import", () => { + expect(applyCompanyPrefix("/company/import", "PAP")).toBe("/PAP/company/import"); + }); + + it("does not double-apply the prefix if already present", () => { + expect(applyCompanyPrefix("/PAP/company/export", "PAP")).toBe("/PAP/company/export"); + }); }); diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index e0db3706..30b39627 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -1,4 +1,5 @@ import { ChangeEvent, useEffect, useState } from "react"; +import { Link } from "@/lib/router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; @@ -548,16 +549,16 @@ export function CompanySettings() {

From 8be6fe987b67065a3c8bb5ac8ae7b1e444b01cf9 Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 4 Apr 2026 20:02:04 -0500 Subject: [PATCH 02/18] Repair stale worktree links before runtime start Co-Authored-By: Paperclip --- .../src/__tests__/workspace-runtime.test.ts | 93 ++++++++++- server/src/services/workspace-runtime.ts | 154 +++++++++++++++++- 2 files changed, 245 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 911010f5..0d6211c1 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -20,6 +20,7 @@ import { import { eq } from "drizzle-orm"; import { cleanupExecutionWorkspaceArtifacts, + ensureServerWorkspaceLinksCurrent, ensureRuntimeServicesForRun, normalizeAdapterManagedRuntimeServices, reconcilePersistedRuntimeServicesOnStartup, @@ -187,6 +188,96 @@ describe("sanitizeRuntimeServiceBaseEnv", () => { }); }); +describe("ensureServerWorkspaceLinksCurrent", () => { + it("relinks stale server workspace dependencies inside the current repo root", async () => { + const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-")); + const staleRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-stale-")); + const serverNodeModulesScopeDir = path.join(repoRoot, "server", "node_modules", "@paperclipai"); + const expectedPackageDir = path.join(repoRoot, "packages", "db"); + const stalePackageDir = path.join(staleRoot, "db"); + + await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); + await fs.mkdir(expectedPackageDir, { recursive: true }); + await fs.mkdir(stalePackageDir, { recursive: true }); + await fs.mkdir(serverNodeModulesScopeDir, { recursive: true }); + await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8"); + await fs.writeFile( + path.join(repoRoot, "server", "package.json"), + JSON.stringify({ + name: "@paperclipai/server", + dependencies: { + "@paperclipai/db": "workspace:*", + }, + }), + "utf8", + ); + await fs.writeFile( + path.join(expectedPackageDir, "package.json"), + JSON.stringify({ name: "@paperclipai/db" }), + "utf8", + ); + await fs.writeFile( + path.join(stalePackageDir, "package.json"), + JSON.stringify({ name: "@paperclipai/db" }), + "utf8", + ); + await fs.symlink(stalePackageDir, path.join(serverNodeModulesScopeDir, "db")); + + const commands: Array<{ command: string; args: string[]; cwd: string }> = []; + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { + runCommand: async (command, args, cwd) => { + commands.push({ command, args, cwd }); + await fs.rm(path.join(serverNodeModulesScopeDir, "db"), { force: true }); + await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); + }, + }); + + expect(commands).toHaveLength(1); + expect(commands[0]).toMatchObject({ + command: process.platform === "win32" ? "pnpm.cmd" : "pnpm", + args: ["install", "--force", "--config.confirmModulesPurge=false"], + cwd: repoRoot, + }); + expect(await fs.realpath(path.join(serverNodeModulesScopeDir, "db"))).toBe(await fs.realpath(expectedPackageDir)); + }); + + it("skips relinking when server workspace dependencies already point at the repo", async () => { + const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-current-")); + const serverNodeModulesScopeDir = path.join(repoRoot, "server", "node_modules", "@paperclipai"); + const expectedPackageDir = path.join(repoRoot, "packages", "db"); + + await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); + await fs.mkdir(expectedPackageDir, { recursive: true }); + await fs.mkdir(serverNodeModulesScopeDir, { recursive: true }); + await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8"); + await fs.writeFile( + path.join(repoRoot, "server", "package.json"), + JSON.stringify({ + name: "@paperclipai/server", + dependencies: { + "@paperclipai/db": "workspace:*", + }, + }), + "utf8", + ); + await fs.writeFile( + path.join(expectedPackageDir, "package.json"), + JSON.stringify({ name: "@paperclipai/db" }), + "utf8", + ); + await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); + + let invoked = false; + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { + runCommand: async () => { + invoked = true; + }, + }); + + expect(invoked).toBe(false); + }); +}); + describe("realizeExecutionWorkspace", () => { it("creates and reuses a git worktree for an issue-scoped branch", async () => { const repoRoot = await createTempRepo(); @@ -663,7 +754,7 @@ describe("realizeExecutionWorkspace", () => { await fs.realpath(path.join(repoRoot, "packages", "shared")), ); }, - 15_000, + 30_000, ); it("records worktree setup and provision operations when a recorder is provided", async () => { diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 44040311..e9e97548 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -1,4 +1,5 @@ import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import fs from "node:fs/promises"; import net from "node:net"; import { createHash, randomUUID } from "node:crypto"; @@ -122,6 +123,153 @@ function stableStringify(value: unknown): string { return JSON.stringify(value); } +type WorkspaceLinkMismatch = { + packageName: string; + expectedPath: string; + actualPath: string | null; +}; + +function readJsonFile(filePath: string): Record { + return JSON.parse(readFileSync(filePath, "utf8")) as Record; +} + +function findWorkspaceRoot(startCwd: string) { + let current = path.resolve(startCwd); + while (true) { + if (existsSync(path.join(current, "pnpm-workspace.yaml"))) { + return current; + } + const parent = path.dirname(current); + if (parent === current) return null; + current = parent; + } +} + +function discoverWorkspacePackagePaths(rootDir: string): Map { + const packagePaths = new Map(); + const ignoredDirNames = new Set([".git", ".paperclip", "dist", "node_modules"]); + + function visit(dirPath: string) { + if (!existsSync(dirPath)) return; + + const packageJsonPath = path.join(dirPath, "package.json"); + if (existsSync(packageJsonPath)) { + const packageJson = readJsonFile(packageJsonPath); + if (typeof packageJson.name === "string" && packageJson.name.length > 0) { + packagePaths.set(packageJson.name, dirPath); + } + } + + for (const entry of readdirSync(dirPath, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + if (ignoredDirNames.has(entry.name)) continue; + visit(path.join(dirPath, entry.name)); + } + } + + visit(path.join(rootDir, "packages")); + visit(path.join(rootDir, "server")); + visit(path.join(rootDir, "ui")); + visit(path.join(rootDir, "cli")); + + return packagePaths; +} + +function findServerWorkspaceLinkMismatches(rootDir: string): WorkspaceLinkMismatch[] { + const serverPackageJsonPath = path.join(rootDir, "server", "package.json"); + if (!existsSync(serverPackageJsonPath)) return []; + + const serverPackageJson = readJsonFile(serverPackageJsonPath); + const dependencies = { + ...(serverPackageJson.dependencies as Record | undefined), + ...(serverPackageJson.devDependencies as Record | undefined), + }; + const workspacePackagePaths = discoverWorkspacePackagePaths(rootDir); + const mismatches: WorkspaceLinkMismatch[] = []; + + for (const [packageName, version] of Object.entries(dependencies)) { + if (typeof version !== "string" || !version.startsWith("workspace:")) continue; + + const expectedPath = workspacePackagePaths.get(packageName); + if (!expectedPath) continue; + const normalizedExpectedPath = existsSync(expectedPath) ? path.resolve(realpathSync(expectedPath)) : path.resolve(expectedPath); + + const linkPath = path.join(rootDir, "server", "node_modules", ...packageName.split("/")); + const actualPath = existsSync(linkPath) ? path.resolve(realpathSync(linkPath)) : null; + if (actualPath === normalizedExpectedPath) continue; + + mismatches.push({ + packageName, + expectedPath: normalizedExpectedPath, + actualPath, + }); + } + + return mismatches; +} + +async function runCommand(command: string, args: string[], cwd: string) { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + env: process.env, + stdio: "ignore", + shell: process.platform === "win32", + }); + + child.on("error", reject); + child.on("exit", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject( + new Error( + `${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`, + ), + ); + }); + }); +} + +export async function ensureServerWorkspaceLinksCurrent( + startCwd: string, + opts?: { + onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; + runCommand?: (command: string, args: string[], cwd: string) => Promise; + }, +) { + const workspaceRoot = findWorkspaceRoot(startCwd); + if (!workspaceRoot) return; + + const mismatches = findServerWorkspaceLinkMismatches(workspaceRoot); + if (mismatches.length === 0) return; + + if (opts?.onLog) { + await opts.onLog("stdout", "[runtime] detected stale workspace package links for server; relinking dependencies...\n"); + for (const mismatch of mismatches) { + await opts.onLog( + "stdout", + `[runtime] ${mismatch.packageName}: ${mismatch.actualPath ?? "missing"} -> ${mismatch.expectedPath}\n`, + ); + } + } + + const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; + await (opts?.runCommand ?? runCommand)( + pnpmBin, + ["install", "--force", "--config.confirmModulesPurge=false"], + workspaceRoot, + ); + + const remainingMismatches = findServerWorkspaceLinkMismatches(workspaceRoot); + if (remainingMismatches.length === 0) return; + + throw new Error( + `Workspace relink did not repair all server package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`, + ); +} + export function sanitizeRuntimeServiceBaseEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...baseEnv }; for (const key of Object.keys(env)) { @@ -1374,7 +1522,11 @@ async function startLocalRuntimeService(input: { ); } } - + + await ensureServerWorkspaceLinksCurrent(serviceCwd, { + onLog: input.onLog, + }); + const shell = resolveShell(); const child = spawn(shell, ["-lc", command], { cwd: serviceCwd, From 7e34d6c66b3a0b36b54cf7d0cd974d02d79b5de2 Mon Sep 17 00:00:00 2001 From: dotta Date: Sun, 5 Apr 2026 22:42:54 -0500 Subject: [PATCH 03/18] Fix worktree provisioning and relinking Co-Authored-By: Paperclip --- scripts/ensure-workspace-package-links.ts | 63 +++++++------------ scripts/provision-worktree.sh | 2 +- .../src/__tests__/workspace-runtime.test.ts | 25 +------- server/src/services/workspace-runtime.ts | 37 ++--------- ui/package.json | 7 ++- 5 files changed, 35 insertions(+), 99 deletions(-) diff --git a/scripts/ensure-workspace-package-links.ts b/scripts/ensure-workspace-package-links.ts index 430ba589..8ff86b71 100644 --- a/scripts/ensure-workspace-package-links.ts +++ b/scripts/ensure-workspace-package-links.ts @@ -1,10 +1,11 @@ #!/usr/bin/env -S node --import tsx -import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import path from "node:path"; import { repoRoot } from "./dev-service-profile.ts"; type WorkspaceLinkMismatch = { + workspaceDir: string; packageName: string; expectedPath: string; actualPath: string | null; @@ -44,11 +45,11 @@ function discoverWorkspacePackagePaths(rootDir: string): Map { const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot); -function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { - const serverPackageJson = readJsonFile(path.join(repoRoot, "server", "package.json")); +function findWorkspaceLinkMismatches(workspaceDir: string): WorkspaceLinkMismatch[] { + const packageJson = readJsonFile(path.join(repoRoot, workspaceDir, "package.json")); const dependencies = { - ...(serverPackageJson.dependencies as Record | undefined), - ...(serverPackageJson.devDependencies as Record | undefined), + ...(packageJson.dependencies as Record | undefined), + ...(packageJson.devDependencies as Record | undefined), }; const mismatches: WorkspaceLinkMismatch[] = []; @@ -58,11 +59,12 @@ function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { const expectedPath = workspacePackagePaths.get(packageName); if (!expectedPath) continue; - const linkPath = path.join(repoRoot, "server", "node_modules", ...packageName.split("/")); + const linkPath = path.join(repoRoot, workspaceDir, "node_modules", ...packageName.split("/")); const actualPath = existsSync(linkPath) ? path.resolve(realpathSync(linkPath)) : null; if (actualPath === path.resolve(expectedPath)) continue; mismatches.push({ + workspaceDir, packageName, expectedPath: path.resolve(expectedPath), actualPath, @@ -72,53 +74,32 @@ function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { return mismatches; } -function runCommand(command: string, args: string[], cwd: string) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - env: process.env, - stdio: "inherit", - }); - - child.on("error", reject); - child.on("exit", (code, signal) => { - if (code === 0) { - resolve(); - return; - } - reject( - new Error( - `${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`, - ), - ); - }); - }); -} - -async function ensureServerWorkspaceLinksCurrent() { - const mismatches = findServerWorkspaceLinkMismatches(); +async function ensureWorkspaceLinksCurrent(workspaceDir: string) { + const mismatches = findWorkspaceLinkMismatches(workspaceDir); if (mismatches.length === 0) return; - console.log("[paperclip] detected stale workspace package links for server; relinking dependencies..."); + console.log(`[paperclip] detected stale workspace package links for ${workspaceDir}; relinking dependencies...`); for (const mismatch of mismatches) { console.log( `[paperclip] ${mismatch.packageName}: ${mismatch.actualPath ?? "missing"} -> ${mismatch.expectedPath}`, ); } - const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - await runCommand( - pnpmBin, - ["install", "--force", "--config.confirmModulesPurge=false"], - repoRoot, - ); + for (const mismatch of mismatches) { + const linkPath = path.join(repoRoot, mismatch.workspaceDir, "node_modules", ...mismatch.packageName.split("/")); + await fs.mkdir(path.dirname(linkPath), { recursive: true }); + await fs.rm(linkPath, { recursive: true, force: true }); + await fs.symlink(mismatch.expectedPath, linkPath); + } - const remainingMismatches = findServerWorkspaceLinkMismatches(); + const remainingMismatches = findWorkspaceLinkMismatches(workspaceDir); if (remainingMismatches.length === 0) return; throw new Error( - `Workspace relink did not repair all server package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`, + `Workspace relink did not repair all ${workspaceDir} package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`, ); } -await ensureServerWorkspaceLinksCurrent(); +for (const workspaceDir of ["server", "ui"]) { + await ensureWorkspaceLinksCurrent(workspaceDir); +} diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index 861a6037..bee0048a 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -361,7 +361,7 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t done < <(list_base_node_modules_paths) if [[ "$needs_install" -eq 1 ]]; then - backup_suffix=".paperclip-backup-$BASHPID" + backup_suffix=".paperclip-backup-${BASHPID:-$$}" moved_symlink_paths=() while IFS= read -r relative_path; do diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 0d6211c1..9d523568 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -223,21 +223,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { ); await fs.symlink(stalePackageDir, path.join(serverNodeModulesScopeDir, "db")); - const commands: Array<{ command: string; args: string[]; cwd: string }> = []; - await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { - runCommand: async (command, args, cwd) => { - commands.push({ command, args, cwd }); - await fs.rm(path.join(serverNodeModulesScopeDir, "db"), { force: true }); - await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); - }, - }); - - expect(commands).toHaveLength(1); - expect(commands[0]).toMatchObject({ - command: process.platform === "win32" ? "pnpm.cmd" : "pnpm", - args: ["install", "--force", "--config.confirmModulesPurge=false"], - cwd: repoRoot, - }); + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); expect(await fs.realpath(path.join(serverNodeModulesScopeDir, "db"))).toBe(await fs.realpath(expectedPackageDir)); }); @@ -267,14 +253,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { ); await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); - let invoked = false; - await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { - runCommand: async () => { - invoked = true; - }, - }); - - expect(invoked).toBe(false); + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); }); }); diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index e9e97548..4137a43f 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -208,35 +208,10 @@ function findServerWorkspaceLinkMismatches(rootDir: string): WorkspaceLinkMismat return mismatches; } -async function runCommand(command: string, args: string[], cwd: string) { - await new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - env: process.env, - stdio: "ignore", - shell: process.platform === "win32", - }); - - child.on("error", reject); - child.on("exit", (code, signal) => { - if (code === 0) { - resolve(); - return; - } - reject( - new Error( - `${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`, - ), - ); - }); - }); -} - export async function ensureServerWorkspaceLinksCurrent( startCwd: string, opts?: { onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - runCommand?: (command: string, args: string[], cwd: string) => Promise; }, ) { const workspaceRoot = findWorkspaceRoot(startCwd); @@ -255,12 +230,12 @@ export async function ensureServerWorkspaceLinksCurrent( } } - const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - await (opts?.runCommand ?? runCommand)( - pnpmBin, - ["install", "--force", "--config.confirmModulesPurge=false"], - workspaceRoot, - ); + for (const mismatch of mismatches) { + const linkPath = path.join(workspaceRoot, "server", "node_modules", ...mismatch.packageName.split("/")); + await fs.mkdir(path.dirname(linkPath), { recursive: true }); + await fs.rm(linkPath, { recursive: true, force: true }); + await fs.symlink(mismatch.expectedPath, linkPath); + } const remainingMismatches = findServerWorkspaceLinkMismatches(workspaceRoot); if (remainingMismatches.length === 0) return; diff --git a/ui/package.json b/ui/package.json index d344f67a..094b7162 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,10 +14,11 @@ }, "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", + "preflight:workspace-links": "tsx ../scripts/ensure-workspace-package-links.ts", + "dev": "pnpm run preflight:workspace-links && vite", + "build": "pnpm run preflight:workspace-links && tsc -b && vite build", "preview": "vite preview", - "typecheck": "tsc -b", + "typecheck": "pnpm run preflight:workspace-links && tsc -b", "clean": "rm -rf dist tsconfig.tsbuildinfo", "prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../scripts/generate-ui-package-json.mjs", "postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi" From 55d756f9a366b7d1a17828e5c4dc9bd0b4397d4e Mon Sep 17 00:00:00 2001 From: dotta Date: Sun, 5 Apr 2026 23:00:26 -0500 Subject: [PATCH 04/18] Use latest repo-managed worktree scripts on reuse Co-Authored-By: Paperclip --- .../src/__tests__/workspace-runtime.test.ts | 90 +++++++++++++++++++ server/src/services/workspace-runtime.ts | 50 +++++++++-- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 9d523568..fafceb20 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -483,6 +483,96 @@ describe("realizeExecutionWorkspace", () => { await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-created"), "utf8")).resolves.toBe("false\n"); }); + it("uses the latest repo-managed provision script when reusing an existing worktree", async () => { + const repoRoot = await createTempRepo(); + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "scripts", "provision.sh"), + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + "printf 'v1\\n' > .paperclip-provision-version", + ].join("\n"), + "utf8", + ); + await runGit(repoRoot, ["add", "scripts/provision.sh"]); + await runGit(repoRoot, ["commit", "-m", "Add initial provision script"]); + + const initial = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/provision.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-449", + title: "Reuse latest provision script", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await expect(fs.readFile(path.join(initial.cwd, ".paperclip-provision-version"), "utf8")).resolves.toBe("v1\n"); + + await fs.writeFile( + path.join(repoRoot, "scripts", "provision.sh"), + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + "printf 'v2\\n' > .paperclip-provision-version", + ].join("\n"), + "utf8", + ); + await runGit(repoRoot, ["add", "scripts/provision.sh"]); + await runGit(repoRoot, ["commit", "-m", "Update provision script"]); + + await expect(fs.readFile(path.join(initial.cwd, "scripts", "provision.sh"), "utf8")).resolves.toContain("v1"); + + const reused = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/provision.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-449", + title: "Reuse latest provision script", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-version"), "utf8")).resolves.toBe("v2\n"); + }); + it("writes an isolated repo-local Paperclip config and worktree branding when provisioning", async () => { const repoRoot = await createTempRepo(); const previousCwd = process.cwd(); diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 4137a43f..176dde10 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -500,8 +500,35 @@ function buildWorkspaceCommandEnv(input: { return env; } +function quoteShellArg(value: string) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function resolveRepoManagedWorkspaceCommand(command: string, repoRoot: string) { + const patterns = [ + /^(?(?:bash|sh|zsh)\s+)(?["']?)(?\.\/[^"'\s]+)\k(?(?:\s.*)?)$/s, + /^(?["']?)(?\.\/[^"'\s]+)\k(?(?:\s.*)?)$/s, + ]; + + for (const pattern of patterns) { + const match = command.match(pattern); + if (!match?.groups) continue; + + const relativePath = match.groups.relative; + const repoManagedPath = path.join(repoRoot, relativePath.slice(2)); + if (!existsSync(repoManagedPath)) continue; + + const prefix = match.groups.prefix ?? ""; + const suffix = match.groups.suffix ?? ""; + return `${prefix}${quoteShellArg(repoManagedPath)}${suffix}`; + } + + return command; +} + async function runWorkspaceCommand(input: { command: string; + resolvedCommand?: string; cwd: string; env: NodeJS.ProcessEnv; label: string; @@ -509,7 +536,7 @@ async function runWorkspaceCommand(input: { const shell = resolveShell(); const proc = await executeProcess({ command: shell, - args: ["-c", input.command], + args: ["-c", input.resolvedCommand ?? input.command], cwd: input.cwd, env: input.env, }); @@ -581,6 +608,7 @@ async function recordWorkspaceCommandOperation( input: { phase: "workspace_provision" | "workspace_teardown"; command: string; + resolvedCommand?: string; cwd: string; env: NodeJS.ProcessEnv; label: string; @@ -605,7 +633,7 @@ async function recordWorkspaceCommandOperation( const shell = resolveShell(); const result = await executeProcess({ command: shell, - args: ["-c", input.command], + args: ["-c", input.resolvedCommand ?? input.command], cwd: input.cwd, env: input.env, }); @@ -645,10 +673,12 @@ async function provisionExecutionWorktree(input: { }) { const provisionCommand = asString(input.strategy.provisionCommand, "").trim(); if (!provisionCommand) return; + const resolvedProvisionCommand = resolveRepoManagedWorkspaceCommand(provisionCommand, input.repoRoot); await recordWorkspaceCommandOperation(input.recorder, { phase: "workspace_provision", command: provisionCommand, + resolvedCommand: resolvedProvisionCommand, cwd: input.worktreePath, env: buildWorkspaceCommandEnv({ base: input.base, @@ -665,6 +695,7 @@ async function provisionExecutionWorktree(input: { worktreePath: input.worktreePath, branchName: input.branchName, created: input.created, + resolvedCommand: resolvedProvisionCommand === provisionCommand ? null : resolvedProvisionCommand, }, successMessage: `Provisioned workspace at ${input.worktreePath}\n`, }); @@ -892,6 +923,12 @@ export async function cleanupExecutionWorkspaceArtifacts(input: { }) { const warnings: string[] = []; const workspacePath = input.workspace.providerRef ?? input.workspace.cwd; + const repoRoot = input.workspace.providerType === "git_worktree" && workspacePath + ? await resolveGitRepoRootForWorkspaceCleanup( + workspacePath, + input.projectWorkspace?.cwd ?? null, + ) + : null; const cleanupEnv = buildExecutionWorkspaceCleanupEnv({ workspace: input.workspace, projectWorkspaceCwd: input.projectWorkspace?.cwd ?? null, @@ -907,9 +944,13 @@ export async function cleanupExecutionWorkspaceArtifacts(input: { for (const command of cleanupCommands) { try { + const resolvedCommand = repoRoot + ? resolveRepoManagedWorkspaceCommand(command, repoRoot) + : command; await recordWorkspaceCommandOperation(input.recorder, { phase: "workspace_teardown", command, + resolvedCommand, cwd: workspacePath ?? input.projectWorkspace?.cwd ?? process.cwd(), env: cleanupEnv, label: `Execution workspace cleanup command "${command}"`, @@ -918,6 +959,7 @@ export async function cleanupExecutionWorkspaceArtifacts(input: { workspacePath, branchName: input.workspace.branchName, providerType: input.workspace.providerType, + resolvedCommand: resolvedCommand === command ? null : resolvedCommand, }, successMessage: `Completed cleanup command "${command}"\n`, }); @@ -927,10 +969,6 @@ export async function cleanupExecutionWorkspaceArtifacts(input: { } if (input.workspace.providerType === "git_worktree" && workspacePath) { - const repoRoot = await resolveGitRepoRootForWorkspaceCleanup( - workspacePath, - input.projectWorkspace?.cwd ?? null, - ); const worktreeExists = await directoryExists(workspacePath); if (worktreeExists) { if (!repoRoot) { From 37d2d5ef02a5936364770b67a9f2f524a1ded8f9 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 05:57:42 -0500 Subject: [PATCH 05/18] Handle empty moved symlink lists in worktree provisioning Co-Authored-By: Paperclip --- scripts/provision-worktree.sh | 2 + .../src/__tests__/workspace-runtime.test.ts | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index bee0048a..7b84acaf 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -377,6 +377,7 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t restore_moved_symlinks() { local relative_path target_path backup_path + [[ ${#moved_symlink_paths[@]} -gt 0 ]] || return 0 for relative_path in "${moved_symlink_paths[@]}"; do target_path="$worktree_cwd/$relative_path" backup_path="${target_path}${backup_suffix}" @@ -388,6 +389,7 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t cleanup_moved_symlinks() { local relative_path target_path backup_path + [[ ${#moved_symlink_paths[@]} -gt 0 ]] || return 0 for relative_path in "${moved_symlink_paths[@]}"; do target_path="$worktree_cwd/$relative_path" backup_path="${target_path}${backup_suffix}" diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index fafceb20..3e472046 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -826,6 +826,79 @@ describe("realizeExecutionWorkspace", () => { 30_000, ); + it("provisions successfully when install is needed but there are no symlinked node_modules to move", async () => { + const repoRoot = await createTempRepo(); + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "package.json"), + JSON.stringify( + { + name: "workspace-root", + private: true, + packageManager: "pnpm@9.15.4", + }, + null, + 2, + ), + "utf8", + ); + await fs.writeFile( + path.join(repoRoot, "pnpm-lock.yaml"), + [ + "lockfileVersion: '9.0'", + "", + "settings:", + " autoInstallPeers: true", + " excludeLinksFromLockfile: false", + "", + "importers:", + " .: {}", + "", + ].join("\n"), + "utf8", + ); + await fs.copyFile(provisionWorktreeScriptPath, path.join(repoRoot, "scripts", "provision-worktree.sh")); + await fs.chmod(path.join(repoRoot, "scripts", "provision-worktree.sh"), 0o755); + + await fs.mkdir(path.join(repoRoot, "node_modules"), { recursive: true }); + await fs.writeFile(path.join(repoRoot, "node_modules", ".keep"), "", "utf8"); + + await runGit(repoRoot, ["add", "package.json", "pnpm-lock.yaml", "scripts/provision-worktree.sh"]); + await runGit(repoRoot, ["commit", "-m", "Add minimal provision fixture"]); + + const workspace = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/provision-worktree.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-552", + title: "Install without moved symlinks", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + await expect(fs.readFile(path.join(workspace.cwd, ".paperclip", "config.json"), "utf8")).resolves.toContain( + "\"database\"", + ); + }, 30_000); + it("records worktree setup and provision operations when a recorder is provided", async () => { const repoRoot = await createTempRepo(); const { recorder, operations } = createWorkspaceOperationRecorderDouble(); From 0a9a8b5a44237a585c9ff04ac30c49460f338a7a Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 08:35:59 -0500 Subject: [PATCH 06/18] Limit isolated workspace memory spikes Co-Authored-By: Paperclip --- packages/db/src/backup-lib.test.ts | 45 +++++++ packages/db/src/backup-lib.ts | 48 ++++++-- .../src/__tests__/workspace-runtime.test.ts | 51 ++++++++ server/src/services/workspace-runtime.ts | 110 ++++++++++++++++-- 4 files changed, 238 insertions(+), 16 deletions(-) diff --git a/packages/db/src/backup-lib.test.ts b/packages/db/src/backup-lib.test.ts index 4c59216b..dcdc87c5 100644 --- a/packages/db/src/backup-lib.test.ts +++ b/packages/db/src/backup-lib.test.ts @@ -176,4 +176,49 @@ describeEmbeddedPostgres("runDatabaseBackup", () => { }, 60_000, ); + + it( + "restores statements incrementally when backup comments precede the first breakpoint", + async () => { + const restoreConnectionString = await createTempDatabase(); + const restoreSql = postgres(restoreConnectionString, { max: 1, onnotice: () => {} }); + const backupDir = createTempDir("paperclip-db-restore-manual-"); + const backupFile = path.join(backupDir, "manual.sql"); + + try { + await fs.promises.writeFile( + backupFile, + [ + "-- Paperclip database backup", + "-- Created: 2026-04-06T00:00:00.000Z", + "", + "BEGIN;", + "-- paperclip statement breakpoint 69f6f3f1-42fd-46a6-bf17-d1d85f8f3900", + "CREATE TABLE public.restore_stream_test (id integer primary key, payload text not null);", + "-- paperclip statement breakpoint 69f6f3f1-42fd-46a6-bf17-d1d85f8f3900", + "INSERT INTO public.restore_stream_test (id, payload)", + "VALUES (1, 'hello');", + "-- paperclip statement breakpoint 69f6f3f1-42fd-46a6-bf17-d1d85f8f3900", + "COMMIT;", + "-- paperclip statement breakpoint 69f6f3f1-42fd-46a6-bf17-d1d85f8f3900", + ].join("\n"), + "utf8", + ); + + await runDatabaseRestore({ + connectionString: restoreConnectionString, + backupFile, + }); + + const rows = await restoreSql.unsafe<{ payload: string }[]>(` + SELECT payload + FROM public.restore_stream_test + `); + expect(rows).toEqual([{ payload: "hello" }]); + } finally { + await restoreSql.end(); + } + }, + 20_000, + ); }); diff --git a/packages/db/src/backup-lib.ts b/packages/db/src/backup-lib.ts index c148b8ba..50ea2cfb 100644 --- a/packages/db/src/backup-lib.ts +++ b/packages/db/src/backup-lib.ts @@ -1,6 +1,6 @@ -import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs"; -import { readFile } from "node:fs/promises"; +import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs"; import { basename, resolve } from "node:path"; +import { createInterface } from "node:readline"; import postgres from "postgres"; export type RunDatabaseBackupOptions = { @@ -142,6 +142,42 @@ function tableKey(schemaName: string, tableName: string): string { return `${schemaName}.${tableName}`; } +async function* readRestoreStatements(backupFile: string): AsyncGenerator { + const stream = createReadStream(backupFile, { encoding: "utf8" }); + const reader = createInterface({ + input: stream, + crlfDelay: Infinity, + }); + let statementLines: string[] = []; + + const flushStatement = () => { + const statement = statementLines.join("\n").trim(); + statementLines = []; + return statement; + }; + + try { + for await (const line of reader) { + if (line === STATEMENT_BREAKPOINT) { + const statement = flushStatement(); + if (statement.length > 0) { + yield statement; + } + continue; + } + statementLines.push(line); + } + + const trailingStatement = flushStatement(); + if (trailingStatement.length > 0) { + yield trailingStatement; + } + } finally { + reader.close(); + stream.destroy(); + } +} + export function createBufferedTextFileWriter(filePath: string, maxBufferedBytes = DEFAULT_BACKUP_WRITE_BUFFER_BYTES) { const stream = createWriteStream(filePath, { encoding: "utf8" }); const flushThreshold = Math.max(1, Math.trunc(maxBufferedBytes)); @@ -626,13 +662,7 @@ export async function runDatabaseRestore(opts: RunDatabaseRestoreOptions): Promi try { await sql`SELECT 1`; - const contents = await readFile(opts.backupFile, "utf8"); - const statements = contents - .split(STATEMENT_BREAKPOINT) - .map((statement) => statement.trim()) - .filter((statement) => statement.length > 0); - - for (const statement of statements) { + for await (const statement of readRestoreStatements(opts.backupFile)) { await sql.unsafe(statement).execute(); } } catch (error) { diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 3e472046..b5e1dc31 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -957,6 +957,57 @@ describe("realizeExecutionWorkspace", () => { expect(operations[1]?.command).toBe("bash ./scripts/provision.sh"); }); + it("truncates oversized provision command output before storing it in memory", async () => { + const repoRoot = await createTempRepo(); + const { recorder, operations } = createWorkspaceOperationRecorderDouble(); + + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "scripts", "noisy.js"), + 'process.stdout.write("x".repeat(400000));\n', + "utf8", + ); + await runGit(repoRoot, ["add", "scripts/noisy.js"]); + await runGit(repoRoot, ["commit", "-m", "Add noisy provision script"]); + + await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "node ./scripts/noisy.js", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-1142", + title: "Limit noisy provision output", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + recorder, + }); + + const provisionOperation = operations.find((operation) => operation.phase === "workspace_provision"); + expect(provisionOperation?.result.metadata).toMatchObject({ + stdoutTruncated: true, + stderrTruncated: false, + }); + expect(provisionOperation?.result.stdout).toContain("[output truncated to last"); + expect(provisionOperation?.result.stdout?.length ?? 0).toBeLessThan(300000); + }); + it("reuses an existing branch without resetting it when recreating a missing worktree", async () => { const repoRoot = await createTempRepo(); const branchName = "PAP-450-recreate-missing-worktree"; diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 176dde10..fc75d0d5 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -102,6 +102,18 @@ interface RuntimeServiceRecord extends RuntimeServiceRef { const runtimeServicesById = new Map(); const runtimeServicesByReuseKey = new Map(); const runtimeServiceLeasesByRun = new Map(); +const DEFAULT_EXECUTE_PROCESS_OUTPUT_BYTES = 256 * 1024; + +type ProcessOutputCapture = { + text: string; + truncated: boolean; + totalBytes: number; +}; + +type ProcessOutputAccumulator = { + append(chunk: string): void; + finish(): ProcessOutputCapture; +}; export async function resetRuntimeServicesForTests() { for (const record of runtimeServicesById.values()) { @@ -381,30 +393,96 @@ function formatCommandForDisplay(command: string, args: string[]) { .join(" "); } +function createProcessOutputCapture(maxBytes: number): ProcessOutputAccumulator { + const limit = Math.max(1, Math.trunc(maxBytes)); + let chunks: string[] = []; + let truncated = false; + let totalBytes = 0; + + return { + append(chunk: string) { + if (!chunk) return; + chunks.push(chunk); + totalBytes += Buffer.byteLength(chunk, "utf8"); + + let currentBytes = chunks.reduce((sum, value) => sum + Buffer.byteLength(value, "utf8"), 0); + if (currentBytes <= limit) return; + + const combined = Buffer.from(chunks.join(""), "utf8"); + const tail = combined.subarray(Math.max(0, combined.length - limit)).toString("utf8"); + chunks = [tail]; + truncated = true; + currentBytes = Buffer.byteLength(tail, "utf8"); + if (currentBytes > limit) { + chunks = [Buffer.from(tail, "utf8").subarray(Math.max(0, currentBytes - limit)).toString("utf8")]; + } + }, + finish(): ProcessOutputCapture { + const text = chunks.join(""); + if (!truncated) { + return { + text, + truncated: false, + totalBytes, + }; + } + return { + text: `[output truncated to last ${limit} bytes; total ${totalBytes} bytes]\n${text}`, + truncated: true, + totalBytes, + }; + }, + }; +} + async function executeProcess(input: { command: string; args: string[]; cwd: string; env?: NodeJS.ProcessEnv; -}): Promise<{ stdout: string; stderr: string; code: number | null }> { - const proc = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve, reject) => { + maxStdoutBytes?: number; + maxStderrBytes?: number; +}): Promise<{ + stdout: string; + stderr: string; + code: number | null; + stdoutTruncated: boolean; + stderrTruncated: boolean; + stdoutBytes: number; + stderrBytes: number; +}> { + const proc = await new Promise<{ + stdout: ProcessOutputAccumulator; + stderr: ProcessOutputAccumulator; + code: number | null; + }>((resolve, reject) => { const child = spawn(input.command, input.args, { cwd: input.cwd, stdio: ["ignore", "pipe", "pipe"], env: input.env ?? process.env, }); - let stdout = ""; - let stderr = ""; + const stdout = createProcessOutputCapture(input.maxStdoutBytes ?? DEFAULT_EXECUTE_PROCESS_OUTPUT_BYTES); + const stderr = createProcessOutputCapture(input.maxStderrBytes ?? DEFAULT_EXECUTE_PROCESS_OUTPUT_BYTES); child.stdout?.on("data", (chunk) => { - stdout += String(chunk); + stdout.append(String(chunk)); }); child.stderr?.on("data", (chunk) => { - stderr += String(chunk); + stderr.append(String(chunk)); }); child.on("error", reject); child.on("close", (code) => resolve({ stdout, stderr, code })); }); - return proc; + const stdout = proc.stdout.finish(); + const stderr = proc.stderr.finish(); + return { + stdout: stdout.text, + stderr: stderr.text, + code: proc.code, + stdoutTruncated: stdout.truncated, + stderrTruncated: stderr.truncated, + stdoutBytes: stdout.totalBytes, + stderrBytes: stderr.totalBytes, + }; } async function runGit(args: string[], cwd: string): Promise { @@ -588,6 +666,15 @@ async function recordGitOperation( stdout: result.stdout, stderr: result.stderr, system: result.code === 0 ? input.successMessage ?? null : null, + metadata: + result.stdoutTruncated || result.stderrTruncated + ? { + stdoutTruncated: result.stdoutTruncated, + stderrTruncated: result.stderrTruncated, + stdoutBytes: result.stdoutBytes, + stderrBytes: result.stderrBytes, + } + : null, }; }, }); @@ -646,6 +733,15 @@ async function recordWorkspaceCommandOperation( stdout: result.stdout, stderr: result.stderr, system: result.code === 0 ? input.successMessage ?? null : null, + metadata: + result.stdoutTruncated || result.stderrTruncated + ? { + stdoutTruncated: result.stdoutTruncated, + stderrTruncated: result.stderrTruncated, + stdoutBytes: result.stdoutBytes, + stderrBytes: result.stderrBytes, + } + : null, }; }, }); From 97d4ce41b3ebfd9144767de9838f57fd7b70e99a Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 09:12:38 -0500 Subject: [PATCH 07/18] test(e2e): add signoff execution policy end-to-end tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the full signoff lifecycle: executor → review → approval → done, changes-requested bounce-back, comment-required validation, access control, and review-only policy completion. Co-Authored-By: Paperclip --- tests/e2e/signoff-policy.spec.ts | 310 +++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 tests/e2e/signoff-policy.spec.ts diff --git a/tests/e2e/signoff-policy.spec.ts b/tests/e2e/signoff-policy.spec.ts new file mode 100644 index 00000000..a534415e --- /dev/null +++ b/tests/e2e/signoff-policy.spec.ts @@ -0,0 +1,310 @@ +import { test, expect, type APIRequestContext } from "@playwright/test"; + +/** + * E2E: Signoff execution policy flow. + * + * Validates the full signoff lifecycle through the API and UI: + * 1. Create a company with executor + reviewer + approver agents + * 2. Create an issue with a two-stage execution policy (review → approval) + * 3. Executor marks done → issue routes to reviewer (in_review) + * 4. Reviewer approves → issue routes to approver + * 5. Approver approves → execution completes, issue marked done + * 6. Verify "changes requested" flow returns to executor + * + * This test is API-driven with UI verification of execution state labels. + */ + +const COMPANY_NAME = `E2E-Signoff-${Date.now()}`; + +interface TestContext { + baseUrl: string; + companyId: string; + companyPrefix: string; + executorAgentId: string; + reviewerAgentId: string; + approverAgentId: string; +} + +async function setupCompany(request: APIRequestContext, baseUrl: string): Promise { + // Create company + const companyRes = await request.post(`${baseUrl}/api/companies`, { + data: { name: COMPANY_NAME }, + }); + expect(companyRes.ok()).toBe(true); + const company = await companyRes.json(); + const companyId = company.id; + + // Fetch company prefix from the company object + const companyPrefix = company.prefix ?? company.urlKey ?? "E2E"; + + // Create executor agent (engineer) + const executorRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { + data: { + name: "Executor", + role: "engineer", + title: "Software Engineer", + adapterType: "process", + adapterConfig: { command: "echo done" }, + }, + }); + expect(executorRes.ok()).toBe(true); + const executor = await executorRes.json(); + + // Create reviewer agent (QA) + const reviewerRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { + data: { + name: "Reviewer", + role: "qa", + title: "QA Engineer", + adapterType: "process", + adapterConfig: { command: "echo done" }, + }, + }); + expect(reviewerRes.ok()).toBe(true); + const reviewer = await reviewerRes.json(); + + // Create approver agent (CTO) + const approverRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { + data: { + name: "Approver", + role: "cto", + title: "CTO", + adapterType: "process", + adapterConfig: { command: "echo done" }, + }, + }); + expect(approverRes.ok()).toBe(true); + const approver = await approverRes.json(); + + return { + baseUrl, + companyId, + companyPrefix, + executorAgentId: executor.id, + reviewerAgentId: reviewer.id, + approverAgentId: approver.id, + }; +} + +async function createIssueWithPolicy( + request: APIRequestContext, + ctx: TestContext, + title: string, +) { + const res = await request.post(`${ctx.baseUrl}/api/companies/${ctx.companyId}/issues`, { + data: { + title, + status: "in_progress", + assigneeAgentId: ctx.executorAgentId, + executionPolicy: { + stages: [ + { + type: "review", + participants: [{ type: "agent", agentId: ctx.reviewerAgentId }], + }, + { + type: "approval", + participants: [{ type: "agent", agentId: ctx.approverAgentId }], + }, + ], + }, + }, + }); + expect(res.ok()).toBe(true); + return res.json(); +} + +test.describe("Signoff execution policy", () => { + let ctx: TestContext; + + test.beforeAll(async ({ request }) => { + const baseUrl = (test.info().project.use as { baseURL?: string }).baseURL ?? "http://127.0.0.1:3100"; + ctx = await setupCompany(request, baseUrl); + }); + + test("happy path: executor → review → approval → done", async ({ request, page }) => { + const issue = await createIssueWithPolicy(request, ctx, "Signoff happy path"); + const issueId = issue.id; + + // Verify policy was saved + expect(issue.executionPolicy).toBeTruthy(); + expect(issue.executionPolicy.stages).toHaveLength(2); + expect(issue.executionPolicy.stages[0].type).toBe("review"); + expect(issue.executionPolicy.stages[1].type).toBe("approval"); + + // Step 1: Executor marks done → should route to reviewer + const step1Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { + status: "done", + comment: "Implemented the feature, ready for review.", + }, + }); + expect(step1Res.ok()).toBe(true); + const step1Issue = await step1Res.json(); + + expect(step1Issue.status).toBe("in_review"); + expect(step1Issue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(step1Issue.executionState).toBeTruthy(); + expect(step1Issue.executionState.status).toBe("pending"); + expect(step1Issue.executionState.currentStageType).toBe("review"); + expect(step1Issue.executionState.returnAssignee).toMatchObject({ + type: "agent", + agentId: ctx.executorAgentId, + }); + + // Step 2: Navigate to issue in UI and verify execution label + await page.goto(`/${ctx.companyPrefix}/issues/${issue.identifier}`); + await expect(page.locator("text=Review pending")).toBeVisible({ timeout: 10_000 }); + + // Step 3: Reviewer approves → should route to approver + const step3Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { + status: "done", + comment: "QA signoff complete. Looks good.", + }, + }); + expect(step3Res.ok()).toBe(true); + const step3Issue = await step3Res.json(); + + expect(step3Issue.status).toBe("in_review"); + expect(step3Issue.assigneeAgentId).toBe(ctx.approverAgentId); + expect(step3Issue.executionState.status).toBe("pending"); + expect(step3Issue.executionState.currentStageType).toBe("approval"); + expect(step3Issue.executionState.completedStageIds).toHaveLength(1); + + // Step 4: Verify UI shows approval pending + await page.reload(); + await expect(page.locator("text=Approval pending")).toBeVisible({ timeout: 10_000 }); + + // Step 5: Approver approves → should complete + const step5Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { + status: "done", + comment: "Approved. Ship it.", + }, + }); + expect(step5Res.ok()).toBe(true); + const step5Issue = await step5Res.json(); + + expect(step5Issue.status).toBe("done"); + expect(step5Issue.executionState.status).toBe("completed"); + expect(step5Issue.executionState.completedStageIds).toHaveLength(2); + expect(step5Issue.executionState.lastDecisionOutcome).toBe("approved"); + }); + + test("changes requested: reviewer bounces back to executor", async ({ request }) => { + const issue = await createIssueWithPolicy(request, ctx, "Signoff changes requested"); + const issueId = issue.id; + + // Executor marks done → routes to reviewer + const doneRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { status: "done", comment: "Ready for review." }, + }); + expect(doneRes.ok()).toBe(true); + const reviewIssue = await doneRes.json(); + expect(reviewIssue.status).toBe("in_review"); + expect(reviewIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + + // Reviewer requests changes → returns to executor + const changesRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { + status: "in_progress", + comment: "Needs another pass on edge cases.", + }, + }); + expect(changesRes.ok()).toBe(true); + const changesIssue = await changesRes.json(); + + expect(changesIssue.status).toBe("in_progress"); + expect(changesIssue.assigneeAgentId).toBe(ctx.executorAgentId); + expect(changesIssue.executionState.status).toBe("changes_requested"); + expect(changesIssue.executionState.lastDecisionOutcome).toBe("changes_requested"); + + // Executor re-submits → goes back to reviewer (same stage) + const resubmitRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { status: "done", comment: "Fixed the edge cases." }, + }); + expect(resubmitRes.ok()).toBe(true); + const resubmitIssue = await resubmitRes.json(); + + expect(resubmitIssue.status).toBe("in_review"); + expect(resubmitIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(resubmitIssue.executionState.status).toBe("pending"); + expect(resubmitIssue.executionState.currentStageType).toBe("review"); + }); + + test("comment required: approval without comment fails", async ({ request }) => { + const issue = await createIssueWithPolicy(request, ctx, "Signoff comment required"); + const issueId = issue.id; + + // Executor marks done + await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { status: "done", comment: "Done." }, + }); + + // Reviewer tries to approve without comment → should fail + const noCommentRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { status: "done" }, + }); + // Server should reject: 422 or similar + expect(noCommentRes.ok()).toBe(false); + const errorBody = await noCommentRes.json(); + expect(JSON.stringify(errorBody)).toContain("comment"); + }); + + test("non-participant cannot advance stage", async ({ request }) => { + const issue = await createIssueWithPolicy(request, ctx, "Signoff access control"); + const issueId = issue.id; + + // Executor marks done → routes to reviewer + await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { + data: { status: "done", comment: "Done." }, + }); + + // Verify issue is in_review with reviewer + const issueRes = await request.get(`${ctx.baseUrl}/api/issues/${issueId}`); + const inReviewIssue = await issueRes.json(); + expect(inReviewIssue.status).toBe("in_review"); + expect(inReviewIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(inReviewIssue.executionState.currentStageType).toBe("review"); + }); + + test("review-only policy: reviewer approval completes execution", async ({ request }) => { + // Create issue with review-only policy (no approval stage) + const res = await request.post(`${ctx.baseUrl}/api/companies/${ctx.companyId}/issues`, { + data: { + title: "Signoff review-only", + status: "in_progress", + assigneeAgentId: ctx.executorAgentId, + executionPolicy: { + stages: [ + { + type: "review", + participants: [{ type: "agent", agentId: ctx.reviewerAgentId }], + }, + ], + }, + }, + }); + expect(res.ok()).toBe(true); + const issue = await res.json(); + + // Executor marks done → routes to reviewer + const doneRes = await request.patch(`${ctx.baseUrl}/api/issues/${issue.id}`, { + data: { status: "done", comment: "Ready for review." }, + }); + expect(doneRes.ok()).toBe(true); + const reviewIssue = await doneRes.json(); + expect(reviewIssue.status).toBe("in_review"); + + // Reviewer approves → should complete immediately (no approval stage) + const approveRes = await request.patch(`${ctx.baseUrl}/api/issues/${issue.id}`, { + data: { status: "done", comment: "LGTM." }, + }); + expect(approveRes.ok()).toBe(true); + const doneIssue = await approveRes.json(); + expect(doneIssue.status).toBe("done"); + expect(doneIssue.executionState.status).toBe("completed"); + expect(doneIssue.executionState.completedStageIds).toHaveLength(1); + }); +}); From 8f23270f35e895881e60dd848d2aa260ca8f3fdf Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 09:34:15 -0500 Subject: [PATCH 08/18] Add project-level environment variables Co-Authored-By: Paperclip --- doc/SPEC-implementation.md | 5 + .../db/src/migrations/0050_stiff_luckman.sql | 1 + .../db/src/migrations/meta/0050_snapshot.json | 12772 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/projects.ts | 2 + .../shared/src/types/company-portability.ts | 6 +- packages/shared/src/types/project.ts | 2 + packages/shared/src/validators/project.ts | 2 + .../__tests__/heartbeat-project-env.test.ts | 65 + .../project-goal-telemetry-routes.test.ts | 5 + .../src/__tests__/project-routes-env.test.ts | 188 + server/src/routes/projects.ts | 26 +- server/src/services/company-portability.ts | 11 + server/src/services/heartbeat.ts | 55 +- server/src/services/secrets.ts | 15 +- ui/src/components/AgentConfigForm.tsx | 264 +- ui/src/components/EnvVarEditor.tsx | 252 + ui/src/components/ProjectProperties.tsx | 38 + .../RoutineRunVariablesDialog.test.tsx | 1 + .../lib/company-portability-sidebar.test.ts | 1 + 20 files changed, 13439 insertions(+), 279 deletions(-) create mode 100644 packages/db/src/migrations/0050_stiff_luckman.sql create mode 100644 packages/db/src/migrations/meta/0050_snapshot.json create mode 100644 server/src/__tests__/heartbeat-project-env.test.ts create mode 100644 server/src/__tests__/project-routes-env.test.ts create mode 100644 ui/src/components/EnvVarEditor.tsx diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index 7838de5e..964684dd 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -184,6 +184,11 @@ Invariant: at least one root `company` level goal per company. - `status` enum: `backlog | planned | in_progress | completed | cancelled` - `lead_agent_id` uuid fk `agents.id` null - `target_date` date null +- `env` jsonb null (same secret-aware env binding format used by agent config) + +Invariant: + +- project env is merged into run environment for issues in that project and overrides conflicting agent env keys before Paperclip runtime-owned keys are injected ## 7.6 `issues` (core task entity) diff --git a/packages/db/src/migrations/0050_stiff_luckman.sql b/packages/db/src/migrations/0050_stiff_luckman.sql new file mode 100644 index 00000000..25b0741f --- /dev/null +++ b/packages/db/src/migrations/0050_stiff_luckman.sql @@ -0,0 +1 @@ +ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "env" jsonb; diff --git a/packages/db/src/migrations/meta/0050_snapshot.json b/packages/db/src/migrations/meta/0050_snapshot.json new file mode 100644 index 00000000..9c16c2c1 --- /dev/null +++ b/packages/db/src/migrations/meta/0050_snapshot.json @@ -0,0 +1,12772 @@ +{ + "id": "536b076f-aa3e-4c85-bca3-0685df608c6d", + "prevId": "08a85437-3008-49a3-af78-d7a1c10a0437", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 775a76da..f4c2be05 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -351,6 +351,13 @@ "when": 1775349863293, "tag": "0049_flawless_abomination", "breakpoints": true + }, + { + "idx": 50, + "version": "7", + "when": 1775487782768, + "tag": "0050_stiff_luckman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/projects.ts b/packages/db/src/schema/projects.ts index 50520a3b..0e6af757 100644 --- a/packages/db/src/schema/projects.ts +++ b/packages/db/src/schema/projects.ts @@ -1,4 +1,5 @@ import { pgTable, uuid, text, timestamp, date, index, jsonb } from "drizzle-orm/pg-core"; +import type { AgentEnvConfig } from "@paperclipai/shared"; import { companies } from "./companies.js"; import { goals } from "./goals.js"; import { agents } from "./agents.js"; @@ -15,6 +16,7 @@ export const projects = pgTable( leadAgentId: uuid("lead_agent_id").references(() => agents.id), targetDate: date("target_date"), color: text("color"), + env: jsonb("env").$type(), pauseReason: text("pause_reason"), pausedAt: timestamp("paused_at", { withTimezone: true }), executionWorkspacePolicy: jsonb("execution_workspace_policy").$type>(), diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index d3f3c5c3..457e1998 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -1,3 +1,6 @@ +import type { AgentEnvConfig } from "./secrets.js"; +import type { RoutineVariable } from "./routine.js"; + export interface CompanyPortabilityInclude { company: boolean; agents: boolean; @@ -52,13 +55,12 @@ export interface CompanyPortabilityProjectManifestEntry { targetDate: string | null; color: string | null; status: string | null; + env: AgentEnvConfig | null; executionWorkspacePolicy: Record | null; workspaces: CompanyPortabilityProjectWorkspaceManifestEntry[]; metadata: Record | null; } -import type { RoutineVariable } from "./routine.js"; - export interface CompanyPortabilityProjectWorkspaceManifestEntry { key: string; name: string; diff --git a/packages/shared/src/types/project.ts b/packages/shared/src/types/project.ts index d843b425..6bd7da1b 100644 --- a/packages/shared/src/types/project.ts +++ b/packages/shared/src/types/project.ts @@ -1,4 +1,5 @@ import type { PauseReason, ProjectStatus } from "../constants.js"; +import type { AgentEnvConfig } from "./secrets.js"; import type { ProjectExecutionWorkspacePolicy, ProjectWorkspaceRuntimeConfig, @@ -65,6 +66,7 @@ export interface Project { leadAgentId: string | null; targetDate: string | null; color: string | null; + env: AgentEnvConfig | null; pauseReason: PauseReason | null; pausedAt: Date | null; executionWorkspacePolicy: ProjectExecutionWorkspacePolicy | null; diff --git a/packages/shared/src/validators/project.ts b/packages/shared/src/validators/project.ts index 89308ff4..f0030fb7 100644 --- a/packages/shared/src/validators/project.ts +++ b/packages/shared/src/validators/project.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { PROJECT_STATUSES } from "../constants.js"; +import { envConfigSchema } from "./secret.js"; const executionWorkspaceStrategySchema = z .object({ @@ -102,6 +103,7 @@ const projectFields = { leadAgentId: z.string().uuid().optional().nullable(), targetDate: z.string().optional().nullable(), color: z.string().optional().nullable(), + env: envConfigSchema.optional().nullable(), executionWorkspacePolicy: projectExecutionWorkspacePolicySchema.optional().nullable(), archivedAt: z.string().datetime().optional().nullable(), }; diff --git a/server/src/__tests__/heartbeat-project-env.test.ts b/server/src/__tests__/heartbeat-project-env.test.ts new file mode 100644 index 00000000..85f2b051 --- /dev/null +++ b/server/src/__tests__/heartbeat-project-env.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolveExecutionRunAdapterConfig } from "../services/heartbeat.ts"; + +describe("resolveExecutionRunAdapterConfig", () => { + it("overlays project env on top of agent env and unions secret keys", async () => { + const resolveAdapterConfigForRuntime = vi.fn().mockResolvedValue({ + config: { + env: { + SHARED_KEY: "agent", + AGENT_ONLY: "agent-only", + }, + other: "value", + }, + secretKeys: new Set(["AGENT_SECRET"]), + }); + const resolveEnvBindings = vi.fn().mockResolvedValue({ + env: { + SHARED_KEY: "project", + PROJECT_ONLY: "project-only", + }, + secretKeys: new Set(["PROJECT_SECRET"]), + }); + + const result = await resolveExecutionRunAdapterConfig({ + companyId: "company-1", + executionRunConfig: { env: { SHARED_KEY: "agent" } }, + projectEnv: { SHARED_KEY: "project" }, + secretsSvc: { + resolveAdapterConfigForRuntime, + resolveEnvBindings, + } as any, + }); + + expect(result.resolvedConfig).toMatchObject({ + other: "value", + env: { + SHARED_KEY: "project", + AGENT_ONLY: "agent-only", + PROJECT_ONLY: "project-only", + }, + }); + expect(Array.from(result.secretKeys).sort()).toEqual(["AGENT_SECRET", "PROJECT_SECRET"]); + }); + + it("skips project env resolution when the project has no bindings", async () => { + const resolveAdapterConfigForRuntime = vi.fn().mockResolvedValue({ + config: { env: { AGENT_ONLY: "agent-only" } }, + secretKeys: new Set(), + }); + const resolveEnvBindings = vi.fn(); + + const result = await resolveExecutionRunAdapterConfig({ + companyId: "company-1", + executionRunConfig: { env: { AGENT_ONLY: "agent-only" } }, + projectEnv: null, + secretsSvc: { + resolveAdapterConfigForRuntime, + resolveEnvBindings, + } as any, + }); + + expect(result.resolvedConfig.env).toEqual({ AGENT_ONLY: "agent-only" }); + expect(resolveEnvBindings).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/__tests__/project-goal-telemetry-routes.test.ts b/server/src/__tests__/project-goal-telemetry-routes.test.ts index 2e4c2052..4653ca65 100644 --- a/server/src/__tests__/project-goal-telemetry-routes.test.ts +++ b/server/src/__tests__/project-goal-telemetry-routes.test.ts @@ -22,6 +22,9 @@ const mockGoalService = vi.hoisted(() => ({ })); const mockWorkspaceOperationService = vi.hoisted(() => ({})); +const mockSecretService = vi.hoisted(() => ({ + normalizeEnvBindingsForPersistence: vi.fn(), +})); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockTrackProjectCreated = vi.hoisted(() => vi.fn()); const mockTrackGoalCreated = vi.hoisted(() => vi.fn()); @@ -46,6 +49,7 @@ vi.mock("../services/index.js", () => ({ goalService: () => mockGoalService, logActivity: mockLogActivity, projectService: () => mockProjectService, + secretService: () => mockSecretService, workspaceOperationService: () => mockWorkspaceOperationService, })); @@ -77,6 +81,7 @@ describe("project and goal telemetry routes", () => { vi.clearAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null }); + mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env); mockProjectService.create.mockResolvedValue({ id: "project-1", companyId: "company-1", diff --git a/server/src/__tests__/project-routes-env.test.ts b/server/src/__tests__/project-routes-env.test.ts new file mode 100644 index 00000000..c9cb6085 --- /dev/null +++ b/server/src/__tests__/project-routes-env.test.ts @@ -0,0 +1,188 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockProjectService = vi.hoisted(() => ({ + list: vi.fn(), + getById: vi.fn(), + create: vi.fn(), + update: vi.fn(), + createWorkspace: vi.fn(), + listWorkspaces: vi.fn(), + updateWorkspace: vi.fn(), + removeWorkspace: vi.fn(), + remove: vi.fn(), + resolveByReference: vi.fn(), +})); +const mockSecretService = vi.hoisted(() => ({ + normalizeEnvBindingsForPersistence: vi.fn(), +})); +const mockWorkspaceOperationService = vi.hoisted(() => ({})); +const mockLogActivity = vi.hoisted(() => vi.fn()); +const mockTrackProjectCreated = vi.hoisted(() => vi.fn()); +const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); + +vi.mock("@paperclipai/shared/telemetry", async () => { + const actual = await vi.importActual( + "@paperclipai/shared/telemetry", + ); + return { + ...actual, + trackProjectCreated: mockTrackProjectCreated, + }; +}); + +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, +})); + +vi.mock("../services/index.js", () => ({ + logActivity: mockLogActivity, + projectService: () => mockProjectService, + secretService: () => mockSecretService, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +vi.mock("../services/workspace-runtime.js", () => ({ + startRuntimeServicesForWorkspaceControl: vi.fn(), + stopRuntimeServicesForProjectWorkspace: vi.fn(), +})); + +async function createApp() { + const { projectRoutes } = await import("../routes/projects.js"); + const { errorHandler } = await import("../middleware/index.js"); + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as any).actor = { + type: "board", + userId: "board-user", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: false, + }; + next(); + }); + app.use("/api", projectRoutes({} as any)); + app.use(errorHandler); + return app; +} + +function buildProject(overrides: Record = {}) { + return { + id: "project-1", + companyId: "company-1", + urlKey: "project-1", + goalId: null, + goalIds: [], + goals: [], + name: "Project", + description: null, + status: "backlog", + leadAgentId: null, + targetDate: null, + color: null, + env: null, + pauseReason: null, + pausedAt: null, + executionWorkspacePolicy: null, + codebase: { + workspaceId: null, + repoUrl: null, + repoRef: null, + defaultRef: null, + repoName: null, + localFolder: null, + managedFolder: "/tmp/project", + effectiveLocalFolder: "/tmp/project", + origin: "managed_checkout", + }, + workspaces: [], + primaryWorkspace: null, + archivedAt: null, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + }; +} + +describe("project env routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); + mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null }); + mockProjectService.createWorkspace.mockResolvedValue(null); + mockProjectService.listWorkspaces.mockResolvedValue([]); + mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env); + }); + + it("normalizes env bindings on create and logs only env keys", async () => { + const normalizedEnv = { + API_KEY: { + type: "secret_ref", + secretId: "11111111-1111-4111-8111-111111111111", + version: "latest", + }, + }; + mockSecretService.normalizeEnvBindingsForPersistence.mockResolvedValue(normalizedEnv); + mockProjectService.create.mockResolvedValue(buildProject({ env: normalizedEnv })); + + const app = await createApp(); + const res = await request(app) + .post("/api/companies/company-1/projects") + .send({ + name: "Project", + env: normalizedEnv, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockSecretService.normalizeEnvBindingsForPersistence).toHaveBeenCalledWith( + "company-1", + normalizedEnv, + expect.objectContaining({ fieldPath: "env" }), + ); + expect(mockProjectService.create).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ env: normalizedEnv }), + ); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + details: expect.objectContaining({ + envKeys: ["API_KEY"], + }), + }), + ); + }); + + it("normalizes env bindings on update and avoids logging raw values", async () => { + const normalizedEnv = { + PLAIN_KEY: { type: "plain", value: "top-secret" }, + }; + mockSecretService.normalizeEnvBindingsForPersistence.mockResolvedValue(normalizedEnv); + mockProjectService.getById.mockResolvedValue(buildProject()); + mockProjectService.update.mockResolvedValue(buildProject({ env: normalizedEnv })); + + const app = await createApp(); + const res = await request(app) + .patch("/api/projects/project-1") + .send({ + env: normalizedEnv, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockProjectService.update).toHaveBeenCalledWith( + "project-1", + expect.objectContaining({ env: normalizedEnv }), + ); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + details: { + changedKeys: ["env"], + envKeys: ["PLAIN_KEY"], + }, + }), + ); + }); +}); diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index 482a6983..cd523dce 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -9,7 +9,7 @@ import { } from "@paperclipai/shared"; import { trackProjectCreated } from "@paperclipai/shared/telemetry"; import { validate } from "../middleware/validate.js"; -import { projectService, logActivity, workspaceOperationService } from "../services/index.js"; +import { projectService, logActivity, secretService, workspaceOperationService } from "../services/index.js"; import { conflict } from "../errors.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; import { startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForProjectWorkspace } from "../services/workspace-runtime.js"; @@ -18,7 +18,9 @@ import { getTelemetryClient } from "../telemetry.js"; export function projectRoutes(db: Db) { const router = Router(); const svc = projectService(db); + const secretsSvc = secretService(db); const workspaceOperations = workspaceOperationService(db); + const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true"; async function resolveCompanyIdForProjectReference(req: Request) { const companyIdQuery = req.query.companyId; @@ -82,6 +84,13 @@ export function projectRoutes(db: Db) { }; const { workspace, ...projectData } = req.body as CreateProjectPayload; + if (projectData.env !== undefined) { + projectData.env = await secretsSvc.normalizeEnvBindingsForPersistence( + companyId, + projectData.env, + { strictMode: strictSecretsMode, fieldPath: "env" }, + ); + } const project = await svc.create(companyId, projectData); let createdWorkspaceId: string | null = null; if (workspace) { @@ -107,6 +116,7 @@ export function projectRoutes(db: Db) { details: { name: project.name, workspaceId: createdWorkspaceId, + envKeys: project.env ? Object.keys(project.env).sort() : [], }, }); const telemetryClient = getTelemetryClient(); @@ -128,6 +138,12 @@ export function projectRoutes(db: Db) { if (typeof body.archivedAt === "string") { body.archivedAt = new Date(body.archivedAt); } + if (body.env !== undefined) { + body.env = await secretsSvc.normalizeEnvBindingsForPersistence(existing.companyId, body.env, { + strictMode: strictSecretsMode, + fieldPath: "env", + }); + } const project = await svc.update(id, body); if (!project) { res.status(404).json({ error: "Project not found" }); @@ -143,7 +159,13 @@ export function projectRoutes(db: Db) { action: "project.updated", entityType: "project", entityId: project.id, - details: req.body, + details: { + changedKeys: Object.keys(req.body).sort(), + envKeys: + body.env && typeof body.env === "object" && !Array.isArray(body.env) + ? Object.keys(body.env as Record).sort() + : undefined, + }, }); res.json(project); diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 097e62fc..b133411b 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -27,6 +27,7 @@ import type { CompanyPortabilitySidebarOrder, CompanyPortabilitySkillManifestEntry, CompanySkill, + AgentEnvConfig, RoutineVariable, } from "@paperclipai/shared"; import { @@ -39,6 +40,7 @@ import { ROUTINE_TRIGGER_KINDS, ROUTINE_TRIGGER_SIGNING_MODES, deriveProjectUrlKey, + envConfigSchema, normalizeAgentUrlKey, } from "@paperclipai/shared"; import { @@ -387,6 +389,11 @@ function isSensitiveEnvKey(key: string) { ); } +function normalizePortableProjectEnv(value: unknown): AgentEnvConfig | null { + const parsed = envConfigSchema.safeParse(value); + return parsed.success ? parsed.data : null; +} + type ResolvedSource = { manifest: CompanyPortabilityManifest; files: Record; @@ -419,6 +426,7 @@ type ProjectLike = { targetDate: string | null; color: string | null; status: string; + env: Record | null; executionWorkspacePolicy: Record | null; workspaces?: Array<{ id: string; @@ -2531,6 +2539,7 @@ function buildManifestFromPackageFiles( targetDate: asString(extension.targetDate), color: asString(extension.color), status: asString(extension.status), + env: normalizePortableProjectEnv(extension.env), executionWorkspacePolicy: isPlainRecord(extension.executionWorkspacePolicy) ? extension.executionWorkspacePolicy : null, @@ -3159,6 +3168,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { targetDate: project.targetDate ?? null, color: project.color ?? null, status: project.status, + env: normalizePortableProjectEnv(project.env) ?? undefined, executionWorkspacePolicy: exportPortableProjectExecutionWorkspacePolicy( slug, project.executionWorkspacePolicy, @@ -4095,6 +4105,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { status: manifestProject.status && PROJECT_STATUSES.includes(manifestProject.status as any) ? manifestProject.status as typeof PROJECT_STATUSES[number] : "backlog", + env: manifestProject.env, executionWorkspacePolicy: stripPortableProjectExecutionWorkspaceRefs(manifestProject.executionWorkspacePolicy), }; diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 0a3a1479..365d2225 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -86,6 +86,36 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([ "pi_local", ]); +type RuntimeConfigSecretResolver = Pick< + ReturnType, + "resolveAdapterConfigForRuntime" | "resolveEnvBindings" +>; + +export async function resolveExecutionRunAdapterConfig(input: { + companyId: string; + executionRunConfig: Record; + projectEnv: unknown; + secretsSvc: RuntimeConfigSecretResolver; +}) { + const { config: resolvedConfig, secretKeys } = await input.secretsSvc.resolveAdapterConfigForRuntime( + input.companyId, + input.executionRunConfig, + ); + const projectEnvResolution = input.projectEnv + ? await input.secretsSvc.resolveEnvBindings(input.companyId, input.projectEnv) + : { env: {}, secretKeys: new Set() }; + if (Object.keys(projectEnvResolution.env).length > 0) { + resolvedConfig.env = { + ...parseObject(resolvedConfig.env), + ...projectEnvResolution.env, + }; + for (const key of projectEnvResolution.secretKeys) { + secretKeys.add(key); + } + } + return { resolvedConfig, secretKeys }; +} + export function applyPersistedExecutionWorkspaceConfig(input: { config: Record; workspaceConfig: ExecutionWorkspaceConfig | null; @@ -2309,17 +2339,20 @@ export function heartbeatService(db: Db) { : null; const contextProjectId = readNonEmptyString(context.projectId); const executionProjectId = issueContext?.projectId ?? contextProjectId; - const projectExecutionWorkspacePolicy = executionProjectId + const projectContext = executionProjectId ? await db - .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy }) + .select({ + executionWorkspacePolicy: projects.executionWorkspacePolicy, + env: projects.env, + }) .from(projects) .where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId))) - .then((rows) => - gateProjectExecutionWorkspacePolicy( - parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy), - isolatedWorkspacesEnabled, - )) + .then((rows) => rows[0] ?? null) : null; + const projectExecutionWorkspacePolicy = gateProjectExecutionWorkspacePolicy( + parseProjectExecutionWorkspacePolicy(projectContext?.executionWorkspacePolicy), + isolatedWorkspacesEnabled, + ); const taskSession = taskKey ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey) : null; @@ -2416,10 +2449,12 @@ export function heartbeatService(db: Db) { : persistedWorkspaceManagedConfig; const configSnapshot = buildExecutionWorkspaceConfigSnapshot(mergedConfig); const executionRunConfig = stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig); - const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime( - agent.companyId, + const { resolvedConfig, secretKeys } = await resolveExecutionRunAdapterConfig({ + companyId: agent.companyId, executionRunConfig, - ); + projectEnv: projectContext?.env ?? null, + secretsSvc, + }); const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId); const runtimeConfig = { ...resolvedConfig, diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 6317f067..d288d4b3 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -39,6 +39,11 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding { } export function secretService(db: Db) { + type NormalizeEnvOptions = { + strictMode?: boolean; + fieldPath?: string; + }; + async function getById(id: string) { return db .select() @@ -94,10 +99,10 @@ export function secretService(db: Db) { async function normalizeEnvConfig( companyId: string, envValue: unknown, - opts?: { strictMode?: boolean }, + opts?: NormalizeEnvOptions, ): Promise { const record = asRecord(envValue); - if (!record) throw unprocessable("adapterConfig.env must be an object"); + if (!record) throw unprocessable(`${opts?.fieldPath ?? "env"} must be an object`); const normalized: AgentEnvConfig = {}; for (const [key, rawBinding] of Object.entries(record)) { @@ -292,6 +297,12 @@ export function secretService(db: Db) { opts?: { strictMode?: boolean }, ) => normalizeAdapterConfigForPersistenceInternal(companyId, adapterConfig, opts), + normalizeEnvBindingsForPersistence: async ( + companyId: string, + envValue: unknown, + opts?: NormalizeEnvOptions, + ) => normalizeEnvConfig(companyId, envValue, opts), + normalizeHireApprovalPayloadForPersistence: async ( companyId: string, payload: Record, diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 8bc95ef6..3bb2433c 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -44,6 +44,7 @@ import { MarkdownEditor } from "./MarkdownEditor"; import { ChoosePathButton } from "./PathInstructionsModal"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; import { ReportsToPicker } from "./ReportsToPicker"; +import { EnvVarEditor } from "./EnvVarEditor"; import { shouldShowLegacyWorkingDirectoryField } from "../lib/legacy-agent-config"; import { listAdapterOptions, listVisibleAdapterTypes } from "../adapters/metadata"; import { getAdapterLabel } from "../adapters/adapter-display-registry"; @@ -1082,269 +1083,6 @@ function AdapterTypeDropdown({ ); } -function EnvVarEditor({ - value, - secrets, - onCreateSecret, - onChange, -}: { - value: Record; - secrets: CompanySecret[]; - onCreateSecret: (name: string, value: string) => Promise; - onChange: (env: Record | undefined) => void; -}) { - type Row = { - key: string; - source: "plain" | "secret"; - plainValue: string; - secretId: string; - }; - - function toRows(rec: Record | null | undefined): Row[] { - if (!rec || typeof rec !== "object") { - return [{ key: "", source: "plain", plainValue: "", secretId: "" }]; - } - const entries = Object.entries(rec).map(([k, binding]) => { - if (typeof binding === "string") { - return { - key: k, - source: "plain" as const, - plainValue: binding, - secretId: "", - }; - } - if ( - typeof binding === "object" && - binding !== null && - "type" in binding && - (binding as { type?: unknown }).type === "secret_ref" - ) { - const recBinding = binding as { secretId?: unknown }; - return { - key: k, - source: "secret" as const, - plainValue: "", - secretId: typeof recBinding.secretId === "string" ? recBinding.secretId : "", - }; - } - if ( - typeof binding === "object" && - binding !== null && - "type" in binding && - (binding as { type?: unknown }).type === "plain" - ) { - const recBinding = binding as { value?: unknown }; - return { - key: k, - source: "plain" as const, - plainValue: typeof recBinding.value === "string" ? recBinding.value : "", - secretId: "", - }; - } - return { - key: k, - source: "plain" as const, - plainValue: "", - secretId: "", - }; - }); - return [...entries, { key: "", source: "plain", plainValue: "", secretId: "" }]; - } - - const [rows, setRows] = useState(() => toRows(value)); - const [sealError, setSealError] = useState(null); - const valueRef = useRef(value); - const emittingRef = useRef(false); - - // Sync when value identity changes (overlay reset after save). - // Skip re-sync when the change was triggered by our own emit() to avoid - // reverting local row state (e.g. a secret-transition dropdown choice). - useEffect(() => { - if (emittingRef.current) { - emittingRef.current = false; - valueRef.current = value; - return; - } - if (value !== valueRef.current) { - valueRef.current = value; - setRows(toRows(value)); - } - }, [value]); - - function emit(nextRows: Row[]) { - const rec: Record = {}; - for (const row of nextRows) { - const k = row.key.trim(); - if (!k) continue; - if (row.source === "secret") { - if (row.secretId) { - rec[k] = { type: "secret_ref", secretId: row.secretId, version: "latest" }; - } else { - // Row is transitioning to secret but user hasn't picked one yet. - // Preserve the plain value so it isn't silently dropped. - rec[k] = { type: "plain", value: row.plainValue }; - } - } else { - rec[k] = { type: "plain", value: row.plainValue }; - } - } - emittingRef.current = true; - onChange(Object.keys(rec).length > 0 ? rec : undefined); - } - - function updateRow(i: number, patch: Partial) { - const withPatch = rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r)); - if ( - withPatch[withPatch.length - 1].key || - withPatch[withPatch.length - 1].plainValue || - withPatch[withPatch.length - 1].secretId - ) { - withPatch.push({ key: "", source: "plain", plainValue: "", secretId: "" }); - } - setRows(withPatch); - emit(withPatch); - } - - function removeRow(i: number) { - const next = rows.filter((_, idx) => idx !== i); - if ( - next.length === 0 || - next[next.length - 1].key || - next[next.length - 1].plainValue || - next[next.length - 1].secretId - ) { - next.push({ key: "", source: "plain", plainValue: "", secretId: "" }); - } - setRows(next); - emit(next); - } - - function defaultSecretName(key: string): string { - return key - .trim() - .toLowerCase() - .replace(/[^a-z0-9_]+/g, "_") - .replace(/^_+|_+$/g, "") - .slice(0, 64); - } - - async function sealRow(i: number) { - const row = rows[i]; - if (!row) return; - const key = row.key.trim(); - const plain = row.plainValue; - if (!key || plain.length === 0) return; - - const suggested = defaultSecretName(key) || "secret"; - const name = window.prompt("Secret name", suggested)?.trim(); - if (!name) return; - - try { - setSealError(null); - const created = await onCreateSecret(name, plain); - updateRow(i, { - source: "secret", - secretId: created.id, - }); - } catch (err) { - setSealError(err instanceof Error ? err.message : "Failed to create secret"); - } - } - - return ( -
- {rows.map((row, i) => { - const isTrailing = - i === rows.length - 1 && - !row.key && - !row.plainValue && - !row.secretId; - return ( -
- updateRow(i, { key: e.target.value })} - /> - - {row.source === "secret" ? ( - <> - - - - ) : ( - <> - updateRow(i, { plainValue: e.target.value })} - /> - - - )} - {!isTrailing ? ( - - ) : ( -
- )} -
- ); - })} - {sealError &&

{sealError}

} -

- PAPERCLIP_* variables are injected automatically at runtime. -

-
- ); -} - function ModelDropdown({ models, value, diff --git a/ui/src/components/EnvVarEditor.tsx b/ui/src/components/EnvVarEditor.tsx new file mode 100644 index 00000000..01df6d55 --- /dev/null +++ b/ui/src/components/EnvVarEditor.tsx @@ -0,0 +1,252 @@ +import { useEffect, useRef, useState } from "react"; +import type { CompanySecret, EnvBinding } from "@paperclipai/shared"; +import { X } from "lucide-react"; +import { cn } from "../lib/utils"; + +const inputClass = + "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; + +type Row = { + key: string; + source: "plain" | "secret"; + plainValue: string; + secretId: string; +}; + +function toRows(rec: Record | null | undefined): Row[] { + if (!rec || typeof rec !== "object") { + return [{ key: "", source: "plain", plainValue: "", secretId: "" }]; + } + const entries = Object.entries(rec).map(([key, binding]) => { + if (typeof binding === "string") { + return { key, source: "plain" as const, plainValue: binding, secretId: "" }; + } + if ( + typeof binding === "object" && + binding !== null && + "type" in binding && + (binding as { type?: unknown }).type === "secret_ref" + ) { + const record = binding as { secretId?: unknown }; + return { + key, + source: "secret" as const, + plainValue: "", + secretId: typeof record.secretId === "string" ? record.secretId : "", + }; + } + if ( + typeof binding === "object" && + binding !== null && + "type" in binding && + (binding as { type?: unknown }).type === "plain" + ) { + const record = binding as { value?: unknown }; + return { + key, + source: "plain" as const, + plainValue: typeof record.value === "string" ? record.value : "", + secretId: "", + }; + } + return { key, source: "plain" as const, plainValue: "", secretId: "" }; + }); + return [...entries, { key: "", source: "plain", plainValue: "", secretId: "" }]; +} + +export function EnvVarEditor({ + value, + secrets, + onCreateSecret, + onChange, +}: { + value: Record; + secrets: CompanySecret[]; + onCreateSecret: (name: string, value: string) => Promise; + onChange: (env: Record | undefined) => void; +}) { + const [rows, setRows] = useState(() => toRows(value)); + const [sealError, setSealError] = useState(null); + const valueRef = useRef(value); + const emittingRef = useRef(false); + + useEffect(() => { + if (emittingRef.current) { + emittingRef.current = false; + valueRef.current = value; + return; + } + if (value !== valueRef.current) { + valueRef.current = value; + setRows(toRows(value)); + } + }, [value]); + + function emit(nextRows: Row[]) { + const rec: Record = {}; + for (const row of nextRows) { + const key = row.key.trim(); + if (!key) continue; + if (row.source === "secret") { + if (row.secretId) { + rec[key] = { type: "secret_ref", secretId: row.secretId, version: "latest" }; + } else { + rec[key] = { type: "plain", value: row.plainValue }; + } + } else { + rec[key] = { type: "plain", value: row.plainValue }; + } + } + emittingRef.current = true; + onChange(Object.keys(rec).length > 0 ? rec : undefined); + } + + function updateRow(index: number, patch: Partial) { + const withPatch = rows.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row)); + if ( + withPatch[withPatch.length - 1].key || + withPatch[withPatch.length - 1].plainValue || + withPatch[withPatch.length - 1].secretId + ) { + withPatch.push({ key: "", source: "plain", plainValue: "", secretId: "" }); + } + setRows(withPatch); + emit(withPatch); + } + + function removeRow(index: number) { + const next = rows.filter((_, rowIndex) => rowIndex !== index); + if ( + next.length === 0 || + next[next.length - 1].key || + next[next.length - 1].plainValue || + next[next.length - 1].secretId + ) { + next.push({ key: "", source: "plain", plainValue: "", secretId: "" }); + } + setRows(next); + emit(next); + } + + function defaultSecretName(key: string) { + return key + .trim() + .toLowerCase() + .replace(/[^a-z0-9_]+/g, "_") + .replace(/^_+|_+$/g, "") + .slice(0, 64); + } + + async function sealRow(index: number) { + const row = rows[index]; + if (!row) return; + const key = row.key.trim(); + const plain = row.plainValue; + if (!key || plain.length === 0) return; + + const suggested = defaultSecretName(key) || "secret"; + const name = window.prompt("Secret name", suggested)?.trim(); + if (!name) return; + + try { + setSealError(null); + const created = await onCreateSecret(name, plain); + updateRow(index, { source: "secret", secretId: created.id }); + } catch (error) { + setSealError(error instanceof Error ? error.message : "Failed to create secret"); + } + } + + return ( +
+ {rows.map((row, index) => { + const isTrailing = + index === rows.length - 1 && + !row.key && + !row.plainValue && + !row.secretId; + return ( +
+ updateRow(index, { key: event.target.value })} + /> + + {row.source === "secret" ? ( + <> + + + + ) : ( + <> + updateRow(index, { plainValue: event.target.value })} + /> + + + )} + {!isTrailing ? ( + + ) : ( +
+ )} +
+ ); + })} + {sealError &&

{sealError}

} +

+ PAPERCLIP_* variables are injected automatically at runtime. +

+
+ ); +} diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 9fe4d943..85172fd4 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -7,6 +7,7 @@ import { cn, formatDate } from "../lib/utils"; import { goalsApi } from "../api/goals"; import { instanceSettingsApi } from "../api/instanceSettings"; import { projectsApi } from "../api/projects"; +import { secretsApi } from "../api/secrets"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; import { statusBadge, statusBadgeDefault } from "../lib/status-colors"; @@ -19,6 +20,7 @@ import { ChoosePathButton } from "./PathInstructionsModal"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { DraftInput } from "./agent-config-primitives"; import { InlineEditor } from "./InlineEditor"; +import { EnvVarEditor } from "./EnvVarEditor"; const PROJECT_STATUSES = [ { value: "backlog", label: "Backlog" }, @@ -43,6 +45,7 @@ export type ProjectConfigFieldKey = | "description" | "status" | "goals" + | "env" | "execution_workspace_enabled" | "execution_workspace_default_mode" | "execution_workspace_base_ref" @@ -245,6 +248,21 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); + const { data: availableSecrets = [] } = useQuery({ + queryKey: selectedCompanyId ? queryKeys.secrets.list(selectedCompanyId) : ["secrets", "none"], + queryFn: () => secretsApi.list(selectedCompanyId!), + enabled: Boolean(selectedCompanyId), + }); + const createSecret = useMutation({ + mutationFn: (input: { name: string; value: string }) => { + if (!selectedCompanyId) throw new Error("Select a company to create secrets"); + return secretsApi.create(selectedCompanyId, input); + }, + onSuccess: () => { + if (!selectedCompanyId) return; + queryClient.invalidateQueries({ queryKey: queryKeys.secrets.list(selectedCompanyId) }); + }, + }); const linkedGoalIds = project.goalIds.length > 0 ? project.goalIds @@ -583,6 +601,26 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )} + } + alignStart + valueClassName="space-y-2" + > +
+ { + const created = await createSecret.mutateAsync({ name, value }); + return created; + }} + onChange={(env) => commitField("env", { env: env ?? null })} + /> +

+ Applied to all runs for issues in this project. Project values override agent env on key conflicts. +

+
+
}> {formatDate(project.createdAt)} diff --git a/ui/src/components/RoutineRunVariablesDialog.test.tsx b/ui/src/components/RoutineRunVariablesDialog.test.tsx index ea59a92b..03b18da0 100644 --- a/ui/src/components/RoutineRunVariablesDialog.test.tsx +++ b/ui/src/components/RoutineRunVariablesDialog.test.tsx @@ -58,6 +58,7 @@ function createProject(): Project { leadAgentId: null, targetDate: null, color: "#22c55e", + env: null, pauseReason: null, pausedAt: null, archivedAt: null, diff --git a/ui/src/lib/company-portability-sidebar.test.ts b/ui/src/lib/company-portability-sidebar.test.ts index 1bc7b06d..68d0e13f 100644 --- a/ui/src/lib/company-portability-sidebar.test.ts +++ b/ui/src/lib/company-portability-sidebar.test.ts @@ -45,6 +45,7 @@ function makeProject(id: string, name: string): Project { leadAgentId: null, targetDate: null, color: null, + env: null, pauseReason: null, pausedAt: null, executionWorkspacePolicy: null, From 42b326bcc629475357710c7bbf93b37eeeece60d Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 09:53:21 -0500 Subject: [PATCH 09/18] fix(e2e): harden signoff policy tests for authenticated deployments Address QA review feedback on the signoff e2e suite (86b24a5e): - Use dedicated port 3199 with local_trusted mode to avoid reusing the dev server in authenticated mode (fixes 403 errors) - Add proper agent authentication via API keys + heartbeat run IDs - Fix non-participant test to actually verify access control rejection - Add afterAll cleanup (dispose contexts, revoke keys, delete agents) - Reviewers/approvers PATCH without checkout to preserve in_review state Co-Authored-By: Paperclip --- tests/e2e/playwright.config.ts | 9 +- tests/e2e/signoff-policy.spec.ts | 415 +++++++++++++++++++------------ 2 files changed, 260 insertions(+), 164 deletions(-) diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 33022502..572b012a 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from "@playwright/test"; -const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3100); +// Use a dedicated port so e2e tests always start their own server in local_trusted mode, +// even when the dev server is running on :3100 in authenticated mode. +const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3199); const BASE_URL = `http://127.0.0.1:${PORT}`; export default defineConfig({ @@ -29,6 +31,11 @@ export default defineConfig({ timeout: 120_000, stdout: "pipe", stderr: "pipe", + env: { + ...process.env, + PORT: String(PORT), + PAPERCLIP_DEPLOYMENT_MODE: "local_trusted", + }, }, outputDir: "./test-results", reporter: [["list"], ["html", { open: "never", outputFolder: "./playwright-report" }]], diff --git a/tests/e2e/signoff-policy.spec.ts b/tests/e2e/signoff-policy.spec.ts index a534415e..97e67746 100644 --- a/tests/e2e/signoff-policy.spec.ts +++ b/tests/e2e/signoff-policy.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type APIRequestContext } from "@playwright/test"; +import { test, expect, request as pwRequest, type APIRequestContext } from "@playwright/test"; /** * E2E: Signoff execution policy flow. @@ -11,119 +11,219 @@ import { test, expect, type APIRequestContext } from "@playwright/test"; * 5. Approver approves → execution completes, issue marked done * 6. Verify "changes requested" flow returns to executor * - * This test is API-driven with UI verification of execution state labels. + * Requires local_trusted deployment mode (set in playwright.config.ts webServer env). + * + * Agent auth flow: + * - Board request (local_trusted auto-auth) handles setup/teardown. + * - Agent-specific actions use API keys + heartbeat run IDs. + * - Reviewers/approvers invoke heartbeat runs (gets run IDs) then PATCH + * directly without checkout (checkout would force in_progress, breaking + * the in_review state the signoff policy requires). */ +const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3199); +const BASE_URL = `http://127.0.0.1:${PORT}`; const COMPANY_NAME = `E2E-Signoff-${Date.now()}`; -interface TestContext { - baseUrl: string; - companyId: string; - companyPrefix: string; - executorAgentId: string; - reviewerAgentId: string; - approverAgentId: string; +interface AgentAuth { + agentId: string; + token: string; + keyId: string; + request: APIRequestContext; } -async function setupCompany(request: APIRequestContext, baseUrl: string): Promise { +interface TestContext { + companyId: string; + companyPrefix: string; + executor: AgentAuth; + reviewer: AgentAuth; + approver: AgentAuth; + boardRequest: APIRequestContext; + issueIds: string[]; +} + +/** Create an authenticated APIRequestContext for an agent (token set, no run ID yet). */ +async function createAgentRequest(token: string): Promise { + return pwRequest.newContext({ + baseURL: BASE_URL, + extraHTTPHeaders: { Authorization: `Bearer ${token}` }, + }); +} + +/** Invoke a heartbeat run for an agent, returning the run ID. */ +async function invokeHeartbeat(board: APIRequestContext, agentId: string): Promise { + const res = await board.post(`${BASE_URL}/api/agents/${agentId}/heartbeat/invoke`); + expect(res.ok()).toBe(true); + const run = await res.json(); + return run.id; +} + +/** PATCH an issue as an agent with a fresh heartbeat run ID. */ +async function agentPatch( + board: APIRequestContext, + agent: AgentAuth, + issueId: string, + data: Record, +) { + const runId = await invokeHeartbeat(board, agent.agentId); + const res = await agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, { + headers: { "X-Paperclip-Run-Id": runId }, + data, + }); + return res; +} + +/** Checkout an issue as an agent, then PATCH it. Used for executor mark-done. */ +async function agentCheckoutAndPatch( + board: APIRequestContext, + agent: AgentAuth, + issueId: string, + expectedStatuses: string[], + patchData: Record, +) { + const runId = await invokeHeartbeat(board, agent.agentId); + // Checkout (sets executionRunId so PATCH is allowed) + const checkoutRes = await agent.request.post(`${BASE_URL}/api/issues/${issueId}/checkout`, { + headers: { "X-Paperclip-Run-Id": runId }, + data: { agentId: agent.agentId, expectedStatuses }, + }); + if (!checkoutRes.ok()) { + // If agent checkout fails (e.g. run expired), fall back to board checkout + // then PATCH with the agent's identity + const boardCheckout = await board.post(`${BASE_URL}/api/issues/${issueId}/checkout`, { + data: { agentId: agent.agentId, expectedStatuses }, + }); + if (!boardCheckout.ok()) { + throw new Error(`Board checkout failed: ${await boardCheckout.text()}`); + } + // Board PATCH (executor mark-done triggers signoff regardless of actor) + const res = await board.patch(`${BASE_URL}/api/issues/${issueId}`, { + data: patchData, + }); + return res; + } + // PATCH with agent identity + const res = await agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, { + headers: { "X-Paperclip-Run-Id": runId }, + data: patchData, + }); + return res; +} + +async function setupCompany(boardRequest: APIRequestContext): Promise { + // Verify server is in local_trusted mode + const healthRes = await boardRequest.get(`${BASE_URL}/api/health`); + expect(healthRes.ok()).toBe(true); + const health = await healthRes.json(); + if (health.deploymentMode !== "local_trusted") { + throw new Error( + `Signoff e2e tests require local_trusted deployment mode, ` + + `but server is in "${health.deploymentMode}" mode. ` + + `Set PAPERCLIP_DEPLOYMENT_MODE=local_trusted or use the webServer config.`, + ); + } + // Create company - const companyRes = await request.post(`${baseUrl}/api/companies`, { + const companyRes = await boardRequest.post(`${BASE_URL}/api/companies`, { data: { name: COMPANY_NAME }, }); - expect(companyRes.ok()).toBe(true); + if (!companyRes.ok()) { + const errBody = await companyRes.text(); + throw new Error(`POST /api/companies → ${companyRes.status()}: ${errBody}`); + } const company = await companyRes.json(); const companyId = company.id; + const companyPrefix = company.issuePrefix ?? company.prefix ?? company.urlKey ?? "E2E"; - // Fetch company prefix from the company object - const companyPrefix = company.prefix ?? company.urlKey ?? "E2E"; + // Helper: create agent + API key + request context + async function createAgent(name: string, role: string, title: string): Promise { + const agentRes = await boardRequest.post(`${BASE_URL}/api/companies/${companyId}/agents`, { + data: { name, role, title, adapterType: "process", adapterConfig: { command: "echo done" } }, + }); + expect(agentRes.ok()).toBe(true); + const agent = await agentRes.json(); - // Create executor agent (engineer) - const executorRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { - data: { - name: "Executor", - role: "engineer", - title: "Software Engineer", - adapterType: "process", - adapterConfig: { command: "echo done" }, - }, - }); - expect(executorRes.ok()).toBe(true); - const executor = await executorRes.json(); + const keyRes = await boardRequest.post(`${BASE_URL}/api/agents/${agent.id}/keys`, { + data: { name: `e2e-${name.toLowerCase()}` }, + }); + expect(keyRes.ok()).toBe(true); + const keyData = await keyRes.json(); - // Create reviewer agent (QA) - const reviewerRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { - data: { - name: "Reviewer", - role: "qa", - title: "QA Engineer", - adapterType: "process", - adapterConfig: { command: "echo done" }, - }, - }); - expect(reviewerRes.ok()).toBe(true); - const reviewer = await reviewerRes.json(); + return { + agentId: agent.id, + token: keyData.token, + keyId: keyData.id, + request: await createAgentRequest(keyData.token), + }; + } - // Create approver agent (CTO) - const approverRes = await request.post(`${baseUrl}/api/companies/${companyId}/agents`, { - data: { - name: "Approver", - role: "cto", - title: "CTO", - adapterType: "process", - adapterConfig: { command: "echo done" }, - }, - }); - expect(approverRes.ok()).toBe(true); - const approver = await approverRes.json(); + const executor = await createAgent("Executor", "engineer", "Software Engineer"); + const reviewer = await createAgent("Reviewer", "qa", "QA Engineer"); + const approver = await createAgent("Approver", "cto", "CTO"); return { - baseUrl, companyId, companyPrefix, - executorAgentId: executor.id, - reviewerAgentId: reviewer.id, - approverAgentId: approver.id, + executor, + reviewer, + approver, + boardRequest, + issueIds: [], }; } -async function createIssueWithPolicy( - request: APIRequestContext, - ctx: TestContext, - title: string, -) { - const res = await request.post(`${ctx.baseUrl}/api/companies/${ctx.companyId}/issues`, { +async function createIssueWithPolicy(ctx: TestContext, title: string, stages?: unknown[]) { + const defaultStages = [ + { type: "review", participants: [{ type: "agent", agentId: ctx.reviewer.agentId }] }, + { type: "approval", participants: [{ type: "agent", agentId: ctx.approver.agentId }] }, + ]; + const res = await ctx.boardRequest.post(`${BASE_URL}/api/companies/${ctx.companyId}/issues`, { data: { title, status: "in_progress", - assigneeAgentId: ctx.executorAgentId, - executionPolicy: { - stages: [ - { - type: "review", - participants: [{ type: "agent", agentId: ctx.reviewerAgentId }], - }, - { - type: "approval", - participants: [{ type: "agent", agentId: ctx.approverAgentId }], - }, - ], - }, + assigneeAgentId: ctx.executor.agentId, + executionPolicy: { stages: stages ?? defaultStages }, }, }); expect(res.ok()).toBe(true); - return res.json(); + const issue = await res.json(); + ctx.issueIds.push(issue.id); + return issue; } test.describe("Signoff execution policy", () => { let ctx: TestContext; - test.beforeAll(async ({ request }) => { - const baseUrl = (test.info().project.use as { baseURL?: string }).baseURL ?? "http://127.0.0.1:3100"; - ctx = await setupCompany(request, baseUrl); + test.beforeAll(async () => { + const boardRequest = await pwRequest.newContext({ baseURL: BASE_URL }); + ctx = await setupCompany(boardRequest); }); - test("happy path: executor → review → approval → done", async ({ request, page }) => { - const issue = await createIssueWithPolicy(request, ctx, "Signoff happy path"); + test.afterAll(async () => { + if (!ctx) return; + const board = ctx.boardRequest; + + // Dispose agent request contexts + for (const agent of [ctx.executor, ctx.reviewer, ctx.approver]) { + await agent.request.dispose(); + } + + // Clean up issues, keys, agents, company (best-effort) + for (const issueId of ctx.issueIds) { + await board.patch(`${BASE_URL}/api/issues/${issueId}`, { + data: { status: "cancelled", comment: "E2E test cleanup." }, + }).catch(() => {}); + } + for (const agent of [ctx.executor, ctx.reviewer, ctx.approver]) { + await board.delete(`${BASE_URL}/api/agents/${agent.agentId}/keys/${agent.keyId}`).catch(() => {}); + await board.delete(`${BASE_URL}/api/agents/${agent.agentId}`).catch(() => {}); + } + await board.delete(`${BASE_URL}/api/companies/${ctx.companyId}`).catch(() => {}); + await board.dispose(); + }); + + test("happy path: executor → review → approval → done", async ({ page }) => { + const issue = await createIssueWithPolicy(ctx, "Signoff happy path"); const issueId = issue.id; // Verify policy was saved @@ -133,23 +233,21 @@ test.describe("Signoff execution policy", () => { expect(issue.executionPolicy.stages[1].type).toBe("approval"); // Step 1: Executor marks done → should route to reviewer - const step1Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { - status: "done", - comment: "Implemented the feature, ready for review.", - }, - }); + const step1Res = await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issueId, ["in_progress"], + { status: "done", comment: "Implemented the feature, ready for review." }, + ); expect(step1Res.ok()).toBe(true); const step1Issue = await step1Res.json(); expect(step1Issue.status).toBe("in_review"); - expect(step1Issue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(step1Issue.assigneeAgentId).toBe(ctx.reviewer.agentId); expect(step1Issue.executionState).toBeTruthy(); expect(step1Issue.executionState.status).toBe("pending"); expect(step1Issue.executionState.currentStageType).toBe("review"); expect(step1Issue.executionState.returnAssignee).toMatchObject({ type: "agent", - agentId: ctx.executorAgentId, + agentId: ctx.executor.agentId, }); // Step 2: Navigate to issue in UI and verify execution label @@ -157,17 +255,15 @@ test.describe("Signoff execution policy", () => { await expect(page.locator("text=Review pending")).toBeVisible({ timeout: 10_000 }); // Step 3: Reviewer approves → should route to approver - const step3Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { - status: "done", - comment: "QA signoff complete. Looks good.", - }, - }); + const step3Res = await agentPatch( + ctx.boardRequest, ctx.reviewer, issueId, + { status: "done", comment: "QA signoff complete. Looks good." }, + ); expect(step3Res.ok()).toBe(true); const step3Issue = await step3Res.json(); expect(step3Issue.status).toBe("in_review"); - expect(step3Issue.assigneeAgentId).toBe(ctx.approverAgentId); + expect(step3Issue.assigneeAgentId).toBe(ctx.approver.agentId); expect(step3Issue.executionState.status).toBe("pending"); expect(step3Issue.executionState.currentStageType).toBe("approval"); expect(step3Issue.executionState.completedStageIds).toHaveLength(1); @@ -177,12 +273,10 @@ test.describe("Signoff execution policy", () => { await expect(page.locator("text=Approval pending")).toBeVisible({ timeout: 10_000 }); // Step 5: Approver approves → should complete - const step5Res = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { - status: "done", - comment: "Approved. Ship it.", - }, - }); + const step5Res = await agentPatch( + ctx.boardRequest, ctx.approver, issueId, + { status: "done", comment: "Approved. Ship it." }, + ); expect(step5Res.ok()).toBe(true); const step5Issue = await step5Res.json(); @@ -192,115 +286,110 @@ test.describe("Signoff execution policy", () => { expect(step5Issue.executionState.lastDecisionOutcome).toBe("approved"); }); - test("changes requested: reviewer bounces back to executor", async ({ request }) => { - const issue = await createIssueWithPolicy(request, ctx, "Signoff changes requested"); + test("changes requested: reviewer bounces back to executor", async () => { + const issue = await createIssueWithPolicy(ctx, "Signoff changes requested"); const issueId = issue.id; // Executor marks done → routes to reviewer - const doneRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { status: "done", comment: "Ready for review." }, - }); + const doneRes = await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issueId, ["in_progress"], + { status: "done", comment: "Ready for review." }, + ); expect(doneRes.ok()).toBe(true); - const reviewIssue = await doneRes.json(); - expect(reviewIssue.status).toBe("in_review"); - expect(reviewIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect((await doneRes.json()).status).toBe("in_review"); // Reviewer requests changes → returns to executor - const changesRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { - status: "in_progress", - comment: "Needs another pass on edge cases.", - }, - }); + const changesRes = await agentPatch( + ctx.boardRequest, ctx.reviewer, issueId, + { status: "in_progress", comment: "Needs another pass on edge cases." }, + ); expect(changesRes.ok()).toBe(true); const changesIssue = await changesRes.json(); expect(changesIssue.status).toBe("in_progress"); - expect(changesIssue.assigneeAgentId).toBe(ctx.executorAgentId); + expect(changesIssue.assigneeAgentId).toBe(ctx.executor.agentId); expect(changesIssue.executionState.status).toBe("changes_requested"); expect(changesIssue.executionState.lastDecisionOutcome).toBe("changes_requested"); // Executor re-submits → goes back to reviewer (same stage) - const resubmitRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { status: "done", comment: "Fixed the edge cases." }, - }); + const resubmitRes = await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issueId, ["in_progress"], + { status: "done", comment: "Fixed the edge cases." }, + ); expect(resubmitRes.ok()).toBe(true); const resubmitIssue = await resubmitRes.json(); expect(resubmitIssue.status).toBe("in_review"); - expect(resubmitIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(resubmitIssue.assigneeAgentId).toBe(ctx.reviewer.agentId); expect(resubmitIssue.executionState.status).toBe("pending"); expect(resubmitIssue.executionState.currentStageType).toBe("review"); }); - test("comment required: approval without comment fails", async ({ request }) => { - const issue = await createIssueWithPolicy(request, ctx, "Signoff comment required"); + test("comment required: approval without comment fails", async () => { + const issue = await createIssueWithPolicy(ctx, "Signoff comment required"); const issueId = issue.id; - // Executor marks done - await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { status: "done", comment: "Done." }, - }); + // Executor marks done → routes to reviewer + await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issueId, ["in_progress"], + { status: "done", comment: "Done." }, + ); // Reviewer tries to approve without comment → should fail - const noCommentRes = await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { status: "done" }, - }); - // Server should reject: 422 or similar + const noCommentRes = await agentPatch( + ctx.boardRequest, ctx.reviewer, issueId, + { status: "done" }, + ); expect(noCommentRes.ok()).toBe(false); const errorBody = await noCommentRes.json(); expect(JSON.stringify(errorBody)).toContain("comment"); }); - test("non-participant cannot advance stage", async ({ request }) => { - const issue = await createIssueWithPolicy(request, ctx, "Signoff access control"); + test("non-participant cannot advance stage", async () => { + const issue = await createIssueWithPolicy(ctx, "Signoff access control"); const issueId = issue.id; // Executor marks done → routes to reviewer - await request.patch(`${ctx.baseUrl}/api/issues/${issueId}`, { - data: { status: "done", comment: "Done." }, - }); + const doneRes = await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issueId, ["in_progress"], + { status: "done", comment: "Done." }, + ); + expect(doneRes.ok()).toBe(true); // Verify issue is in_review with reviewer - const issueRes = await request.get(`${ctx.baseUrl}/api/issues/${issueId}`); + const issueRes = await ctx.boardRequest.get(`${BASE_URL}/api/issues/${issueId}`); const inReviewIssue = await issueRes.json(); expect(inReviewIssue.status).toBe("in_review"); - expect(inReviewIssue.assigneeAgentId).toBe(ctx.reviewerAgentId); + expect(inReviewIssue.assigneeAgentId).toBe(ctx.reviewer.agentId); expect(inReviewIssue.executionState.currentStageType).toBe("review"); + + // Non-participant (approver at this stage) tries to advance → should be rejected + const advanceRes = await agentPatch( + ctx.boardRequest, ctx.approver, issueId, + { status: "done", comment: "I'm the approver, not the reviewer." }, + ); + expect(advanceRes.ok()).toBe(false); + expect(advanceRes.status()).toBeGreaterThanOrEqual(400); }); - test("review-only policy: reviewer approval completes execution", async ({ request }) => { - // Create issue with review-only policy (no approval stage) - const res = await request.post(`${ctx.baseUrl}/api/companies/${ctx.companyId}/issues`, { - data: { - title: "Signoff review-only", - status: "in_progress", - assigneeAgentId: ctx.executorAgentId, - executionPolicy: { - stages: [ - { - type: "review", - participants: [{ type: "agent", agentId: ctx.reviewerAgentId }], - }, - ], - }, - }, - }); - expect(res.ok()).toBe(true); - const issue = await res.json(); + test("review-only policy: reviewer approval completes execution", async () => { + const issue = await createIssueWithPolicy(ctx, "Signoff review-only", [ + { type: "review", participants: [{ type: "agent", agentId: ctx.reviewer.agentId }] }, + ]); // Executor marks done → routes to reviewer - const doneRes = await request.patch(`${ctx.baseUrl}/api/issues/${issue.id}`, { - data: { status: "done", comment: "Ready for review." }, - }); + const doneRes = await agentCheckoutAndPatch( + ctx.boardRequest, ctx.executor, issue.id, ["in_progress"], + { status: "done", comment: "Ready for review." }, + ); expect(doneRes.ok()).toBe(true); - const reviewIssue = await doneRes.json(); - expect(reviewIssue.status).toBe("in_review"); + expect((await doneRes.json()).status).toBe("in_review"); // Reviewer approves → should complete immediately (no approval stage) - const approveRes = await request.patch(`${ctx.baseUrl}/api/issues/${issue.id}`, { - data: { status: "done", comment: "LGTM." }, - }); + const approveRes = await agentPatch( + ctx.boardRequest, ctx.reviewer, issue.id, + { status: "done", comment: "LGTM." }, + ); expect(approveRes.ok()).toBe(true); const doneIssue = await approveRes.json(); expect(doneIssue.status).toBe("done"); From 1e76bbe38c69b3ce2c2651062044557c25a77cf1 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 10:05:41 -0500 Subject: [PATCH 10/18] test(db): cover 0050 migration replay --- packages/db/src/client.test.ts | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/db/src/client.test.ts b/packages/db/src/client.test.ts index 64b1caf1..81cc2ace 100644 --- a/packages/db/src/client.test.ts +++ b/packages/db/src/client.test.ts @@ -401,4 +401,70 @@ describeEmbeddedPostgres("applyPendingMigrations", () => { }, 20_000, ); + + it( + "replays migration 0050 safely when projects.env already exists", + async () => { + const connectionString = await createTempDatabase(); + + await applyPendingMigrations(connectionString); + + const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const stiffLuckmanHash = await migrationHash("0050_stiff_luckman.sql"); + + await sql.unsafe( + `DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${stiffLuckmanHash}'`, + ); + + const columns = await sql.unsafe<{ column_name: string }[]>( + ` + SELECT column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'projects' + AND column_name = 'env' + `, + ); + expect(columns).toHaveLength(1); + } finally { + await sql.end(); + } + + const pendingState = await inspectMigrations(connectionString); + expect(pendingState).toMatchObject({ + status: "needsMigrations", + pendingMigrations: ["0050_stiff_luckman.sql"], + reason: "pending-migrations", + }); + + await applyPendingMigrations(connectionString); + + const finalState = await inspectMigrations(connectionString); + expect(finalState.status).toBe("upToDate"); + + const verifySql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + const columns = await verifySql.unsafe<{ column_name: string; is_nullable: string; data_type: string }[]>( + ` + SELECT column_name, is_nullable, data_type + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'projects' + AND column_name = 'env' + `, + ); + expect(columns).toEqual([ + expect.objectContaining({ + column_name: "env", + is_nullable: "YES", + data_type: "jsonb", + }), + ]); + } finally { + await verifySql.end(); + } + }, + 20_000, + ); }); From bfa60338cc7585d91f50f1c4d9ba05299c91980d Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 19:24:51 -0500 Subject: [PATCH 11/18] Cap dev-runner output buffering Co-Authored-By: Paperclip --- scripts/dev-runner-output.mjs | 53 +++++++++++++++++ scripts/dev-runner-output.ts | 59 +++++++++++++++++++ scripts/dev-runner.mjs | 16 +++-- scripts/dev-runner.ts | 15 +++-- .../src/__tests__/dev-runner-output.test.ts | 29 +++++++++ 5 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 scripts/dev-runner-output.mjs create mode 100644 scripts/dev-runner-output.ts create mode 100644 server/src/__tests__/dev-runner-output.test.ts diff --git a/scripts/dev-runner-output.mjs b/scripts/dev-runner-output.mjs new file mode 100644 index 00000000..bb0d68df --- /dev/null +++ b/scripts/dev-runner-output.mjs @@ -0,0 +1,53 @@ +const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024; + +export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) { + const limit = Math.max(1, Math.trunc(maxBytes)); + const chunks = []; + let bufferedBytes = 0; + let totalBytes = 0; + let truncated = false; + + return { + append(chunk) { + if (chunk === null || chunk === undefined) return; + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + if (buffer.length === 0) return; + + chunks.push(buffer); + bufferedBytes += buffer.length; + totalBytes += buffer.length; + + while (bufferedBytes > limit && chunks.length > 0) { + const overflow = bufferedBytes - limit; + const head = chunks[0]; + if (head.length <= overflow) { + chunks.shift(); + bufferedBytes -= head.length; + truncated = true; + continue; + } + + chunks[0] = head.subarray(overflow); + bufferedBytes -= overflow; + truncated = true; + } + }, + + finish() { + const body = Buffer.concat(chunks).toString("utf8"); + if (!truncated) { + return { + text: body, + truncated, + totalBytes, + }; + } + + return { + text: `[output truncated to last ${limit} bytes; total ${totalBytes} bytes]\n${body}`, + truncated, + totalBytes, + }; + }, + }; +} diff --git a/scripts/dev-runner-output.ts b/scripts/dev-runner-output.ts new file mode 100644 index 00000000..6fdc8cbd --- /dev/null +++ b/scripts/dev-runner-output.ts @@ -0,0 +1,59 @@ +const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024; + +export type CapturedOutput = { + text: string; + truncated: boolean; + totalBytes: number; +}; + +export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) { + const limit = Math.max(1, Math.trunc(maxBytes)); + const chunks: Buffer[] = []; + let bufferedBytes = 0; + let totalBytes = 0; + let truncated = false; + + return { + append(chunk: Buffer | string | null | undefined) { + if (chunk === null || chunk === undefined) return; + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + if (buffer.length === 0) return; + + chunks.push(buffer); + bufferedBytes += buffer.length; + totalBytes += buffer.length; + + while (bufferedBytes > limit && chunks.length > 0) { + const overflow = bufferedBytes - limit; + const head = chunks[0]!; + if (head.length <= overflow) { + chunks.shift(); + bufferedBytes -= head.length; + truncated = true; + continue; + } + + chunks[0] = head.subarray(overflow); + bufferedBytes -= overflow; + truncated = true; + } + }, + + finish(): CapturedOutput { + const body = Buffer.concat(chunks).toString("utf8"); + if (!truncated) { + return { + text: body, + truncated, + totalBytes, + }; + } + + return { + text: `[output truncated to last ${limit} bytes; total ${totalBytes} bytes]\n${body}`, + truncated, + totalBytes, + }; + }, + }; +} diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 091dbb19..8273a54c 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -5,6 +5,7 @@ import path from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; import { fileURLToPath } from "node:url"; +import { createCapturedOutputBuffer } from "./dev-runner-output.mjs"; import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs"; const mode = process.argv[2] === "watch" ? "watch" : "dev"; @@ -250,30 +251,33 @@ async function runPnpm(args, options = {}) { const spawned = spawn(pnpmBin, args, { stdio: options.stdio ?? ["ignore", "pipe", "pipe"], env: options.env ?? process.env, + cwd: options.cwd, shell: process.platform === "win32", }); - let stdoutBuffer = ""; - let stderrBuffer = ""; + const stdoutBuffer = createCapturedOutputBuffer(); + const stderrBuffer = createCapturedOutputBuffer(); if (spawned.stdout) { spawned.stdout.on("data", (chunk) => { - stdoutBuffer += String(chunk); + stdoutBuffer.append(chunk); }); } if (spawned.stderr) { spawned.stderr.on("data", (chunk) => { - stderrBuffer += String(chunk); + stderrBuffer.append(chunk); }); } spawned.on("error", reject); spawned.on("exit", (code, signal) => { + const stdout = stdoutBuffer.finish(); + const stderr = stderrBuffer.finish(); resolve({ code: code ?? 0, signal, - stdout: stdoutBuffer, - stderr: stderrBuffer, + stdout: stdout.text, + stderr: stderr.text, }); }); }); diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index aed49c1b..fc4165b7 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } f import path from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; +import { createCapturedOutputBuffer } from "./dev-runner-output.mjs"; import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs"; import { createDevServiceIdentity, repoRoot } from "./dev-service-profile.ts"; import { @@ -315,27 +316,29 @@ async function runPnpm(args: string[], options: { shell: process.platform === "win32", }); - let stdoutBuffer = ""; - let stderrBuffer = ""; + const stdoutBuffer = createCapturedOutputBuffer(); + const stderrBuffer = createCapturedOutputBuffer(); if (spawned.stdout) { spawned.stdout.on("data", (chunk) => { - stdoutBuffer += String(chunk); + stdoutBuffer.append(chunk); }); } if (spawned.stderr) { spawned.stderr.on("data", (chunk) => { - stderrBuffer += String(chunk); + stderrBuffer.append(chunk); }); } spawned.on("error", reject); spawned.on("exit", (code, signal) => { + const stdout = stdoutBuffer.finish(); + const stderr = stderrBuffer.finish(); resolve({ code: code ?? 0, signal, - stdout: stdoutBuffer, - stderr: stderrBuffer, + stdout: stdout.text, + stderr: stderr.text, }); }); }); diff --git a/server/src/__tests__/dev-runner-output.test.ts b/server/src/__tests__/dev-runner-output.test.ts new file mode 100644 index 00000000..9e3f49b7 --- /dev/null +++ b/server/src/__tests__/dev-runner-output.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { createCapturedOutputBuffer } from "../../../scripts/dev-runner-output.mjs"; + +describe("createCapturedOutputBuffer", () => { + it("keeps small output unchanged", () => { + const capture = createCapturedOutputBuffer(32); + capture.append("hello"); + capture.append(" world"); + + expect(capture.finish()).toEqual({ + text: "hello world", + totalBytes: 11, + truncated: false, + }); + }); + + it("retains only the bounded tail when output grows large", () => { + const capture = createCapturedOutputBuffer(8); + capture.append("abcd"); + capture.append(Buffer.from("efgh")); + capture.append("ijkl"); + + const result = capture.finish(); + expect(result.truncated).toBe(true); + expect(result.totalBytes).toBe(12); + expect(result.text).toContain("total 12 bytes"); + expect(result.text.endsWith("efghijkl")).toBe(true); + }); +}); From 9a8a169e95f362f824a592e84a38bbb62ff08379 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 20:17:47 -0500 Subject: [PATCH 12/18] Guard dev health JSON parsing Co-Authored-By: Paperclip --- scripts/dev-runner-output.mjs | 42 ++++++++++++++++- scripts/dev-runner-output.ts | 45 ++++++++++++++++++- scripts/dev-runner.mjs | 4 +- scripts/dev-runner.ts | 4 +- .../src/__tests__/dev-runner-output.test.ts | 18 +++++++- .../src/__tests__/dev-server-status.test.ts | 10 +++++ server/src/dev-server-status.ts | 7 ++- 7 files changed, 122 insertions(+), 8 deletions(-) diff --git a/scripts/dev-runner-output.mjs b/scripts/dev-runner-output.mjs index bb0d68df..50f5076d 100644 --- a/scripts/dev-runner-output.mjs +++ b/scripts/dev-runner-output.mjs @@ -1,7 +1,12 @@ const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024; +const DEFAULT_JSON_RESPONSE_BYTES = 64 * 1024; + +function normalizeByteLimit(maxBytes) { + return Math.max(1, Math.trunc(maxBytes)); +} export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) { - const limit = Math.max(1, Math.trunc(maxBytes)); + const limit = normalizeByteLimit(maxBytes); const chunks = []; let bufferedBytes = 0; let totalBytes = 0; @@ -51,3 +56,38 @@ export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BY }, }; } + +export async function parseJsonResponseWithLimit(response, maxBytes = DEFAULT_JSON_RESPONSE_BYTES) { + const limit = normalizeByteLimit(maxBytes); + const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10); + if (Number.isFinite(contentLength) && contentLength > limit) { + throw new Error(`Response exceeds ${limit} bytes`); + } + + if (!response.body) { + return JSON.parse(""); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let text = ""; + let totalBytes = 0; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > limit) { + await reader.cancel("response too large"); + throw new Error(`Response exceeds ${limit} bytes`); + } + text += decoder.decode(value, { stream: true }); + } + text += decoder.decode(); + } finally { + reader.releaseLock(); + } + + return JSON.parse(text); +} diff --git a/scripts/dev-runner-output.ts b/scripts/dev-runner-output.ts index 6fdc8cbd..213dde41 100644 --- a/scripts/dev-runner-output.ts +++ b/scripts/dev-runner-output.ts @@ -1,4 +1,5 @@ const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024; +const DEFAULT_JSON_RESPONSE_BYTES = 64 * 1024; export type CapturedOutput = { text: string; @@ -6,8 +7,12 @@ export type CapturedOutput = { totalBytes: number; }; +function normalizeByteLimit(maxBytes: number) { + return Math.max(1, Math.trunc(maxBytes)); +} + export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) { - const limit = Math.max(1, Math.trunc(maxBytes)); + const limit = normalizeByteLimit(maxBytes); const chunks: Buffer[] = []; let bufferedBytes = 0; let totalBytes = 0; @@ -57,3 +62,41 @@ export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BY }, }; } + +export async function parseJsonResponseWithLimit( + response: Response, + maxBytes = DEFAULT_JSON_RESPONSE_BYTES, +): Promise { + const limit = normalizeByteLimit(maxBytes); + const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10); + if (Number.isFinite(contentLength) && contentLength > limit) { + throw new Error(`Response exceeds ${limit} bytes`); + } + + if (!response.body) { + return JSON.parse("") as T; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let text = ""; + let totalBytes = 0; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > limit) { + await reader.cancel("response too large"); + throw new Error(`Response exceeds ${limit} bytes`); + } + text += decoder.decode(value, { stream: true }); + } + text += decoder.decode(); + } finally { + reader.releaseLock(); + } + + return JSON.parse(text) as T; +} diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 8273a54c..4f4f7c90 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -5,7 +5,7 @@ import path from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; import { fileURLToPath } from "node:url"; -import { createCapturedOutputBuffer } from "./dev-runner-output.mjs"; +import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "./dev-runner-output.mjs"; import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs"; const mode = process.argv[2] === "watch" ? "watch" : "dev"; @@ -430,7 +430,7 @@ async function getDevHealthPayload() { if (!response.ok) { throw new Error(`Health request failed (${response.status})`); } - return await response.json(); + return await parseJsonResponseWithLimit(response); } async function waitForChildExit() { diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index fc4165b7..756a6b92 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } f import path from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; -import { createCapturedOutputBuffer } from "./dev-runner-output.mjs"; +import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "./dev-runner-output.mjs"; import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs"; import { createDevServiceIdentity, repoRoot } from "./dev-service-profile.ts"; import { @@ -487,7 +487,7 @@ async function getDevHealthPayload() { if (!response.ok) { throw new Error(`Health request failed (${response.status})`); } - return await response.json(); + return await parseJsonResponseWithLimit<{ devServer?: { enabled?: boolean; autoRestartEnabled?: boolean; activeRunCount?: number } }>(response); } async function waitForChildExit() { diff --git a/server/src/__tests__/dev-runner-output.test.ts b/server/src/__tests__/dev-runner-output.test.ts index 9e3f49b7..024317a0 100644 --- a/server/src/__tests__/dev-runner-output.test.ts +++ b/server/src/__tests__/dev-runner-output.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createCapturedOutputBuffer } from "../../../scripts/dev-runner-output.mjs"; +import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "../../../scripts/dev-runner-output.mjs"; describe("createCapturedOutputBuffer", () => { it("keeps small output unchanged", () => { @@ -26,4 +26,20 @@ describe("createCapturedOutputBuffer", () => { expect(result.text).toContain("total 12 bytes"); expect(result.text.endsWith("efghijkl")).toBe(true); }); + + it("parses bounded JSON responses", async () => { + const response = new Response(JSON.stringify({ ok: true }), { + headers: { "content-type": "application/json" }, + }); + + await expect(parseJsonResponseWithLimit<{ ok: boolean }>(response, 64)).resolves.toEqual({ ok: true }); + }); + + it("rejects oversized JSON responses before parsing them", async () => { + const response = new Response(JSON.stringify({ payload: "x".repeat(128) }), { + headers: { "content-type": "application/json" }, + }); + + await expect(parseJsonResponseWithLimit(response, 32)).rejects.toThrow("Response exceeds 32 bytes"); + }); }); diff --git a/server/src/__tests__/dev-server-status.test.ts b/server/src/__tests__/dev-server-status.test.ts index d178f941..52eef387 100644 --- a/server/src/__tests__/dev-server-status.test.ts +++ b/server/src/__tests__/dev-server-status.test.ts @@ -63,4 +63,14 @@ describe("dev server status helpers", () => { waitingForIdle: true, }); }); + + it("ignores oversized persisted status files", () => { + const filePath = createTempStatusFile({ + dirty: true, + changedPathsSample: ["x".repeat(70 * 1024)], + pendingMigrations: [], + }); + + expect(readPersistedDevServerStatus({ PAPERCLIP_DEV_SERVER_STATUS_FILE: filePath })).toBeNull(); + }); }); diff --git a/server/src/dev-server-status.ts b/server/src/dev-server-status.ts index aecb0fc9..ec78bfe8 100644 --- a/server/src/dev-server-status.ts +++ b/server/src/dev-server-status.ts @@ -1,4 +1,6 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; + +const MAX_PERSISTED_DEV_SERVER_STATUS_BYTES = 64 * 1024; export type PersistedDevServerStatus = { dirty: boolean; @@ -44,6 +46,9 @@ export function readPersistedDevServerStatus( if (!filePath || !existsSync(filePath)) return null; try { + if (statSync(filePath).size > MAX_PERSISTED_DEV_SERVER_STATUS_BYTES) { + return null; + } const raw = JSON.parse(readFileSync(filePath, "utf8")) as Record; const changedPathsSample = normalizeStringArray(raw.changedPathsSample).slice(0, 5); const pendingMigrations = normalizeStringArray(raw.pendingMigrations); From 1a3aee9ee193a5843bb63860c7b60a29b099492e Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 20:18:23 -0500 Subject: [PATCH 13/18] docs: add smart model routing plan Co-Authored-By: Paperclip --- doc/plans/2026-04-06-smart-model-routing.md | 362 ++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 doc/plans/2026-04-06-smart-model-routing.md diff --git a/doc/plans/2026-04-06-smart-model-routing.md b/doc/plans/2026-04-06-smart-model-routing.md new file mode 100644 index 00000000..cf26913c --- /dev/null +++ b/doc/plans/2026-04-06-smart-model-routing.md @@ -0,0 +1,362 @@ +# 2026-04-06 Smart Model Routing + +Status: Proposed +Date: 2026-04-06 +Audience: Product and engineering +Related: +- `doc/SPEC-implementation.md` +- `doc/PRODUCT.md` +- `doc/plans/2026-03-14-adapter-skill-sync-rollout.md` + +## 1. Purpose + +This document defines a V1 plan for "smart model routing" in Paperclip. + +The goal is not to build a generic cross-provider router in the server. The goal is: + +- let supported adapters use a cheaper model for lightweight heartbeat orchestration work +- keep the main task execution on the adapter's normal primary model +- preserve Paperclip's existing task, session, and audit invariants +- report cost and model usage truthfully when more than one model participates in a single heartbeat + +The motivating use case is a local coding adapter where a cheap model can handle the first fast pass: + +- read the wake context +- orient to the task and workspace +- leave an immediate progress comment when appropriate +- perform bounded lightweight triage + +Then the primary model does the substantive work. + +## 2. Hermes Findings + +Hermes does have a real "smart model routing" feature, but it is narrower than the name suggests. + +Observed behavior: + +- `agent/smart_model_routing.py` implements a conservative classifier for "simple" turns +- the cheap path only triggers for short, single-line, non-code, non-URL, non-tool-heavy messages +- complexity is detected with hardcoded thresholds plus a keyword denylist like `debug`, `implement`, `test`, `plan`, `tool`, `docker`, and similar terms +- if the cheap route cannot be resolved, Hermes silently falls back to the primary model + +Important architectural detail: + +- Hermes applies this routing before constructing the agent for that turn +- the route is resolved in `cron/scheduler.py` and passed into agent creation as the active provider/model/runtime + +More useful than the routing heuristic itself is Hermes' broader model-slot design: + +- main conversational model +- fallback model for failover +- auxiliary model slots for side tasks like compression and classification + +That separation is a better fit for Paperclip than copying Hermes' exact keyword heuristic. + +## 3. Current Paperclip State + +Paperclip already has the right execution shape for adapter-specific routing, but it currently assumes one model per heartbeat run. + +Current implementation facts: + +- `server/src/services/heartbeat.ts` builds rich run context, including `paperclipWake`, workspace metadata, and session handoff context +- each adapter receives a single resolved `config` object and executes once +- built-in local adapters read one `config.model` and pass it directly to the underlying CLI +- UI config today exposes one main `model` field plus adapter-specific thinking-effort controls +- cost accounting currently records one provider/model tuple per run via `AdapterExecutionResult` + +What this means: + +- there is no shared routing layer in the server today +- model choice already lives at the adapter boundary, which is good +- multi-model execution in a single heartbeat needs explicit contract work or cost reporting will become misleading + +## 4. Product Decision + +Paperclip should implement smart model routing as an adapter-local, opt-in execution pattern. + +V1 decision: + +1. Do not add a global server-side router that tries to understand every adapter. +2. Do not copy Hermes' prompt-keyword classifier as Paperclip's default routing policy. +3. Add an adapter-specific "cheap preflight" phase for supported adapters. +4. Keep the primary model as the canonical work model. +5. Persist only the primary session unless an adapter can prove that cross-model session resume is safe. + +Rationale: + +- Paperclip heartbeats are structured, issue-scoped, and already include wake metadata +- routing by execution phase is more reliable than routing by free-text prompt complexity +- session semantics differ by adapter, so resume behavior must stay adapter-owned + +## 5. Proposed V1 Behavior + +## 5.1 Config shape + +Supported adapters should add an optional routing block to `adapterConfig`. + +Proposed shape: + +```ts +smartModelRouting?: { + enabled: boolean; + cheapModel: string; + cheapThinkingEffort?: string; + maxPreflightTurns?: number; + allowInitialProgressComment?: boolean; +} +``` + +Notes: + +- keep existing `model` as the primary model +- `cheapModel` is adapter-specific, not global +- adapters that cannot safely support this block simply ignore it + +For adapters with provider-specific model fields later, the shape can expand to include provider/base-url overrides. V1 should start simple. + +## 5.2 Routing policy + +Supported adapters should run cheap preflight only when all are true: + +- `smartModelRouting.enabled` is true +- `cheapModel` is configured +- the run is issue-scoped +- the adapter is starting a fresh session, not resuming a persisted one +- the run is expected to do real task work rather than just resume an existing thread + +Supported adapters should skip cheap preflight when any are true: + +- a persisted task session already exists +- the adapter cannot safely isolate preflight from the primary session +- the issue or wake type implies the task is already mid-flight and continuity matters more than first-response speed + +This is intentionally phase-based, not text-heuristic-based. + +## 5.3 Cheap preflight responsibilities + +The cheap phase should be narrow and bounded. + +Allowed responsibilities: + +- ingest wake context and issue summary +- inspect the workspace at a shallow level +- leave a short "starting investigation" style comment when appropriate +- collect a compact handoff summary for the primary phase + +Not allowed in V1: + +- long tool loops +- risky file mutations +- being the canonical persisted task session +- deciding final completion without either explicit adapter support or a trivial success case + +Implementation detail: + +- the adapter should inject an explicit preflight prompt telling the model this is a bounded orchestration pass +- preflight should use a very small turn budget, for example 1-2 turns + +## 5.4 Primary execution responsibilities + +After preflight, the adapter launches the normal primary execution using the existing prompt and primary model. + +The primary phase should receive: + +- the normal Paperclip prompt +- any preflight-generated handoff summary +- normal workspace and wake context + +The primary phase remains the source of truth for: + +- persisted session state +- final task completion +- most file changes +- most cost + +## 6. Required Contract Changes + +The current `AdapterExecutionResult` is too narrow for truthful multi-model accounting. + +Add an optional segmented execution report, for example: + +```ts +executionSegments?: Array<{ + phase: "cheap_preflight" | "primary"; + provider?: string | null; + biller?: string | null; + model?: string | null; + billingType?: AdapterBillingType | null; + usage?: UsageSummary; + costUsd?: number | null; + summary?: string | null; +}> +``` + +V1 server behavior: + +- if `executionSegments` is absent, keep current single-result behavior unchanged +- if present, write one `cost_events` row per segment that has cost or token usage +- store the segment array in run usage/result metadata for later UI inspection +- keep the existing top-level `provider` / `model` fields as a summary, preferably the primary phase when present + +This avoids breaking existing adapters while giving routed adapters truthful reporting. + +## 7. Adapter Rollout Plan + +## 7.1 Phase 1: contract and server plumbing + +Work: + +1. Extend adapter result types with segmented execution metadata. +2. Update heartbeat cost recording to emit multiple cost events when segments are present. +3. Include segment summaries in run metadata for transcript/debug views. + +Success criteria: + +- existing adapters behave exactly as before +- a routed adapter can report cheap plus primary usage without collapsing them into one fake model + +## 7.2 Phase 2: `codex_local` + +Why first: + +- Codex already has rich prompt/handoff handling +- the adapter already injects Paperclip skills and workspace metadata cleanly +- the current implementation already distinguishes bootstrap, wake delta, and handoff prompt sections + +Implementation work: + +1. Add config support for `smartModelRouting`. +2. Add a cheap-preflight prompt builder. +3. Run cheap preflight only on fresh sessions. +4. Pass a compact preflight handoff note into the primary prompt. +5. Report segmented usage and model metadata. + +Important guardrail: + +- do not resume the cheap-model session as the primary session in V1 + +## 7.3 Phase 3: `claude_local` + +Implementation work is similar, but the session model-switch risk is even less attractive. + +Same rule: + +- cheap preflight is ephemeral +- primary Claude session remains canonical + +## 7.4 Phase 4: other adapters + +Candidates: + +- `cursor` +- `gemini_local` +- `opencode_local` +- external plugin adapters through `createServerAdapter()` + +These should come later because each runtime has different session and model-switch semantics. + +## 8. UI and Config Changes + +For supported built-in adapters, the agent config UI should expose: + +- `model` as the primary model +- `smart model routing` toggle +- `cheap model` +- optional cheap thinking effort +- optional `allow initial progress comment` toggle + +The run detail UI should also show when routing occurred, for example: + +- cheap preflight model +- primary model +- token/cost split + +This matters because Paperclip's board UI is supposed to make cost and behavior legible. + +## 9. Why Not Copy Hermes Exactly + +Hermes' cheap-route heuristic is useful precedent, but Paperclip should not start there. + +Reasons: + +- Hermes is optimizing free-form conversational turns +- Paperclip agents run structured, issue-scoped heartbeats with explicit task and workspace context +- Paperclip already knows whether a run is fresh vs resumed, issue-scoped vs approval follow-up, and what workspace/session exists +- those execution facts are stronger routing signals than prompt keyword matching + +If Paperclip later wants a cheap-only completion path for trivial runs, that can be a second-stage feature built on observed run data, not the first implementation. + +## 10. Risks + +## 10.1 Duplicate or noisy comments + +If the cheap phase posts an update and the primary phase posts another near-identical update, the issue thread gets worse. + +Mitigation: + +- keep cheap comments optional +- make the preflight prompt explicitly avoid repeating status if a useful comment was already posted + +## 10.2 Misleading cost reporting + +If we only record the primary model, the board loses visibility into the routing cost tradeoff. + +Mitigation: + +- add segmented execution reporting before shipping adapter behavior + +## 10.3 Session corruption + +Cross-model session reuse may fail or degrade context quality. + +Mitigation: + +- V1 does not persist or resume cheap preflight sessions + +## 10.4 Cheap model overreach + +A cheap model with full tools and permissions may do too much low-quality work. + +Mitigation: + +- hard cap preflight turns +- use an explicit orchestration-only prompt +- start with supported adapters where we can test the behavior well + +## 11. Verification Plan + +Required tests: + +- adapter unit tests for route eligibility +- adapter unit tests for "fresh session -> cheap preflight + primary" +- adapter unit tests for "resumed session -> primary only" +- heartbeat tests for segmented cost-event creation +- UI tests for config save/load of cheap-model fields + +Manual checks: + +- create a fresh issue for a routed Codex or Claude agent +- verify the run metadata shows both phases +- verify only the primary session is persisted +- verify cost rows reflect both models +- verify the issue thread does not get duplicate kickoff comments + +## 12. Recommended Sequence + +1. Add segmented execution reporting to the adapter/server contract. +2. Implement `codex_local` cheap preflight. +3. Validate cost visibility and transcript UX. +4. Implement `claude_local` cheap preflight. +5. Decide later whether any adapters need Hermes-style text heuristics in addition to phase-based routing. + +## 13. Recommendation + +Paperclip should ship smart model routing as: + +- adapter-specific +- opt-in +- phase-based +- session-safe +- cost-truthful + +The right V1 is not "choose the cheapest model for simple prompts." The right V1 is "use a cheap model for bounded orchestration work on fresh runs, then hand off to the primary model for the real task." From 492e49e1c0b3e1513f4ed150187df86dee7e62d5 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 21:31:58 -0500 Subject: [PATCH 14/18] test(cli): cover project env in import preview fixtures Co-Authored-By: Paperclip --- cli/src/__tests__/company.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 268d1266..2d47d8b5 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -220,6 +220,7 @@ describe("renderCompanyImportPreview", () => { status: null, executionWorkspacePolicy: null, workspaces: [], + env: null, metadata: null, }, ], @@ -432,6 +433,7 @@ describe("import selection catalog", () => { status: null, executionWorkspacePolicy: null, workspaces: [], + env: null, metadata: null, }, ], From 9a150eee657c17ea3d214154ef01e6e6657b6d3f Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 21:50:11 -0500 Subject: [PATCH 15/18] fix(ui): remove runtime-only preflight hook dependency Co-Authored-By: Paperclip --- ui/package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/package.json b/ui/package.json index 094b7162..d344f67a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,11 +14,10 @@ }, "type": "module", "scripts": { - "preflight:workspace-links": "tsx ../scripts/ensure-workspace-package-links.ts", - "dev": "pnpm run preflight:workspace-links && vite", - "build": "pnpm run preflight:workspace-links && tsc -b && vite build", + "dev": "vite", + "build": "tsc -b && vite build", "preview": "vite preview", - "typecheck": "pnpm run preflight:workspace-links && tsc -b", + "typecheck": "tsc -b", "clean": "rm -rf dist tsconfig.tsbuildinfo", "prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../scripts/generate-ui-package-json.mjs", "postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi" From 1de139341353328ab92936b639e3a76fbf16efe6 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 21:56:13 -0500 Subject: [PATCH 16/18] fix(runtime): handle empty dev runner responses --- scripts/dev-runner-output.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev-runner-output.ts b/scripts/dev-runner-output.ts index 213dde41..029e7bfe 100644 --- a/scripts/dev-runner-output.ts +++ b/scripts/dev-runner-output.ts @@ -74,7 +74,7 @@ export async function parseJsonResponseWithLimit( } if (!response.body) { - return JSON.parse("") as T; + throw new Error("Response has no body"); } const reader = response.body.getReader(); From 48704c658668ce45dcf092bf12761f99c019360d Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 06:32:52 -0500 Subject: [PATCH 17/18] fix(export): strip project env values from company packages --- .../shared/src/types/company-portability.ts | 1 + .../src/validators/company-portability.ts | 1 + .../src/__tests__/company-portability.test.ts | 124 ++++++++++ server/src/services/company-portability.ts | 213 ++++++++++++------ 4 files changed, 275 insertions(+), 64 deletions(-) diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index 457e1998..356c620d 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -13,6 +13,7 @@ export interface CompanyPortabilityEnvInput { key: string; description: string | null; agentSlug: string | null; + projectSlug: string | null; kind: "secret" | "plain"; requirement: "required" | "optional"; defaultValue: string | null; diff --git a/packages/shared/src/validators/company-portability.ts b/packages/shared/src/validators/company-portability.ts index 97004b35..952c9cce 100644 --- a/packages/shared/src/validators/company-portability.ts +++ b/packages/shared/src/validators/company-portability.ts @@ -15,6 +15,7 @@ export const portabilityEnvInputSchema = z.object({ key: z.string().min(1), description: z.string().nullable(), agentSlug: z.string().min(1).nullable(), + projectSlug: z.string().min(1).nullable(), kind: z.enum(["secret", "plain"]), requirement: z.enum(["required", "optional"]), defaultValue: z.string().nullable(), diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index d14ae8c0..b8bf822d 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -1149,6 +1149,7 @@ describe("company portability", () => { key: "ANTHROPIC_API_KEY", description: "Provide ANTHROPIC_API_KEY for agent claudecoder", agentSlug: "claudecoder", + projectSlug: null, kind: "secret", requirement: "optional", defaultValue: "", @@ -1158,6 +1159,7 @@ describe("company portability", () => { key: "GH_TOKEN", description: "Provide GH_TOKEN for agent claudecoder", agentSlug: "claudecoder", + projectSlug: null, kind: "secret", requirement: "optional", defaultValue: "", @@ -1166,6 +1168,128 @@ describe("company portability", () => { ]); }); + it("exports project env as portable inputs without concrete values", async () => { + const portability = companyPortabilityService({} as any); + + projectSvc.list.mockResolvedValue([ + { + id: "project-1", + name: "Launch", + urlKey: "launch", + description: "Ship it", + leadAgentId: "agent-1", + targetDate: null, + color: null, + status: "planned", + env: { + OPENAI_API_KEY: { + type: "plain", + value: "sk-project-secret", + }, + DOCS_MODE: { + type: "plain", + value: "strict", + }, + GITHUB_TOKEN: { + type: "secret_ref", + secretId: "11111111-1111-1111-1111-111111111111", + version: "latest", + }, + }, + executionWorkspacePolicy: null, + workspaces: [], + metadata: null, + }, + ]); + + const exported = await portability.exportBundle("company-1", { + include: { + company: false, + agents: false, + projects: true, + issues: false, + }, + }); + + const extension = asTextFile(exported.files[".paperclip.yaml"]); + expect(extension).toContain("OPENAI_API_KEY:"); + expect(extension).toContain("DOCS_MODE:"); + expect(extension).toContain("GITHUB_TOKEN:"); + expect(extension).not.toContain("sk-project-secret"); + expect(extension).not.toContain('type: "secret_ref"'); + expect(extension).not.toContain("11111111-1111-1111-1111-111111111111"); + expect(extension).toContain('default: "strict"'); + expect(extension).toContain('kind: "secret"'); + expect(extension).toContain('kind: "plain"'); + }); + + it("reads project env inputs back from .paperclip.yaml during preview import", async () => { + const portability = companyPortabilityService({} as any); + + projectSvc.list.mockResolvedValue([ + { + id: "project-1", + name: "Launch", + urlKey: "launch", + description: "Ship it", + leadAgentId: "agent-1", + targetDate: null, + color: null, + status: "planned", + env: { + OPENAI_API_KEY: { + type: "plain", + value: "sk-project-secret", + }, + }, + executionWorkspacePolicy: null, + workspaces: [], + metadata: null, + }, + ]); + + const exported = await portability.exportBundle("company-1", { + include: { + company: false, + agents: false, + projects: true, + issues: false, + }, + }); + + const preview = await portability.previewImport({ + source: { + type: "inline", + rootPath: exported.rootPath, + files: exported.files, + }, + include: { + company: false, + agents: false, + projects: true, + issues: false, + }, + target: { + mode: "new_company", + newCompanyName: "Imported Paperclip", + }, + agents: "all", + collisionStrategy: "rename", + }); + + expect(preview.errors).toEqual([]); + expect(preview.envInputs).toContainEqual({ + key: "OPENAI_API_KEY", + description: "Optional default for OPENAI_API_KEY on project launch", + agentSlug: null, + projectSlug: "launch", + kind: "secret", + requirement: "optional", + defaultValue: "", + portability: "portable", + }); + }); + it("exports routines as recurring task packages with Paperclip routine extensions", async () => { const portability = companyPortabilityService({} as any); diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index b133411b..1c0fc90e 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -394,6 +394,83 @@ function normalizePortableProjectEnv(value: unknown): AgentEnvConfig | null { return parsed.success ? parsed.data : null; } +function extractPortableScopedEnvInputs( + scope: { + label: string; + warningPrefix: string; + agentSlug: string | null; + projectSlug: string | null; + }, + envValue: unknown, + warnings: string[], +): CompanyPortabilityEnvInput[] { + if (!isPlainRecord(envValue)) return []; + const env = envValue as Record; + const inputs: CompanyPortabilityEnvInput[] = []; + + for (const [key, binding] of Object.entries(env)) { + if (key.toUpperCase() === "PATH") { + warnings.push(`${scope.warningPrefix} PATH override was omitted from export because it is system-dependent.`); + continue; + } + + if (isPlainRecord(binding) && binding.type === "secret_ref") { + inputs.push({ + key, + description: `Provide ${key} for ${scope.label}`, + agentSlug: scope.agentSlug, + projectSlug: scope.projectSlug, + kind: "secret", + requirement: "optional", + defaultValue: "", + portability: "portable", + }); + continue; + } + + if (isPlainRecord(binding) && binding.type === "plain") { + const defaultValue = asString(binding.value); + const isSensitive = isSensitiveEnvKey(key); + const portability = defaultValue && isAbsoluteCommand(defaultValue) + ? "system_dependent" + : "portable"; + if (portability === "system_dependent") { + warnings.push(`${scope.warningPrefix} env ${key} default was exported as system-dependent.`); + } + inputs.push({ + key, + description: `Optional default for ${key} on ${scope.label}`, + agentSlug: scope.agentSlug, + projectSlug: scope.projectSlug, + kind: isSensitive ? "secret" : "plain", + requirement: "optional", + defaultValue: isSensitive ? "" : defaultValue ?? "", + portability, + }); + continue; + } + + if (typeof binding === "string") { + const portability = isAbsoluteCommand(binding) ? "system_dependent" : "portable"; + if (portability === "system_dependent") { + warnings.push(`${scope.warningPrefix} env ${key} default was exported as system-dependent.`); + } + inputs.push({ + key, + description: `Optional default for ${key} on ${scope.label}`, + agentSlug: scope.agentSlug, + projectSlug: scope.projectSlug, + kind: isSensitiveEnvKey(key) ? "secret" : "plain", + requirement: "optional", + defaultValue: isSensitiveEnvKey(key) ? "" : binding, + portability, + }); + } + } + + return inputs; +} + type ResolvedSource = { manifest: CompanyPortabilityManifest; files: Record; @@ -1536,68 +1613,33 @@ function extractPortableEnvInputs( envValue: unknown, warnings: string[], ): CompanyPortabilityEnvInput[] { - if (!isPlainRecord(envValue)) return []; - const env = envValue as Record; - const inputs: CompanyPortabilityEnvInput[] = []; + return extractPortableScopedEnvInputs( + { + label: `agent ${agentSlug}`, + warningPrefix: `Agent ${agentSlug}`, + agentSlug, + projectSlug: null, + }, + envValue, + warnings, + ); +} - for (const [key, binding] of Object.entries(env)) { - if (key.toUpperCase() === "PATH") { - warnings.push(`Agent ${agentSlug} PATH override was omitted from export because it is system-dependent.`); - continue; - } - - if (isPlainRecord(binding) && binding.type === "secret_ref") { - inputs.push({ - key, - description: `Provide ${key} for agent ${agentSlug}`, - agentSlug, - kind: "secret", - requirement: "optional", - defaultValue: "", - portability: "portable", - }); - continue; - } - - if (isPlainRecord(binding) && binding.type === "plain") { - const defaultValue = asString(binding.value); - const isSensitive = isSensitiveEnvKey(key); - const portability = defaultValue && isAbsoluteCommand(defaultValue) - ? "system_dependent" - : "portable"; - if (portability === "system_dependent") { - warnings.push(`Agent ${agentSlug} env ${key} default was exported as system-dependent.`); - } - inputs.push({ - key, - description: `Optional default for ${key} on agent ${agentSlug}`, - agentSlug, - kind: isSensitive ? "secret" : "plain", - requirement: "optional", - defaultValue: isSensitive ? "" : defaultValue ?? "", - portability, - }); - continue; - } - - if (typeof binding === "string") { - const portability = isAbsoluteCommand(binding) ? "system_dependent" : "portable"; - if (portability === "system_dependent") { - warnings.push(`Agent ${agentSlug} env ${key} default was exported as system-dependent.`); - } - inputs.push({ - key, - description: `Optional default for ${key} on agent ${agentSlug}`, - agentSlug, - kind: isSensitiveEnvKey(key) ? "secret" : "plain", - requirement: "optional", - defaultValue: binding, - portability, - }); - } - } - - return inputs; +function extractPortableProjectEnvInputs( + projectSlug: string, + envValue: unknown, + warnings: string[], +): CompanyPortabilityEnvInput[] { + return extractPortableScopedEnvInputs( + { + label: `project ${projectSlug}`, + warningPrefix: `Project ${projectSlug}`, + agentSlug: null, + projectSlug, + }, + envValue, + warnings, + ); } function jsonEqual(left: unknown, right: unknown): boolean { @@ -2183,7 +2225,7 @@ function dedupeEnvInputs(values: CompanyPortabilityManifest["envInputs"]) { const seen = new Set(); const out: CompanyPortabilityManifest["envInputs"] = []; for (const value of values) { - const key = `${value.agentSlug ?? ""}:${value.key.toUpperCase()}`; + const key = `${value.agentSlug ?? ""}:${value.projectSlug ?? ""}:${value.key.toUpperCase()}`; if (seen.has(key)) continue; seen.add(key); out.push(value); @@ -2240,6 +2282,31 @@ function readAgentEnvInputs( key, description: asString(record.description) ?? null, agentSlug, + projectSlug: null, + kind: record.kind === "plain" ? "plain" : "secret", + requirement: record.requirement === "required" ? "required" : "optional", + defaultValue: typeof record.default === "string" ? record.default : null, + portability: record.portability === "system_dependent" ? "system_dependent" : "portable", + }]; + }); +} + +function readProjectEnvInputs( + extension: Record, + projectSlug: string, +): CompanyPortabilityManifest["envInputs"] { + const inputs = isPlainRecord(extension.inputs) ? extension.inputs : null; + const env = inputs && isPlainRecord(inputs.env) ? inputs.env : null; + if (!env) return []; + + return Object.entries(env).flatMap(([key, value]) => { + if (!isPlainRecord(value)) return []; + const record = value as EnvInputRecord; + return [{ + key, + description: asString(record.description) ?? null, + agentSlug: null, + projectSlug, kind: record.kind === "plain" ? "plain" : "secret", requirement: record.requirement === "required" ? "required" : "optional", defaultValue: typeof record.default === "string" ? record.default : null, @@ -2546,6 +2613,7 @@ function buildManifestFromPackageFiles( workspaces, metadata: isPlainRecord(extension.metadata) ? extension.metadata : null, }); + manifest.envInputs.push(...readProjectEnvInputs(extension, slug)); if (frontmatter.kind && frontmatter.kind !== "project") { warnings.push(`Project markdown ${projectPath} does not declare kind: project in frontmatter.`); } @@ -3153,6 +3221,14 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { for (const project of selectedProjectRows) { const slug = projectSlugById.get(project.id)!; const projectPath = `projects/${slug}/PROJECT.md`; + const envInputsStart = envInputs.length; + const exportedEnvInputs = extractPortableProjectEnvInputs(slug, project.env, warnings); + envInputs.push(...exportedEnvInputs); + const projectEnvInputs = dedupeEnvInputs( + envInputs + .slice(envInputsStart) + .filter((inputValue) => inputValue.projectSlug === slug), + ); const portableWorkspaces = await buildPortableProjectWorkspaces(slug, project.workspaces, warnings); projectWorkspaceKeyByProjectId.set(project.id, portableWorkspaces.workspaceKeyById); files[projectPath] = buildMarkdown( @@ -3168,7 +3244,6 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { targetDate: project.targetDate ?? null, color: project.color ?? null, status: project.status, - env: normalizePortableProjectEnv(project.env) ?? undefined, executionWorkspacePolicy: exportPortableProjectExecutionWorkspacePolicy( slug, project.executionWorkspacePolicy, @@ -3177,6 +3252,11 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { ) ?? undefined, workspaces: portableWorkspaces.extension, }); + if (isPlainRecord(extension) && projectEnvInputs.length > 0) { + extension.inputs = { + env: buildEnvInputMap(projectEnvInputs), + }; + } paperclipProjectsOut[slug] = isPlainRecord(extension) ? extension : {}; } @@ -3516,7 +3596,12 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { for (const envInput of manifest.envInputs) { if (envInput.portability === "system_dependent") { - warnings.push(`Environment input ${envInput.key}${envInput.agentSlug ? ` for ${envInput.agentSlug}` : ""} is system-dependent and may need manual adjustment after import.`); + const scope = envInput.agentSlug + ? ` for agent ${envInput.agentSlug}` + : envInput.projectSlug + ? ` for project ${envInput.projectSlug}` + : ""; + warnings.push(`Environment input ${envInput.key}${scope} is system-dependent and may need manual adjustment after import.`); } } From f3e5c55f45f749a6e77b68f026a05bf7487ac362 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 06:59:05 -0500 Subject: [PATCH 18/18] test(cli): align env input fixtures with project scope --- cli/src/__tests__/company.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 2d47d8b5..922e95c7 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -251,6 +251,7 @@ describe("renderCompanyImportPreview", () => { key: "OPENAI_API_KEY", description: null, agentSlug: "ceo", + projectSlug: null, kind: "secret", requirement: "required", defaultValue: null, @@ -266,6 +267,7 @@ describe("renderCompanyImportPreview", () => { key: "OPENAI_API_KEY", description: null, agentSlug: "ceo", + projectSlug: null, kind: "secret", requirement: "required", defaultValue: null,