From f3ad1fc301f5c897182b05cee66f7b8e439328b6 Mon Sep 17 00:00:00 2001
From: lempkey
["']?)(?\.\/[^"'\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/51] 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/51] 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/51] 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/51] 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 ( -- ); -} - 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- 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. -
-| 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 ( ++ ); +} 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 )} ++ 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. +
+} + 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/51] 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/51] 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/51] 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/51] 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/51] 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 bdc8e27bf4173533b3d58d90c0f4816849a688cd Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 4 Apr 2026 19:10:58 -0500 Subject: [PATCH 14/51] Fix mention popup placement and spaced queries Co-Authored-By: Paperclip --- ui/src/components/MarkdownEditor.test.tsx | 29 ++++++- ui/src/components/MarkdownEditor.tsx | 94 ++++++++++++++++------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/ui/src/components/MarkdownEditor.test.tsx b/ui/src/components/MarkdownEditor.test.tsx index 0df20323..a2b18728 100644 --- a/ui/src/components/MarkdownEditor.test.tsx +++ b/ui/src/components/MarkdownEditor.test.tsx @@ -3,7 +3,7 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { computeMentionMenuPosition, MarkdownEditor } from "./MarkdownEditor"; +import { computeMentionMenuPosition, findMentionMatch, MarkdownEditor } from "./MarkdownEditor"; const mdxEditorMockState = vi.hoisted(() => ({ emitMountEmptyReset: false, @@ -186,4 +186,31 @@ describe("MarkdownEditor", () => { left: 92, }); }); + + it("keeps a short mention menu on the same line when it fits below the caret", () => { + expect( + computeMentionMenuPosition( + { viewportTop: 160, viewportLeft: 120 }, + { offsetLeft: 0, offsetTop: 0, width: 320, height: 220 }, + { width: 188, height: 42 }, + ), + ).toEqual({ + top: 164, + left: 120, + }); + }); + + it("keeps mention queries active across spaces", () => { + expect(findMentionMatch("Ping @Paperclip App", "Ping @Paperclip App".length)).toEqual({ + trigger: "mention", + marker: "@", + query: "Paperclip App", + atPos: 5, + endPos: "Ping @Paperclip App".length, + }); + }); + + it("still rejects slash commands once spaces are typed", () => { + expect(findMentionMatch("/open issue", "/open issue".length)).toBeNull(); + }); }); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index d528d493..62f4010d 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -108,9 +108,16 @@ interface MentionMenuViewport { height: number; } +interface MentionMenuSize { + width: number; + height: number; +} + const MENTION_MENU_WIDTH = 188; const MENTION_MENU_HEIGHT = 208; const MENTION_MENU_PADDING = 8; +const MENTION_MENU_ROW_HEIGHT = 34; +const MENTION_MENU_CHROME_HEIGHT = 8; const CODE_BLOCK_LANGUAGES: Record = { txt: "Text", @@ -140,19 +147,10 @@ const FALLBACK_CODE_BLOCK_DESCRIPTOR: CodeBlockEditorDescriptor = { Editor: CodeMirrorEditor, }; -function detectMention(container: HTMLElement): MentionState | null { - const sel = window.getSelection(); - if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null; - - const range = sel.getRangeAt(0); - const textNode = range.startContainer; - if (textNode.nodeType !== Node.TEXT_NODE) return null; - if (!container.contains(textNode)) return null; - - const text = textNode.textContent ?? ""; - const offset = range.startOffset; - - // Walk backwards from cursor to find an autocomplete trigger. +export function findMentionMatch( + text: string, + offset: number, +): Pick | null { let atPos = -1; let trigger: MentionState["trigger"] | null = null; let marker: MentionState["marker"] | null = null; @@ -166,31 +164,54 @@ function detectMention(container: HTMLElement): MentionState | null { } break; } - if (/\s/.test(ch)) break; + if (ch === "\n" || ch === "\r") break; } if (atPos === -1) return null; - const query = text.slice(atPos + 1, offset); - - // Get position relative to container - const tempRange = document.createRange(); - tempRange.setStart(textNode, atPos); - tempRange.setEnd(textNode, atPos + 1); - const rect = tempRange.getBoundingClientRect(); - const containerRect = container.getBoundingClientRect(); + if (trigger === "skill" && /\s/.test(query)) return null; return { trigger: trigger ?? "mention", marker: marker ?? "@", query, + atPos, + endPos: offset, + }; +} + +function detectMention(container: HTMLElement): MentionState | null { + const sel = window.getSelection(); + if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null; + + const range = sel.getRangeAt(0); + const textNode = range.startContainer; + if (textNode.nodeType !== Node.TEXT_NODE) return null; + if (!container.contains(textNode)) return null; + + const text = textNode.textContent ?? ""; + const offset = range.startOffset; + const match = findMentionMatch(text, offset); + if (!match) return null; + + // Get position relative to container + const tempRange = document.createRange(); + tempRange.setStart(textNode, match.atPos); + tempRange.setEnd(textNode, match.atPos + 1); + const rect = tempRange.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + return { + trigger: match.trigger, + marker: match.marker, + query: match.query, top: rect.bottom - containerRect.top, left: rect.left - containerRect.left, viewportTop: rect.bottom, viewportLeft: rect.left, textNode: textNode as Text, - atPos, - endPos: offset, + atPos: match.atPos, + endPos: match.endPos, }; } @@ -216,11 +237,12 @@ function getMentionMenuViewport(): MentionMenuViewport { export function computeMentionMenuPosition( anchor: Pick , viewport: MentionMenuViewport, + menuSize: MentionMenuSize = { width: MENTION_MENU_WIDTH, height: MENTION_MENU_HEIGHT }, ) { const minLeft = viewport.offsetLeft + MENTION_MENU_PADDING; - const maxLeft = viewport.offsetLeft + viewport.width - MENTION_MENU_WIDTH; + const maxLeft = viewport.offsetLeft + viewport.width - menuSize.width; const minTop = viewport.offsetTop + MENTION_MENU_PADDING; - const maxTop = viewport.offsetTop + viewport.height - MENTION_MENU_HEIGHT; + const maxTop = viewport.offsetTop + viewport.height - menuSize.height; return { top: Math.max(minTop, Math.min(viewport.offsetTop + anchor.viewportTop + 4, maxTop)), @@ -228,6 +250,17 @@ export function computeMentionMenuPosition( }; } +function getMentionMenuSize(optionCount: number): MentionMenuSize { + const visibleRows = Math.max(1, Math.min(optionCount, 8)); + return { + width: MENTION_MENU_WIDTH, + height: Math.min( + MENTION_MENU_HEIGHT, + visibleRows * MENTION_MENU_ROW_HEIGHT + MENTION_MENU_CHROME_HEIGHT, + ), + }; +} + function nodeInsideCodeLike(container: HTMLElement, node: Node | null): boolean { if (!node || !container.contains(node)) return false; const el = node.nodeType === Node.ELEMENT_NODE @@ -650,7 +683,11 @@ export const MarkdownEditor = forwardRef }, []); const mentionMenuPosition = mentionState - ? computeMentionMenuPosition(mentionState, getMentionMenuViewport()) + ? computeMentionMenuPosition( + mentionState, + getMentionMenuViewport(), + getMentionMenuSize(filteredMentions.length), + ) : null; return ( @@ -673,8 +710,7 @@ export const MarkdownEditor = forwardRef // Mention keyboard handling if (mentionActive) { - // Space dismisses the popup (let the character be typed normally) - if (e.key === " ") { + if (e.key === " " && mentionStateRef.current?.trigger === "skill") { mentionStateRef.current = null; setMentionState(null); return; From 517fe5093e883784163164fd1c45b765a8de61fb Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 4 Apr 2026 19:57:25 -0500 Subject: [PATCH 15/51] Fix inbox archive flashing back after fade-out The archive mutation was only using CSS opacity to hide items while the network request was in flight. When the query refetch completed or the archiving timer expired, the item could reappear. Now we optimistically remove the item from React Query caches on mutate, snapshot previous data for rollback on error, and sync with the server in onSettled. Co-Authored-By: Paperclip --- ui/src/pages/Inbox.tsx | 49 +++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index b0898f40..45594db7 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -1245,30 +1245,53 @@ export function Inbox() { const archiveIssueMutation = useMutation({ mutationFn: (id: string) => issuesApi.archiveFromInbox(id), - onMutate: (id) => { + onMutate: async (id) => { setActionError(null); setArchivingIssueIds((prev) => new Set(prev).add(id)); + + // Cancel in-flight refetches so they don't overwrite our optimistic update + const queryKeys_ = [ + queryKeys.issues.listMineByMe(selectedCompanyId!), + queryKeys.issues.listTouchedByMe(selectedCompanyId!), + queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId!), + ]; + await Promise.all(queryKeys_.map((qk) => queryClient.cancelQueries({ queryKey: qk }))); + + // Snapshot previous data for rollback + const previousData = queryKeys_.map((qk) => [qk, queryClient.getQueryData(qk)] as const); + + // Optimistically remove the issue from all inbox query caches + for (const qk of queryKeys_) { + queryClient.setQueryData(qk, (old: unknown) => { + if (!Array.isArray(old)) return old; + return old.filter((issue: { id: string }) => issue.id !== id); + }); + } + + return { previousData }; }, - onSuccess: () => { - invalidateInboxIssueQueries(); - }, - onError: (err, id) => { + onError: (err, id, context) => { setActionError(err instanceof Error ? err.message : "Failed to archive issue"); setArchivingIssueIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); + // Restore previous query data on failure + if (context?.previousData) { + for (const [qk, data] of context.previousData) { + queryClient.setQueryData(qk, data); + } + } }, onSettled: (_data, error, id) => { - if (error) return; - window.setTimeout(() => { - setArchivingIssueIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - }, 500); + // Clean up archiving state and refetch to sync with server + setArchivingIssueIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + invalidateInboxIssueQueries(); }, }); From e9c8bd48058c42a06d4317b1e2ed00c30907ea7c Mon Sep 17 00:00:00 2001 From: dotta Date: Sun, 5 Apr 2026 06:29:33 -0500 Subject: [PATCH 16/51] Allow arbitrary issue attachments Co-Authored-By: Paperclip --- server/src/__tests__/attachment-types.test.ts | 32 +++- .../__tests__/issue-attachment-routes.test.ts | 175 ++++++++++++++++++ server/src/attachment-types.ts | 24 ++- server/src/routes/issues.ts | 23 ++- ui/src/pages/IssueDetail.tsx | 1 - 5 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 server/src/__tests__/issue-attachment-routes.test.ts diff --git a/server/src/__tests__/attachment-types.test.ts b/server/src/__tests__/attachment-types.test.ts index 5a430102..7dfc34d2 100644 --- a/server/src/__tests__/attachment-types.test.ts +++ b/server/src/__tests__/attachment-types.test.ts @@ -1,8 +1,11 @@ import { describe, it, expect } from "vitest"; import { - parseAllowedTypes, - matchesContentType, DEFAULT_ALLOWED_TYPES, + INLINE_ATTACHMENT_TYPES, + isInlineAttachmentContentType, + matchesContentType, + normalizeContentType, + parseAllowedTypes, } from "../attachment-types.js"; describe("parseAllowedTypes", () => { @@ -95,3 +98,28 @@ describe("matchesContentType", () => { expect(matchesContentType("application/zip", patterns)).toBe(true); }); }); + +describe("normalizeContentType", () => { + it("lowercases and trims explicit types", () => { + expect(normalizeContentType(" Application/Zip ")).toBe("application/zip"); + }); + + it("falls back to octet-stream when the type is missing", () => { + expect(normalizeContentType(undefined)).toBe("application/octet-stream"); + expect(normalizeContentType("")).toBe("application/octet-stream"); + }); +}); + +describe("isInlineAttachmentContentType", () => { + it("allows the configured inline-safe types", () => { + for (const contentType of ["image/png", "image/svg+xml", "application/pdf", "text/plain"]) { + expect(isInlineAttachmentContentType(contentType)).toBe(true); + } + }); + + it("rejects potentially unsafe or binary download types", () => { + expect(INLINE_ATTACHMENT_TYPES).not.toContain("text/html"); + expect(isInlineAttachmentContentType("text/html")).toBe(false); + expect(isInlineAttachmentContentType("application/zip")).toBe(false); + }); +}); diff --git a/server/src/__tests__/issue-attachment-routes.test.ts b/server/src/__tests__/issue-attachment-routes.test.ts new file mode 100644 index 00000000..42a6b4e6 --- /dev/null +++ b/server/src/__tests__/issue-attachment-routes.test.ts @@ -0,0 +1,175 @@ +import { Readable } from "node:stream"; +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { errorHandler } from "../middleware/index.js"; +import { issueRoutes } from "../routes/issues.js"; +import type { StorageService } from "../storage/types.js"; + +const mockIssueService = vi.hoisted(() => ({ + getById: vi.fn(), + getByIdentifier: vi.fn(), + createAttachment: vi.fn(), + getAttachmentById: vi.fn(), +})); + +const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined)); + +vi.mock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(), + hasPermission: vi.fn(), + }), + agentService: () => ({ + getById: vi.fn(), + }), + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), + goalService: () => ({}), + heartbeatService: () => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + getRun: vi.fn(async () => null), + getActiveRunForAgent: vi.fn(async () => null), + cancelRun: vi.fn(async () => null), + }), + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), +})); + +function createStorageService(): StorageService { + return { + provider: "local_disk", + putFile: vi.fn(async (input) => ({ + provider: "local_disk", + objectKey: `${input.namespace}/${input.originalFilename ?? "upload"}`, + contentType: input.contentType, + byteSize: input.body.length, + sha256: "sha256-sample", + originalFilename: input.originalFilename, + })), + getObject: vi.fn(async () => ({ + stream: Readable.from(Buffer.from("test")), + contentLength: 4, + })), + headObject: vi.fn(), + deleteObject: vi.fn(), + }; +} + +function createApp(storage: StorageService) { + const app = express(); + app.use((req, _res, next) => { + (req as any).actor = { + type: "board", + userId: "local-board", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: false, + }; + next(); + }); + app.use("/api", issueRoutes({} as any, storage)); + app.use(errorHandler); + return app; +} + +function makeAttachment(contentType: string, originalFilename: string) { + const now = new Date("2026-01-01T00:00:00.000Z"); + return { + id: "attachment-1", + companyId: "company-1", + issueId: "11111111-1111-4111-8111-111111111111", + issueCommentId: null, + assetId: "asset-1", + provider: "local_disk", + objectKey: `issues/issue-1/${originalFilename}`, + contentType, + byteSize: 4, + sha256: "sha256-sample", + originalFilename, + createdByAgentId: null, + createdByUserId: "local-board", + createdAt: now, + updatedAt: now, + }; +} + +describe("issue attachment routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("accepts zip uploads for issue attachments", async () => { + const storage = createStorageService(); + mockIssueService.getById.mockResolvedValue({ + id: "11111111-1111-4111-8111-111111111111", + companyId: "company-1", + identifier: "PAP-1", + }); + mockIssueService.createAttachment.mockResolvedValue(makeAttachment("application/zip", "bundle.zip")); + + const res = await request(createApp(storage)) + .post("/api/companies/company-1/issues/11111111-1111-4111-8111-111111111111/attachments") + .attach("file", Buffer.from("zip"), { filename: "bundle.zip", contentType: "application/zip" }); + + expect(res.status).toBe(201); + const putFileCall = vi.mocked(storage.putFile).mock.calls[0]?.[0]; + expect(putFileCall).toMatchObject({ + companyId: "company-1", + namespace: "issues/11111111-1111-4111-8111-111111111111", + originalFilename: "bundle.zip", + contentType: "application/zip", + }); + expect(Buffer.isBuffer(putFileCall?.body)).toBe(true); + expect(mockIssueService.createAttachment).toHaveBeenCalledWith( + expect.objectContaining({ + issueId: "11111111-1111-4111-8111-111111111111", + contentType: "application/zip", + originalFilename: "bundle.zip", + }), + ); + expect(res.body.contentType).toBe("application/zip"); + }); + + it("serves html attachments as downloads with nosniff", async () => { + const storage = createStorageService(); + mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("text/html", "report.html")); + + const res = await request(createApp(storage)).get("/api/attachments/attachment-1/content"); + + expect(res.status).toBe(200); + expect(res.headers["content-disposition"]).toBe('attachment; filename="report.html"'); + expect(res.headers["x-content-type-options"]).toBe("nosniff"); + }); + + it("keeps image attachments inline for previews", async () => { + const storage = createStorageService(); + mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("image/png", "preview.png")); + + const res = await request(createApp(storage)).get("/api/attachments/attachment-1/content"); + + expect(res.status).toBe(200); + expect(res.headers["content-disposition"]).toBe('inline; filename="preview.png"'); + }); +}); diff --git a/server/src/attachment-types.ts b/server/src/attachment-types.ts index b9349179..9c02b86d 100644 --- a/server/src/attachment-types.ts +++ b/server/src/attachment-types.ts @@ -1,10 +1,10 @@ /** * Shared attachment content-type configuration. * - * By default only image types are allowed. Set the + * By default a curated set of image/document/text types are allowed. Set the * `PAPERCLIP_ALLOWED_ATTACHMENT_TYPES` environment variable to a * comma-separated list of MIME types or wildcard patterns to expand the - * allowed set. + * allowed set for routes that use this allowlist. * * Examples: * PAPERCLIP_ALLOWED_ATTACHMENT_TYPES=image/*,application/pdf @@ -29,6 +29,17 @@ export const DEFAULT_ALLOWED_TYPES: readonly string[] = [ "text/html", ]; +export const DEFAULT_ATTACHMENT_CONTENT_TYPE = "application/octet-stream"; +export const SVG_CONTENT_TYPE = "image/svg+xml"; +export const INLINE_ATTACHMENT_TYPES: readonly string[] = [ + "image/*", + "application/pdf", + "text/plain", + "text/markdown", + "application/json", + "text/csv", +]; + /** * Parse a comma-separated list of MIME type patterns into a normalised array. * Returns the default image-only list when the input is empty or undefined. @@ -59,6 +70,15 @@ export function matchesContentType(contentType: string, allowedPatterns: string[ }); } +export function normalizeContentType(contentType: string | null | undefined): string { + const normalized = (contentType ?? "").trim().toLowerCase(); + return normalized || DEFAULT_ATTACHMENT_CONTENT_TYPE; +} + +export function isInlineAttachmentContentType(contentType: string): boolean { + return matchesContentType(contentType, [...INLINE_ATTACHMENT_TYPES]); +} + // ---------- Module-level singletons read once at startup ---------- const allowedPatterns: string[] = parseAllowedTypes( diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 4617b7ed..9dfe26db 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -47,7 +47,12 @@ import { logger } from "../middleware/logger.js"; import { forbidden, HttpError, unauthorized } from "../errors.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; import { shouldWakeAssigneeOnCheckout } from "./issues-checkout-wakeup.js"; -import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; +import { + isInlineAttachmentContentType, + MAX_ATTACHMENT_BYTES, + normalizeContentType, + SVG_CONTENT_TYPE, +} from "../attachment-types.js"; import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js"; const MAX_ISSUE_COMMENT_LIMIT = 500; @@ -2108,11 +2113,7 @@ export function issueRoutes( res.status(400).json({ error: "Missing file field 'file'" }); return; } - const contentType = (file.mimetype || "").toLowerCase(); - if (!isAllowedContentType(contentType)) { - res.status(422).json({ error: `Unsupported attachment type: ${contentType || "unknown"}` }); - return; - } + const contentType = normalizeContentType(file.mimetype); if (file.buffer.length <= 0) { res.status(422).json({ error: "Attachment is empty" }); return; @@ -2176,11 +2177,17 @@ export function issueRoutes( assertCompanyAccess(req, attachment.companyId); const object = await storage.getObject(attachment.companyId, attachment.objectKey); - res.setHeader("Content-Type", attachment.contentType || object.contentType || "application/octet-stream"); + const responseContentType = normalizeContentType(attachment.contentType || object.contentType); + res.setHeader("Content-Type", responseContentType); res.setHeader("Content-Length", String(attachment.byteSize || object.contentLength || 0)); res.setHeader("Cache-Control", "private, max-age=60"); + res.setHeader("X-Content-Type-Options", "nosniff"); + if (responseContentType === SVG_CONTENT_TYPE) { + res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'"); + } const filename = attachment.originalFilename ?? "attachment"; - res.setHeader("Content-Disposition", `inline; filename=\"${filename.replaceAll("\"", "")}\"`); + const disposition = isInlineAttachmentContentType(responseContentType) ? "inline" : "attachment"; + res.setHeader("Content-Disposition", `${disposition}; filename=\"${filename.replaceAll("\"", "")}\"`); object.stream.on("error", (err) => { next(err); diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index d16a3103..1ab5b578 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1121,7 +1121,6 @@ export function IssueDetail() { Date: Sun, 5 Apr 2026 06:39:20 -0500 Subject: [PATCH 17/51] Support dropping non-image files onto markdown editor as attachments When dragging files like .zip onto the issue description editor, non-image files are now uploaded as attachments instead of being silently ignored. Images continue to be handled inline by MDXEditor's image plugin. Co-Authored-By: Paperclip --- ui/src/components/InlineEditor.tsx | 4 ++++ ui/src/components/MarkdownEditor.tsx | 34 +++++++++++++++++++++++----- ui/src/pages/IssueDetail.tsx | 3 +++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/ui/src/components/InlineEditor.tsx b/ui/src/components/InlineEditor.tsx index 8b4c5b27..f509fe7a 100644 --- a/ui/src/components/InlineEditor.tsx +++ b/ui/src/components/InlineEditor.tsx @@ -11,6 +11,8 @@ interface InlineEditorProps { placeholder?: string; multiline?: boolean; imageUploadHandler?: (file: File) => Promise ; + /** Called when a non-image file is dropped onto the editor. */ + onDropFile?: (file: File) => Promise ; mentions?: MentionOption[]; nullable?: boolean; } @@ -46,6 +48,7 @@ export function InlineEditor({ multiline = false, nullable = false, imageUploadHandler, + onDropFile, mentions, }: InlineEditorProps) { const [editing, setEditing] = useState(false); @@ -228,6 +231,7 @@ export function InlineEditor({ className="bg-transparent" contentClassName={cn("paperclip-edit-in-place-content", className)} imageUploadHandler={imageUploadHandler} + onDropFile={onDropFile} mentions={mentions} onSubmit={() => { finalizeMultilineBlurOrSubmit(); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 62f4010d..ff779926 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -62,6 +62,8 @@ interface MarkdownEditorProps { contentClassName?: string; onBlur?: () => void; imageUploadHandler?: (file: File) => Promise ; + /** Called when a non-image file is dropped onto the editor (e.g. .zip). */ + onDropFile?: (file: File) => Promise ; bordered?: boolean; /** List of mentionable entities. Enables @-mention autocomplete. */ mentions?: MentionOption[]; @@ -314,6 +316,7 @@ export const MarkdownEditor = forwardRef contentClassName, onBlur, imageUploadHandler, + onDropFile, bordered = true, mentions, onSubmit, @@ -668,6 +671,7 @@ export const MarkdownEditor = forwardRef } const canDropImage = Boolean(imageUploadHandler); + const canDropFile = Boolean(imageUploadHandler || onDropFile); const handlePasteCapture = useCallback((event: ClipboardEvent ) => { const clipboard = event.clipboardData; if (!clipboard || !ref.current) return; @@ -747,23 +751,41 @@ export const MarkdownEditor = forwardRef } }} onDragEnter={(evt) => { - if (!canDropImage || !hasFilePayload(evt)) return; + if (!canDropFile || !hasFilePayload(evt)) return; dragDepthRef.current += 1; setIsDragOver(true); }} onDragOver={(evt) => { - if (!canDropImage || !hasFilePayload(evt)) return; + if (!canDropFile || !hasFilePayload(evt)) return; evt.preventDefault(); evt.dataTransfer.dropEffect = "copy"; }} onDragLeave={() => { - if (!canDropImage) return; + if (!canDropFile) return; dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); if (dragDepthRef.current === 0) setIsDragOver(false); }} - onDrop={() => { + onDrop={(evt) => { dragDepthRef.current = 0; setIsDragOver(false); + if (!onDropFile) return; + const files = evt.dataTransfer?.files; + if (!files || files.length === 0) return; + const allFiles = Array.from(files); + const nonImageFiles = allFiles.filter( + (f) => !f.type.startsWith("image/"), + ); + if (nonImageFiles.length === 0) return; + // If all dropped files are non-image, prevent default so MDXEditor + // doesn't try to handle them. If mixed, let images flow through to + // the image plugin and only handle the non-image files ourselves. + if (nonImageFiles.length === allFiles.length) { + evt.preventDefault(); + evt.stopPropagation(); + } + for (const file of nonImageFiles) { + void onDropFile(file); + } }} onPasteCapture={handlePasteCapture} > @@ -854,14 +876,14 @@ export const MarkdownEditor = forwardRef document.body, )} - {isDragOver && canDropImage && ( + {isDragOver && canDropFile && ( - Drop image to upload + Drop file to upload)} {uploadError && ( diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 1ab5b578..d2cf091f 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1329,6 +1329,9 @@ export function IssueDetail() { const attachment = await uploadAttachment.mutateAsync(file); return attachment.contentPath; }} + onDropFile={async (file) => { + await uploadAttachment.mutateAsync(file); + }} /> From 612bab1eb65242c81c7cace673b6bbd57b6df247 Mon Sep 17 00:00:00 2001 From: dottaDate: Sun, 5 Apr 2026 07:35:33 -0500 Subject: [PATCH 18/51] Make execution workspace detail page responsive for mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce card padding on small screens (p-4 → p-4 sm:p-5) - Reduce spacing between sections on mobile (space-y-4 sm:space-y-6) - Scale heading text (text-xl sm:text-2xl) - Truncate long description on mobile, show full on sm+ - Reduce textarea min-heights on mobile (sm: prefix for larger sizes) - Stack linked issue cards vertically on mobile, horizontal scroll on sm+ - Remove min-width constraint on linked issue cards on mobile Co-Authored-By: Paperclip --- ui/src/pages/ExecutionWorkspaceDetail.tsx | 39 ++++++++++++----------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index 7fdac3e9..a031ed84 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -378,7 +378,7 @@ export function ExecutionWorkspaceDetail() { return ( <> - +---+++Execution workspace-{workspace.name}
+{workspace.name}
- Configure the concrete runtime workspace that Paperclip reuses for this issue flow. These settings stay + Configure the concrete runtime workspace that Paperclip reuses for this issue flow. + These settings stay attached to the execution workspace so future runs can keep local paths, repo refs, provisioning, teardown, - and runtime-service behavior in sync with the actual workspace being reused. + and runtime-service behavior in sync with the actual workspace being reused.
@@ -482,7 +483,7 @@ export function ExecutionWorkspaceDetail() {- -++-Linked objectsWorkspace context
@@ -632,7 +633,7 @@ export function ExecutionWorkspaceDetail() {+-Paths and refsConcrete location
@@ -676,7 +677,7 @@ export function ExecutionWorkspaceDetail() {+-Runtime services@@ -755,7 +756,7 @@ export function ExecutionWorkspaceDetail() { )}+-Recent operationsRuntime and cleanup logs
@@ -798,7 +799,7 @@ export function ExecutionWorkspaceDetail() {+Linked issues@@ -819,12 +820,12 @@ export function ExecutionWorkspaceDetail() { : "Failed to load linked issues."} ) : linkedIssues.length > 0 ? ( -+{linkedIssues.map((issue) => (From aff56c22972dd2e20002d4c9ed38a23b7a21af67 Mon Sep 17 00:00:00 2001 From: dottaFrom 2278d96d5a0fd3da0f2e327ec97f3286018962e2 Mon Sep 17 00:00:00 2001 From: dottaDate: Sun, 5 Apr 2026 07:39:38 -0500 Subject: [PATCH 19/51] Copy inherited config as default when unchecking inherit checkbox When unchecking the "Inherit project workspace runtime config" checkbox, if the runtime config field is empty, automatically populate it with the inherited config value so the user has a starting point to edit from. Existing values are preserved and never overwritten. Co-Authored-By: Paperclip --- ui/src/pages/ExecutionWorkspaceDetail.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index a031ed84..b166e432 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -547,9 +547,17 @@ export function ExecutionWorkspaceDetail() { id="inherit-runtime-config" type="checkbox" checked={form.inheritRuntime} - onChange={(event) => - setForm((current) => current ? { ...current, inheritRuntime: event.target.checked } : current) - } + onChange={(event) => { + const checked = event.target.checked; + setForm((current) => { + if (!current) return current; + // When unchecking "inherit" and the field is empty, copy inherited config as a starting point + if (!checked && !current.workspaceRuntime.trim() && inheritedRuntimeConfig) { + return { ...current, inheritRuntime: checked, workspaceRuntime: formatJson(inheritedRuntimeConfig) }; + } + return { ...current, inheritRuntime: checked }; + }); + }} /> Date: Sun, 5 Apr 2026 07:46:40 -0500 Subject: [PATCH 20/51] Fix execution workspace page overflow on mobile - Add overflow-hidden to the outer container to prevent horizontal scroll - Add min-w-0 to grid children so long monospace content in inputs respects container width instead of expanding it - Truncate the workspace name heading for long names - Add min-w-0 to the header name container Co-Authored-By: Paperclip --- ui/src/pages/ExecutionWorkspaceDetail.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index b166e432..e5445890 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -378,7 +378,7 @@ export function ExecutionWorkspaceDetail() { return ( <> - +-+-+-Execution workspace-{workspace.name}
+{workspace.name}
Configure the concrete runtime workspace that Paperclip reuses for this issue flow. These settings stay @@ -595,7 +595,7 @@ export function ExecutionWorkspaceDetail() {
+Linked objectsFrom 2ac1c62ab136ab830e0b21ca0ce31699bee3c8b3 Mon Sep 17 00:00:00 2001 From: dottaDate: Sun, 5 Apr 2026 10:47:30 -0500 Subject: [PATCH 21/51] Fix mobile inbox layout: move search above tabs, hide column toggle On mobile, the search input, tab selector, and "Show / hide columns" button were all crammed into one row causing horizontal overflow. Now: - Search appears as a full-width row above the tabs on mobile - "Show / hide columns" button is hidden on mobile (columns are desktop-only) - Desktop layout is unchanged Co-Authored-By: Paperclip --- ui/src/pages/Inbox.tsx | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 45594db7..6bb5cb6c 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -1589,7 +1589,19 @@ export function Inbox() { const canMarkAllRead = unreadIssueIds.length > 0; return ( -++ {/* Search — full-width row on mobile, inline on desktop */} ++++ setSearchQuery(e.target.value)} + className="h-8 w-full pl-8 text-xs" + /> + {tab === "all" && ( From 962a8827995510c63b31d04b62583c3f2d340a41 Mon Sep 17 00:00:00 2001 From: dottanavigate(`/inbox/${value}`)}> -++setSearchQuery(e.target.value)} - className="h-8 w-[180px] pl-8 text-xs sm:w-[220px]" + className="h-8 w-[220px] pl-8 text-xs" /> @@ -1624,7 +1636,7 @@ export function Inbox() { type="button" variant="ghost" size="sm" - className="h-8 shrink-0 px-2 text-xs text-muted-foreground hover:text-foreground" + className="hidden h-8 shrink-0 px-2 text-xs text-muted-foreground hover:text-foreground sm:inline-flex" > Show / hide columns @@ -1708,6 +1720,7 @@ export function Inbox() { > )} Date: Mon, 6 Apr 2026 06:32:25 -0500 Subject: [PATCH 22/51] fix(ui): keep issue breadcrumb context out of the URL Co-Authored-By: Paperclip --- ui/src/components/IssueRow.test.tsx | 4 +- ui/src/components/IssueRow.tsx | 5 +- ui/src/lib/issueDetailBreadcrumb.test.ts | 60 +++++++++--- ui/src/lib/issueDetailBreadcrumb.ts | 113 +++++++++++++++++++---- ui/src/pages/Inbox.tsx | 4 +- ui/src/pages/IssueDetail.tsx | 48 ++++++++-- 6 files changed, 185 insertions(+), 49 deletions(-) diff --git a/ui/src/components/IssueRow.test.tsx b/ui/src/components/IssueRow.test.tsx index d5f67e9d..4ba0cee2 100644 --- a/ui/src/components/IssueRow.test.tsx +++ b/ui/src/components/IssueRow.test.tsx @@ -128,9 +128,7 @@ describe("IssueRow", () => { const link = container.querySelector("[data-inbox-issue-link]") as HTMLAnchorElement | null; expect(link).not.toBeNull(); - expect(link?.getAttribute("to") ?? link?.getAttribute("href")).toContain( - "/issues/PAP-1?from=inbox&fromHref=%2FPAP%2Finbox%2Fmine", - ); + expect(link?.getAttribute("to") ?? link?.getAttribute("href")).toBe("/issues/PAP-1"); act(() => { root.unmount(); diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx index 5711a69c..09df3f03 100644 --- a/ui/src/components/IssueRow.tsx +++ b/ui/src/components/IssueRow.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import type { Issue } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { X } from "lucide-react"; -import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb"; +import { createIssueDetailPath, rememberIssueDetailLocationState } from "../lib/issueDetailBreadcrumb"; import { cn } from "../lib/utils"; import { StatusIcon } from "./StatusIcon"; @@ -51,9 +51,10 @@ export function IssueRow({ return ( rememberIssueDetailLocationState(issuePathId, issueLinkState)} className={cn( "group flex items-start gap-2 border-b border-border py-2.5 pl-2 pr-3 text-sm no-underline text-inherit transition-colors last:border-b-0 sm:items-center sm:py-2 sm:pl-1", selected ? "hover:bg-transparent" : "hover:bg-accent/50", diff --git a/ui/src/lib/issueDetailBreadcrumb.test.ts b/ui/src/lib/issueDetailBreadcrumb.test.ts index cebf65bf..84e776c4 100644 --- a/ui/src/lib/issueDetailBreadcrumb.test.ts +++ b/ui/src/lib/issueDetailBreadcrumb.test.ts @@ -3,50 +3,80 @@ import { armIssueDetailInboxQuickArchive, createIssueDetailLocationState, createIssueDetailPath, + hasLegacyIssueDetailQuery, + readIssueDetailLocationState, readIssueDetailBreadcrumb, + rememberIssueDetailLocationState, shouldArmIssueDetailInboxQuickArchive, } from "./issueDetailBreadcrumb"; +const sessionStorageMock = (() => { + const store = new Map (); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + clear: () => { + store.clear(); + }, + }; +})(); + +Object.defineProperty(globalThis, "window", { + configurable: true, + value: { sessionStorage: sessionStorageMock }, +}); + describe("issueDetailBreadcrumb", () => { + it("returns clean issue detail paths", () => { + expect(createIssueDetailPath("PAP-465")).toBe("/issues/PAP-465"); + }); + it("prefers the full breadcrumb from route state", () => { const state = createIssueDetailLocationState("Inbox", "/inbox/mine", "inbox"); - expect(readIssueDetailBreadcrumb(state, "?from=issues")).toEqual({ + expect(readIssueDetailBreadcrumb("PAP-465", state, "?from=issues")).toEqual({ label: "Inbox", href: "/inbox/mine", }); }); it("falls back to the source query param when route state is unavailable", () => { - expect(readIssueDetailBreadcrumb(null, "?from=inbox")).toEqual({ + expect(readIssueDetailBreadcrumb("PAP-465", null, "?from=inbox")).toEqual({ label: "Inbox", href: "/inbox", }); }); - it("adds the source query param when building an issue detail path", () => { - const state = createIssueDetailLocationState("Inbox", "/inbox/mine", "inbox"); - - expect(createIssueDetailPath("PAP-465", state)).toBe( - "/issues/PAP-465?from=inbox&fromHref=%2Finbox%2Fmine", - ); - }); - - it("reuses the current source query param when state has been dropped", () => { - expect(createIssueDetailPath("PAP-465", null, "?from=issues&fromHref=%2Fissues%3Fq%3Dabc")).toBe( - "/issues/PAP-465?from=issues&fromHref=%2Fissues%3Fq%3Dabc", - ); + it("can detect legacy query-based breadcrumb links", () => { + expect(hasLegacyIssueDetailQuery("?from=inbox&fromHref=%2Finbox%2Fmine")).toBe(true); + expect(hasLegacyIssueDetailQuery("?q=test")).toBe(false); }); it("restores the exact breadcrumb href from the query fallback", () => { expect( - readIssueDetailBreadcrumb(null, "?from=inbox&fromHref=%2FPAP%2Finbox%2Funread"), + readIssueDetailBreadcrumb("PAP-465", null, "?from=inbox&fromHref=%2FPAP%2Finbox%2Funread"), ).toEqual({ label: "Inbox", href: "/PAP/inbox/unread", }); }); + it("reads hidden breadcrumb context from session storage when route state is unavailable", () => { + const state = createIssueDetailLocationState("Inbox", "/inbox/mine", "inbox"); + sessionStorageMock.clear(); + rememberIssueDetailLocationState("PAP-465", state); + + expect( + readIssueDetailLocationState("PAP-465", null), + ).toEqual({ + issueDetailBreadcrumb: { label: "Inbox", href: "/inbox/mine" }, + issueDetailSource: "inbox", + issueDetailInboxQuickArchiveArmed: false, + }); + }); + it("can arm quick archive only for explicit inbox keyboard entry state", () => { const state = createIssueDetailLocationState("Inbox", "/inbox/mine", "inbox"); diff --git a/ui/src/lib/issueDetailBreadcrumb.ts b/ui/src/lib/issueDetailBreadcrumb.ts index a53864e4..9670f561 100644 --- a/ui/src/lib/issueDetailBreadcrumb.ts +++ b/ui/src/lib/issueDetailBreadcrumb.ts @@ -13,6 +13,7 @@ type IssueDetailLocationState = { const ISSUE_DETAIL_SOURCE_QUERY_PARAM = "from"; const ISSUE_DETAIL_BREADCRUMB_HREF_QUERY_PARAM = "fromHref"; +const ISSUE_DETAIL_STORAGE_KEY_PREFIX = "paperclip:issue-detail-breadcrumb:"; function isIssueDetailBreadcrumb(value: unknown): value is IssueDetailBreadcrumb { if (typeof value !== "object" || value === null) return false; @@ -44,6 +45,17 @@ function readIssueDetailBreadcrumbHrefFromSearch(search?: string): string | null return href && href.startsWith("/") ? href : null; } +function inferIssueDetailSource( + state: Partial | null, + breadcrumb: IssueDetailBreadcrumb | null, +): IssueDetailSource | null { + if (isIssueDetailSource(state?.issueDetailSource)) return state.issueDetailSource; + if (!breadcrumb) return null; + if (breadcrumb.label === "Inbox" || breadcrumb.href.includes("/inbox")) return "inbox"; + if (breadcrumb.label === "Issues" || breadcrumb.href.includes("/issues")) return "issues"; + return null; +} + function breadcrumbForSource(source: IssueDetailSource): IssueDetailBreadcrumb { if (source === "inbox") return { label: "Inbox", href: "/inbox" }; return { label: "Issues", href: "/issues" }; @@ -71,34 +83,97 @@ export function armIssueDetailInboxQuickArchive(state: unknown): IssueDetailLoca }; } -export function createIssueDetailPath(issuePathId: string, state?: unknown, search?: string): string { - const source = readIssueDetailSource(state) ?? readIssueDetailSourceFromSearch(search); - const breadcrumb = - (typeof state === "object" && state !== null - ? (state as IssueDetailLocationState).issueDetailBreadcrumb - : null); - const breadcrumbHref = - (isIssueDetailBreadcrumb(breadcrumb) ? breadcrumb.href : null) ?? - readIssueDetailBreadcrumbHrefFromSearch(search); - if (!source) return `/issues/${issuePathId}`; - const params = new URLSearchParams(); - params.set(ISSUE_DETAIL_SOURCE_QUERY_PARAM, source); - if (breadcrumbHref) params.set(ISSUE_DETAIL_BREADCRUMB_HREF_QUERY_PARAM, breadcrumbHref); - return `/issues/${issuePathId}?${params.toString()}`; +function readStoredIssueDetailLocationState(issuePathId: string): IssueDetailLocationState | null { + if (typeof window === "undefined" || !window.sessionStorage) return null; + + const raw = window.sessionStorage.getItem(`${ISSUE_DETAIL_STORAGE_KEY_PREFIX}${issuePathId}`); + if (!raw) return null; + + try { + const parsed = JSON.parse(raw) as Partial ; + const breadcrumb = isIssueDetailBreadcrumb(parsed.issueDetailBreadcrumb) + ? parsed.issueDetailBreadcrumb + : null; + const source = inferIssueDetailSource(parsed, breadcrumb); + if (!breadcrumb || !source) return null; + return { + issueDetailBreadcrumb: breadcrumb, + issueDetailSource: source, + issueDetailInboxQuickArchiveArmed: parsed.issueDetailInboxQuickArchiveArmed === true, + }; + } catch { + return null; + } } -export function readIssueDetailBreadcrumb(state: unknown, search?: string): IssueDetailBreadcrumb | null { +function normalizeIssueDetailLocationState( + state: unknown, + search?: string, +): IssueDetailLocationState | null { if (typeof state === "object" && state !== null) { const candidate = (state as IssueDetailLocationState).issueDetailBreadcrumb; - if (isIssueDetailBreadcrumb(candidate)) return candidate; + if (isIssueDetailBreadcrumb(candidate)) { + const source = inferIssueDetailSource(state as Partial , candidate); + if (!source) return null; + return { + issueDetailBreadcrumb: candidate, + issueDetailSource: source, + issueDetailInboxQuickArchiveArmed: + (state as IssueDetailLocationState).issueDetailInboxQuickArchiveArmed === true, + }; + } } const source = readIssueDetailSourceFromSearch(search); + const href = readIssueDetailBreadcrumbHrefFromSearch(search); if (!source) return null; - const fallback = breadcrumbForSource(source); - const href = readIssueDetailBreadcrumbHrefFromSearch(search); - return href ? { ...fallback, href } : fallback; + return { + issueDetailBreadcrumb: href ? { ...breadcrumbForSource(source), href } : breadcrumbForSource(source), + issueDetailSource: source, + issueDetailInboxQuickArchiveArmed: false, + }; +} + +export function rememberIssueDetailLocationState(issuePathId: string, state: unknown, search?: string): void { + if (typeof window === "undefined" || !window.sessionStorage) return; + + const normalized = normalizeIssueDetailLocationState(state, search); + if (!normalized) return; + + window.sessionStorage.setItem( + `${ISSUE_DETAIL_STORAGE_KEY_PREFIX}${issuePathId}`, + JSON.stringify(normalized), + ); +} + +export function createIssueDetailPath(issuePathId: string): string { + return `/issues/${issuePathId}`; +} + +export function hasLegacyIssueDetailQuery(search?: string): boolean { + if (!search) return false; + const params = new URLSearchParams(search); + return params.has(ISSUE_DETAIL_SOURCE_QUERY_PARAM) || params.has(ISSUE_DETAIL_BREADCRUMB_HREF_QUERY_PARAM); +} + +export function readIssueDetailLocationState( + issuePathId: string | null | undefined, + state: unknown, + search?: string, +): IssueDetailLocationState | null { + const normalized = normalizeIssueDetailLocationState(state, search); + if (normalized) return normalized; + if (!issuePathId) return null; + return readStoredIssueDetailLocationState(issuePathId); +} + +export function readIssueDetailBreadcrumb( + issuePathId: string | null | undefined, + state: unknown, + search?: string, +): IssueDetailBreadcrumb | null { + return readIssueDetailLocationState(issuePathId, state, search)?.issueDetailBreadcrumb ?? null; } export function shouldArmIssueDetailInboxQuickArchive(state: unknown): boolean { diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 6bb5cb6c..052dd7c7 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -21,6 +21,7 @@ import { armIssueDetailInboxQuickArchive, createIssueDetailLocationState, createIssueDetailPath, + rememberIssueDetailLocationState, } from "../lib/issueDetailBreadcrumb"; import { hasBlockingShortcutDialog, isKeyboardShortcutTextInputTarget } from "../lib/keyboardShortcuts"; import { EmptyState } from "../components/EmptyState"; @@ -1521,7 +1522,8 @@ export function Inbox() { if (item.kind === "issue") { const pathId = item.issue.identifier ?? item.issue.id; const detailState = armIssueDetailInboxQuickArchive(issueLinkState); - act.navigate(createIssueDetailPath(pathId, detailState), { state: detailState }); + rememberIssueDetailLocationState(pathId, detailState); + act.navigate(createIssueDetailPath(pathId), { state: detailState }); } else if (item.kind === "approval") { act.navigate(`/approvals/${item.approval.id}`); } else if (item.kind === "failed_run") { diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index d2cf091f..966bba18 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -17,8 +17,11 @@ import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../li import { extractIssueTimelineEvents } from "../lib/issue-timeline-events"; import { queryKeys } from "../lib/queryKeys"; import { + hasLegacyIssueDetailQuery, createIssueDetailPath, + readIssueDetailLocationState, readIssueDetailBreadcrumb, + rememberIssueDetailLocationState, shouldArmIssueDetailInboxQuickArchive, } from "../lib/issueDetailBreadcrumb"; import { hasBlockingShortcutDialog, resolveInboxQuickArchiveKeyAction } from "../lib/keyboardShortcuts"; @@ -375,9 +378,13 @@ export function IssueDetail() { ), [activeRun, liveRuns], ); + const resolvedIssueDetailState = useMemo( + () => readIssueDetailLocationState(issueId, location.state, location.search), + [issueId, location.state, location.search], + ); const sourceBreadcrumb = useMemo( - () => readIssueDetailBreadcrumb(location.state, location.search) ?? { label: "Issues", href: "/issues" }, - [location.state, location.search], + () => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Issues", href: "/issues" }, + [issueId, location.state, location.search], ); // Filter out runs already shown by the live widget to avoid duplication @@ -967,13 +974,24 @@ export function IssueDetail() { // Redirect to identifier-based URL if navigated via UUID useEffect(() => { + const nextState = resolvedIssueDetailState ?? location.state; if (issue?.identifier && issueId !== issue.identifier) { - navigate(createIssueDetailPath(issue.identifier, location.state, location.search), { + rememberIssueDetailLocationState(issue.identifier, nextState, location.search); + navigate(createIssueDetailPath(issue.identifier), { replace: true, - state: location.state, + state: nextState, + }); + return; + } + + if (issueId && hasLegacyIssueDetailQuery(location.search)) { + rememberIssueDetailLocationState(issueId, nextState, location.search); + navigate(createIssueDetailPath(issueId), { + replace: true, + state: nextState, }); } - }, [issue, issueId, navigate, location.state, location.search]); + }, [issue, issueId, navigate, location.state, location.search, resolvedIssueDetailState]); useEffect(() => { if (!issue?.id) return; @@ -1155,8 +1173,14 @@ export function IssueDetail() { {i > 0 && } + rememberIssueDetailLocationState( + ancestor.identifier ?? ancestor.id, + resolvedIssueDetailState ?? location.state, + location.search, + )} className="hover:text-foreground transition-colors truncate max-w-[200px]" title={ancestor.title} > @@ -1575,8 +1599,14 @@ export function IssueDetail() { {childIssues.map((child) => ( + rememberIssueDetailLocationState( + child.identifier ?? child.id, + resolvedIssueDetailState ?? location.state, + location.search, + )} className="flex items-center justify-between px-3 py-2 text-sm hover:bg-accent/20 transition-colors" > From 54ac2c6fe9a9cb3be8d7f3419971b8e7eebdfcfd Mon Sep 17 00:00:00 2001 From: dotta+ {issue.currentExecutionWorkspace?.branchName || issue.currentExecutionWorkspace?.cwd ? ( + <> +Date: Mon, 6 Apr 2026 08:10:38 -0500 Subject: [PATCH 23/51] feat(ui): show workspace branch/folder in issue properties sidebar Adds a new workspace section to the IssueProperties sidebar that displays branch name and folder path (cwd) from the execution workspace. Both values have copy-to-clipboard buttons and truncated display with full path on hover. Co-Authored-By: Paperclip --- ui/src/components/IssueProperties.tsx | 58 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index a0728c07..3dfe0541 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { pickTextColorForPillBg } from "@/lib/color-contrast"; import { Link } from "@/lib/router"; import type { Issue } from "@paperclipai/shared"; @@ -19,9 +19,39 @@ import { formatDate, cn, projectUrl } from "../lib/utils"; import { timeAgo } from "../lib/timeAgo"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react"; +import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2, GitBranch, FolderOpen, Copy, Check } from "lucide-react"; import { AgentIcon } from "./AgentIconPicker"; +function TruncatedCopyable({ value, icon: Icon }: { value: string; icon: React.ComponentType<{ className?: string }> }) { + const [copied, setCopied] = useState(false); + const timerRef = useRef >(undefined); + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setCopied(false), 1500); + } catch { /* noop */ } + }, [value]); + + return ( + ++ ); +} + function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null; @@ -700,6 +730,30 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp )}+ + {value} + + + + + {issue.currentExecutionWorkspace?.branchName && ( ++ > + ) : null} ++ + )} + {issue.currentExecutionWorkspace?.cwd && ( ++ + + )} ++ From 13ada98e78567d84a4b6125fbbeac02a99da544e Mon Sep 17 00:00:00 2001 From: dotta); } From 93e8e6447d14d350e7594acb80ea16b30f39bf5a Mon Sep 17 00:00:00 2001 From: dottaDate: Mon, 6 Apr 2026 08:11:50 -0500 Subject: [PATCH 24/51] feat(ui): add document revision diff viewer Add a "View diff" option to the document three-dot menu (visible when revision > 1) that opens a modal showing side-by-side changes between revisions using react-diff-viewer-continued. Defaults to comparing the current revision with its predecessor, with dropdowns to select any two revisions. Co-Authored-By: Paperclip --- ui/package.json | 5 +- ui/src/components/DocumentDiffModal.tsx | 169 ++++++++++++++++++++ ui/src/components/IssueDocumentsSection.tsx | 24 ++- 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 ui/src/components/DocumentDiffModal.tsx diff --git a/ui/package.json b/ui/package.json index d344f67a..c8374200 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@lexical/link": "0.35.0", - "lexical": "0.35.0", "@mdxeditor/editor": "^3.52.4", "@paperclipai/adapter-claude-local": "workspace:*", "@paperclipai/adapter-codex-local": "workspace:*", @@ -41,17 +40,19 @@ "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/shared": "workspace:*", - "hermes-paperclip-adapter": "^0.2.0", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "hermes-paperclip-adapter": "^0.2.0", + "lexical": "0.35.0", "lucide-react": "^0.574.0", "mermaid": "^11.12.0", "radix-ui": "^1.4.3", "react": "^19.0.0", + "react-diff-viewer-continued": "^4.2.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.1.5", diff --git a/ui/src/components/DocumentDiffModal.tsx b/ui/src/components/DocumentDiffModal.tsx new file mode 100644 index 00000000..b85defa5 --- /dev/null +++ b/ui/src/components/DocumentDiffModal.tsx @@ -0,0 +1,169 @@ +import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { DocumentRevision } from "@paperclipai/shared"; +import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued"; +import { issuesApi } from "../api/issues"; +import { queryKeys } from "../lib/queryKeys"; +import { relativeTime } from "../lib/utils"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +function getRevisionLabel(revision: DocumentRevision) { + const actor = revision.createdByUserId + ? "board" + : revision.createdByAgentId + ? "agent" + : "system"; + return `rev ${revision.revisionNumber} — ${relativeTime(revision.createdAt)} • ${actor}`; +} + +export function DocumentDiffModal({ + issueId, + documentKey, + latestRevisionNumber, + open, + onOpenChange, +}: { + issueId: string; + documentKey: string; + latestRevisionNumber: number; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const { data: revisions } = useQuery({ + queryKey: queryKeys.issues.documentRevisions(issueId, documentKey), + queryFn: () => issuesApi.listDocumentRevisions(issueId, documentKey), + enabled: open, + }); + + const sortedRevisions = useMemo(() => { + if (!revisions) return []; + return [...revisions].sort((a, b) => b.revisionNumber - a.revisionNumber); + }, [revisions]); + + // Default: compare previous (latestRevisionNumber - 1) with current (latestRevisionNumber) + const [leftRevisionId, setLeftRevisionId] = useState (null); + const [rightRevisionId, setRightRevisionId] = useState (null); + + const effectiveLeftId = leftRevisionId ?? sortedRevisions.find( + (r) => r.revisionNumber === latestRevisionNumber - 1, + )?.id ?? null; + + const effectiveRightId = rightRevisionId ?? sortedRevisions.find( + (r) => r.revisionNumber === latestRevisionNumber, + )?.id ?? null; + + const leftRevision = sortedRevisions.find((r) => r.id === effectiveLeftId) ?? null; + const rightRevision = sortedRevisions.find((r) => r.id === effectiveRightId) ?? null; + + const leftBody = leftRevision?.body ?? ""; + const rightBody = rightRevision?.body ?? ""; + + return ( + + ); +} diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index 31a6dd46..26db7266 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -29,7 +29,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Check, ChevronDown, ChevronRight, Copy, Download, FilePenLine, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; +import { Check, ChevronDown, ChevronRight, Copy, Diff, Download, FilePenLine, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; +import { DocumentDiffModal } from "./DocumentDiffModal"; type DraftState = { key: string; @@ -162,6 +163,7 @@ export function IssueDocumentsSection({ const [highlightDocumentKey, setHighlightDocumentKey] = useState (null); const [revisionMenuOpenKey, setRevisionMenuOpenKey] = useState (null); const [selectedRevisionIds, setSelectedRevisionIds] = useState >({}); + const [diffViewKey, setDiffViewKey] = useState (null); const autosaveDebounceRef = useRef | null>(null); const copiedDocumentTimerRef = useRef | null>(null); const hasScrolledToHashRef = useRef(false); @@ -929,6 +931,12 @@ export function IssueDocumentsSection({ Download document + {doc.latestRevisionNumber > 1 ? ( + setDiffViewKey(doc.key)}> + + ) : null} {canDeleteDocuments ?+ View diff + : null} {canDeleteDocuments ? ( + + {diffViewKey && (() => { + const diffDoc = sortedDocuments.find((d) => d.key === diffViewKey); + if (!diffDoc) return null; + return ( + { if (!open) setDiffViewKey(null); }} + /> + ); + })()} Date: Mon, 6 Apr 2026 08:17:06 -0500 Subject: [PATCH 25/51] fix(ui): improve diff modal layout and readability - Make modal much wider (90vw) to show full document content - Use monospace font in diff area for better readability - Enable word-wrap with pre-wrap so long lines wrap cleanly without breaking line number gutters - Move revision selectors into a single row with colored Old/New badges instead of stacked Left:/Right: labels Co-Authored-By: Paperclip --- ui/src/components/DocumentDiffModal.tsx | 101 +++++++++++++----------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/ui/src/components/DocumentDiffModal.tsx b/ui/src/components/DocumentDiffModal.tsx index b85defa5..77286ffe 100644 --- a/ui/src/components/DocumentDiffModal.tsx +++ b/ui/src/components/DocumentDiffModal.tsx @@ -72,49 +72,51 @@ export function DocumentDiffModal({ return (