From 284bd733b9b610381c4759b7adde7d21ece34969 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 08:41:01 -0500 Subject: [PATCH 01/33] Add workspace product model plan --- ...orkspace-product-model-and-work-product.md | 959 ++++++++++++++++++ 1 file changed, 959 insertions(+) create mode 100644 doc/plans/workspace-product-model-and-work-product.md diff --git a/doc/plans/workspace-product-model-and-work-product.md b/doc/plans/workspace-product-model-and-work-product.md new file mode 100644 index 00000000..3b201e7d --- /dev/null +++ b/doc/plans/workspace-product-model-and-work-product.md @@ -0,0 +1,959 @@ +# Workspace Product Model, Work Product, and PR Flow + +## Context + +Paperclip needs to support two very different but equally valid ways of working: + +- a solo developer working directly on `master`, or in a folder that is not even a git repo +- a larger engineering workflow with isolated branches, previews, pull requests, and cleanup automation + +Today, Paperclip already has the beginnings of this model: + +- `projects` can carry execution workspace policy +- `project_workspaces` already exist as a durable project-scoped object +- issues can carry execution workspace settings +- runtime services can be attached to a workspace or issue + +What is missing is a clear product model and UI that make these capabilities understandable and operable. + +The main product risk is overloading one concept to do too much: + +- making subissues do the job of branches or PRs +- making projects too infrastructure-heavy +- making workspaces so hidden that users cannot form a mental model +- making Paperclip feel like a code review tool instead of a control plane + +## Goals + +1. Keep `project` lightweight enough to remain a planning container. +2. Make workspace behavior understandable for both git and non-git projects. +3. Support three real workflows without forcing one: + - shared workspace / direct-edit workflows + - isolated issue workspace workflows + - long-lived branch or operator integration workflows +4. Provide a first-class place to see the outputs of work: + - previews + - PRs + - branches + - commits + - documents and artifacts +5. Keep the main navigation and task board simple. + +## Non-Goals + +- Turning Paperclip into a full code review product +- Requiring every issue to have its own branch or PR +- Requiring every project to configure code/workspace automation +- Making workspaces a top-level global navigation primitive in V1 + +## Core Product Decisions + +### 1. Project stays the planning object + +A `project` remains the thing that groups work around a deliverable or initiative. + +It may have: + +- no code at all +- one default codebase/workspace +- several codebases/workspaces + +Projects are not required to become heavyweight. + +### 2. Project workspace is a first-class object, but scoped under project + +A `project workspace` is the durable codebase or root environment for a project. + +Examples: + +- a local folder on disk +- a git repo checkout +- a monorepo package root +- a non-git design/doc folder +- a remote adapter-managed codebase reference + +This is the stable anchor that operators configure once. + +It should not be a top-level sidebar item in the main app. It should live under the project experience. + +### 3. Execution workspace is a first-class runtime object + +An `execution workspace` is where a specific run or issue actually executes. + +Examples: + +- the shared project workspace itself +- an isolated git worktree +- a long-lived operator branch checkout +- an adapter-managed remote sandbox + +This object must be recorded explicitly so that Paperclip can: + +- show where work happened +- attach previews and runtime services +- link PRs and branches +- decide cleanup behavior +- support reuse across multiple related issues + +### 4. PRs are work product, not the core issue model + +A PR is an output of work, not the planning unit. + +Paperclip should treat PRs as a type of work product linked back to: + +- the issue +- the execution workspace +- optionally the project workspace + +Git-specific automation should live under workspace policy, not under the core issue abstraction. + +### 5. Subissues remain planning and ownership structure + +Subissues are for decomposition and parallel ownership. + +They are not the same thing as: + +- a branch +- a worktree +- a PR +- a preview + +They may correlate with those things, but they should not be overloaded to mean them. + +## Terminology + +Use these terms consistently in product copy: + +- `Project`: planning container +- `Project workspace`: durable configured codebase/root +- `Execution workspace`: actual runtime workspace used for issue execution +- `Isolated issue workspace`: user-facing term for an issue-specific derived workspace +- `Work product`: previews, PRs, branches, commits, artifacts, docs +- `Runtime service`: a process or service Paperclip owns or tracks for a workspace + +Avoid teaching users that "workspace" always means "git worktree on my machine". + +## Product Object Model + +## 1. Project + +Existing object. No fundamental change in role. + +### Required behavior + +- can exist without code/workspace configuration +- can have zero or more project workspaces +- can define execution defaults that new issues inherit + +### Proposed fields + +- `id` +- `companyId` +- `name` +- `description` +- `status` +- `goalIds` +- `leadAgentId` +- `targetDate` +- `executionWorkspacePolicy` +- `workspaces[]` +- `primaryWorkspace` + +## 2. Project Workspace + +Durable, configured, project-scoped codebase/root object. + +This should evolve from the current `project_workspaces` table into a more explicit product object. + +### Motivation + +This separates: + +- "what codebase/root does this project use?" + +from: + +- "what temporary execution environment did this issue run in?" + +That keeps the model simple for solo users while still supporting advanced automation. + +### Proposed fields + +- `id` +- `companyId` +- `projectId` +- `name` +- `sourceType` + - `local_path` + - `git_repo` + - `remote_managed` + - `non_git_path` +- `cwd` +- `repoUrl` +- `defaultRef` +- `isPrimary` +- `visibility` + - `default` + - `advanced` +- `setupCommand` +- `cleanupCommand` +- `metadata` +- `createdAt` +- `updatedAt` + +### Notes + +- `sourceType=non_git_path` is important so non-git projects are first-class. +- `setupCommand` and `cleanupCommand` should be allowed here for workspace-root bootstrap, even when isolated execution is not used. +- For a monorepo, multiple project workspaces may point at different roots or packages under one repo. + +## 3. Project Execution Workspace Policy + +Project-level defaults for how issues execute. + +This is the main operator-facing configuration surface. + +### Motivation + +This lets Paperclip support: + +- direct editing in a shared workspace +- isolated workspaces for issue parallelism +- long-lived integration branch workflows + +without forcing every issue or agent to expose low-level runtime configuration. + +### Proposed fields + +- `enabled: boolean` +- `defaultMode` + - `shared_workspace` + - `isolated_workspace` + - `operator_branch` + - `adapter_default` +- `allowIssueOverride: boolean` +- `defaultProjectWorkspaceId: uuid | null` +- `workspaceStrategy` + - `type` + - `project_primary` + - `git_worktree` + - `adapter_managed` + - `baseRef` + - `branchTemplate` + - `worktreeParentDir` + - `provisionCommand` + - `teardownCommand` +- `branchPolicy` + - `namingTemplate` + - `allowReuseExisting` + - `preferredOperatorBranch` +- `pullRequestPolicy` + - `mode` + - `disabled` + - `manual` + - `agent_may_open_draft` + - `approval_required_to_open` + - `approval_required_to_mark_ready` + - `baseBranch` + - `titleTemplate` + - `bodyTemplate` +- `runtimePolicy` + - `allowWorkspaceServices` + - `defaultServicesProfile` + - `autoHarvestOwnedUrls` +- `cleanupPolicy` + - `mode` + - `manual` + - `when_issue_terminal` + - `when_pr_closed` + - `retention_window` + - `retentionHours` + - `keepWhilePreviewHealthy` + - `keepWhileOpenPrExists` + +## 4. Issue Workspace Binding + +Issue-level selection of execution behavior. + +This should remain lightweight in the normal case and only surface richer controls when relevant. + +### Motivation + +Not every issue in a code project should create a new derived workspace. + +Examples: + +- a tiny fix can run in the shared workspace +- three related issues may intentionally share one integration branch +- a solo operator may be working directly on `master` + +### Proposed fields on `issues` + +- `projectWorkspaceId: uuid | null` +- `executionWorkspacePreference` + - `inherit` + - `shared_workspace` + - `isolated_workspace` + - `operator_branch` + - `reuse_existing` +- `preferredExecutionWorkspaceId: uuid | null` +- `executionWorkspaceSettings` + - keep advanced per-issue override fields here + +### Rules + +- if the project has no workspace automation, these fields may all be null +- if the project has one primary workspace, issue creation should default to it silently +- `reuse_existing` is advanced-only and should target active execution workspaces, not the whole workspace universe + +## 5. Execution Workspace + +A durable record for a shared or derived runtime workspace. + +This is the missing object that makes cleanup, previews, PRs, and branch reuse tractable. + +### Motivation + +Without an explicit `execution workspace` record, Paperclip has nowhere stable to attach: + +- derived branch/worktree identity +- active preview ownership +- PR linkage +- cleanup state +- "reuse this existing integration branch" behavior + +### Proposed new object + +`execution_workspaces` + +### Proposed fields + +- `id` +- `companyId` +- `projectId` +- `projectWorkspaceId` +- `sourceIssueId` +- `mode` + - `shared_workspace` + - `isolated_workspace` + - `operator_branch` + - `adapter_managed` +- `strategyType` + - `project_primary` + - `git_worktree` + - `adapter_managed` +- `name` +- `status` + - `active` + - `idle` + - `in_review` + - `archived` + - `cleanup_failed` +- `cwd` +- `repoUrl` +- `baseRef` +- `branchName` +- `providerRef` +- `derivedFromExecutionWorkspaceId` +- `lastUsedAt` +- `openedAt` +- `closedAt` +- `cleanupEligibleAt` +- `cleanupReason` +- `metadata` +- `createdAt` +- `updatedAt` + +### Notes + +- `sourceIssueId` is the issue that originally caused the workspace to be created, not necessarily the only issue linked to it later. +- multiple issues may link to the same execution workspace in a long-lived branch workflow. + +## 6. Issue-to-Execution Workspace Link + +An issue may need to link to one or more execution workspaces over time. + +Examples: + +- an issue begins in a shared workspace and later moves to an isolated one +- a failed attempt is archived and a new workspace is created +- several issues intentionally share one operator branch workspace + +### Proposed object + +`issue_execution_workspaces` + +### Proposed fields + +- `issueId` +- `executionWorkspaceId` +- `relationType` + - `current` + - `historical` + - `preferred` +- `createdAt` +- `updatedAt` + +### UI simplification + +Most issues should only show one current workspace in the main UI. Historical links belong in advanced/history views. + +## 7. Work Product + +User-facing umbrella concept for outputs of work. + +### Motivation + +Paperclip needs a single place to show: + +- "here is the preview" +- "here is the PR" +- "here is the branch" +- "here is the commit" +- "here is the artifact/report/doc" + +without turning issues into a raw dump of adapter details. + +### Proposed new object + +`issue_work_products` + +### Proposed fields + +- `id` +- `companyId` +- `projectId` +- `issueId` +- `executionWorkspaceId` +- `runtimeServiceId` +- `type` + - `preview_url` + - `runtime_service` + - `pull_request` + - `branch` + - `commit` + - `artifact` + - `document` +- `provider` + - `paperclip` + - `github` + - `gitlab` + - `vercel` + - `netlify` + - `custom` +- `externalId` +- `title` +- `url` +- `status` + - `active` + - `ready_for_review` + - `merged` + - `closed` + - `failed` + - `archived` +- `reviewState` + - `none` + - `needs_board_review` + - `approved` + - `changes_requested` +- `isPrimary` +- `healthStatus` + - `unknown` + - `healthy` + - `unhealthy` +- `summary` +- `metadata` +- `createdByRunId` +- `createdAt` +- `updatedAt` + +### Behavior + +- PRs are stored here as `type=pull_request` +- previews are stored here as `type=preview_url` or `runtime_service` +- Paperclip-owned processes should update health/status automatically +- external providers should at least store link, provider, external id, and latest known state + +## Page and UI Model + +## 1. Global Navigation + +Do not add `Workspaces` as a top-level sidebar item in V1. + +### Motivation + +That would make the whole product feel infra-heavy, even for companies that do not use code automation. + +### Global nav remains + +- Dashboard +- Inbox +- Companies +- Agents +- Goals +- Projects +- Issues +- Approvals + +Workspaces and work product should be surfaced through project and issue detail views. + +## 2. Project Detail + +Add a project sub-navigation that keeps planning first and code second. + +### Tabs + +- `Overview` +- `Issues` +- `Code` +- `Activity` + +Optional future: + +- `Outputs` + +### `Overview` tab + +Planning-first summary: + +- project status +- goals +- lead +- issue counts +- top-level progress +- latest major work product summaries + +### `Issues` tab + +- default to top-level issues only +- show parent issue rollups: + - child count + - `x/y` done + - active preview/PR badges +- optional toggle: `Show subissues` + +### `Code` tab + +This is the main workspace configuration and visibility surface. + +#### Section: `Project Workspaces` + +List durable project workspaces for the project. + +Card/list columns: + +- workspace name +- source type +- path or repo +- default ref +- primary/default badge +- active execution workspaces count +- active issue count +- active preview count + +Actions: + +- `Add workspace` +- `Edit` +- `Set default` +- `Archive` + +#### Section: `Execution Defaults` + +Fields: + +- `Enable workspace automation` +- `Default issue execution mode` + - `Shared workspace` + - `Isolated workspace` + - `Operator branch` + - `Adapter default` +- `Default codebase` +- `Allow issue override` + +#### Section: `Provisioning` + +Fields: + +- `Setup command` +- `Cleanup command` +- `Implementation` + - `Shared workspace` + - `Git worktree` + - `Adapter-managed` +- `Base ref` +- `Branch naming template` +- `Derived workspace parent directory` + +Hide git-specific fields when the selected workspace is not git-backed. + +#### Section: `Pull Requests` + +Fields: + +- `PR workflow` + - `Disabled` + - `Manual` + - `Agent may open draft PR` + - `Approval required to open PR` + - `Approval required to mark ready` +- `Default base branch` +- `PR title template` +- `PR body template` + +#### Section: `Previews and Runtime` + +Fields: + +- `Allow workspace runtime services` +- `Default services profile` +- `Harvest owned preview URLs` +- `Track external preview URLs` + +#### Section: `Cleanup` + +Fields: + +- `Cleanup mode` + - `Manual` + - `When issue is terminal` + - `When PR closes` + - `After retention window` +- `Retention window` +- `Keep while preview is active` +- `Keep while PR is open` + +## 3. Add Project Workspace Flow + +Entry point: `Project > Code > Add workspace` + +### Form fields + +- `Name` +- `Source type` + - `Local folder` + - `Git repo` + - `Non-git folder` + - `Remote managed` +- `Local path` +- `Repository URL` +- `Default ref` +- `Set as default workspace` +- `Setup command` +- `Cleanup command` + +### Behavior + +- if source type is non-git, hide branch/PR-specific setup +- if source type is git, show ref and optional advanced branch fields +- for simple solo users, this can be one path field and one save button + +## 4. Issue Create Flow + +Issue creation should stay simple by default. + +### Default behavior + +If the selected project: + +- has no workspace automation: show no workspace UI +- has one default project workspace and default execution mode: inherit silently + +### Show a `Workspace` section only when relevant + +#### Basic fields + +- `Codebase` + - default selected project workspace +- `Execution mode` + - `Project default` + - `Shared workspace` + - `Isolated workspace` + - `Operator branch` + +#### Advanced-only field + +- `Reuse existing execution workspace` + +This dropdown should show only active execution workspaces for the selected project workspace, with labels like: + +- `dotta/integration-branch` +- `PAP-447-add-worktree-support` +- `shared primary workspace` + +### Important rule + +Do not show a picker containing every possible workspace object by default. + +The normal flow should feel like: + +- choose project +- optionally choose codebase +- optionally choose execution mode + +not: + +- choose from a long mixed list of roots, derived worktrees, previews, and branch names + +## 5. Issue Detail + +Issue detail should expose workspace and work product clearly, but without becoming a code host UI. + +### Header chips + +Show compact summary chips near the title/status area: + +- `Codebase: Web App` +- `Workspace: Shared` +- `Workspace: PAP-447-add-worktree-support` +- `PR: Open` +- `Preview: Healthy` + +### Tabs + +- `Comments` +- `Subissues` +- `Work Product` +- `Activity` + +### `Work Product` tab + +Sections: + +- `Current workspace` +- `Previews` +- `Pull requests` +- `Branches and commits` +- `Artifacts and documents` + +#### Current workspace panel + +Fields: + +- workspace name +- mode +- branch +- base ref +- last used +- linked issues count +- cleanup status + +Actions: + +- `Open workspace details` +- `Mark in review` +- `Request cleanup` + +#### Pull request cards + +Fields: + +- title +- provider +- status +- review state +- linked branch +- open/ready/merged timestamps + +Actions: + +- `Open PR` +- `Refresh status` +- `Request board review` + +#### Preview cards + +Fields: + +- title +- URL +- provider +- health +- ownership +- updated at + +Actions: + +- `Open preview` +- `Refresh` +- `Archive` + +## 6. Execution Workspace Detail + +This can be reached from a project code tab or an issue work product tab. + +It does not need to be in the main sidebar. + +### Sections + +- identity +- source issue +- linked issues +- branch/ref +- active runtime services +- previews +- PRs +- cleanup state +- event/activity history + +### Motivation + +This is where advanced users go when they need to inspect the mechanics. Most users should not need it in normal flow. + +## 7. Inbox Behavior + +Inbox should surface actionable work product events, not every implementation detail. + +### Show inbox items for + +- issue assigned or updated +- PR needs board review +- PR opened or marked ready +- preview unhealthy +- workspace cleanup failed +- runtime service failed + +### Do not show by default + +- every workspace heartbeat +- every branch update +- every derived workspace creation + +### Display style + +If the inbox item is about a preview or PR, show issue context with it: + +- issue identifier and title +- parent issue if this is a subissue +- workspace name if relevant + +## 8. Issues List and Kanban + +Keep list and board planning-first. + +### Default behavior + +- show top-level issues by default +- show parent rollups for subissues +- do not flatten every child execution detail into the main board + +### Row/card adornments + +For issues with linked work product, show compact badges: + +- `1 PR` +- `2 previews` +- `shared workspace` +- `isolated workspace` + +### Optional advanced filters + +- `Has PR` +- `Has preview` +- `Workspace mode` +- `Codebase` + +## Behavior Rules + +## 1. Cleanup must not depend on agents remembering `in_review` + +Agents may still use `in_review`, but cleanup behavior must be governed by policy and observed state. + +### Keep an execution workspace alive while any of these are true + +- a linked issue is non-terminal +- a linked PR is open +- a linked preview/runtime service is active +- the workspace is still within retention window + +### Hide instead of deleting aggressively + +Archived or idle workspaces should be hidden from default lists before they are hard-cleaned up. + +## 2. Multiple issues may intentionally share one execution workspace + +This is how Paperclip supports: + +- solo dev on a shared branch +- operator integration branches +- related features batched into one PR + +This is the key reason not to force 1 issue = 1 workspace = 1 PR. + +## 3. Isolated issue workspaces remain opt-in + +Even in a git-heavy project, isolated workspaces should be optional. + +Examples where shared mode is valid: + +- tiny bug fixes +- branchless prototyping +- non-git projects +- single-user local workflows + +## 4. PR policy belongs to git-backed workspace policy + +PR automation decisions should be made at the project/workspace policy layer. + +The issue should only: + +- surface the resulting PR +- route approvals/review requests +- show status and review state + +## 5. Work product is the user-facing unifier + +Previews, PRs, commits, and artifacts should all be discoverable through one consistent issue-level affordance. + +That keeps Paperclip focused on coordination and visibility instead of splitting outputs across many hidden subsystems. + +## Recommended Implementation Order + +## Phase 1: Clarify current objects in UI + +1. Surface `Project > Code` tab +2. Show existing project workspaces there +3. Re-enable project-level execution workspace policy with revised copy +4. Keep issue creation simple with inherited defaults + +## Phase 2: Add explicit execution workspace record + +1. Add `execution_workspaces` +2. Link runs, issues, previews, and PRs to it +3. Add simple execution workspace detail page + +## Phase 3: Add work product model + +1. Add `issue_work_products` +2. Ingest PRs, previews, branches, commits +3. Add issue `Work Product` tab +4. Add inbox items for actionable work product state changes + +## Phase 4: Add advanced reuse and cleanup workflows + +1. Add `reuse existing execution workspace` +2. Add cleanup lifecycle UI +3. Add operator branch workflow shortcuts +4. Add richer external preview harvesting + +## Why This Model Is Right + +This model keeps the product balanced: + +- simple enough for solo users +- strong enough for real engineering teams +- flexible for non-git projects +- explicit enough to govern PRs and previews + +Most importantly, it keeps the abstractions clean: + +- projects plan the work +- project workspaces define the durable codebases +- execution workspaces define where work ran +- work product defines what came out of the work +- PRs remain outputs, not the core task model + +That is a better fit for Paperclip than either extreme: + +- hiding workspace behavior until nobody understands it +- or making the whole app revolve around code-host mechanics From 752a53e38e25e41b52d569ae295831e12287b1ea Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 09:06:49 -0500 Subject: [PATCH 02/33] Expand workspace plan for migration and cloud execution --- ...orkspace-product-model-and-work-product.md | 169 +++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/doc/plans/workspace-product-model-and-work-product.md b/doc/plans/workspace-product-model-and-work-product.md index 3b201e7d..ae5b8e79 100644 --- a/doc/plans/workspace-product-model-and-work-product.md +++ b/doc/plans/workspace-product-model-and-work-product.md @@ -38,6 +38,8 @@ The main product risk is overloading one concept to do too much: - commits - documents and artifacts 5. Keep the main navigation and task board simple. +6. Seamlessly upgrade existing Paperclip users to the new model without forcing disruptive reconfiguration. +7. Support cloud-hosted Paperclip deployments where execution happens in remote or adapter-managed environments rather than local workers. ## Non-Goals @@ -45,6 +47,7 @@ The main product risk is overloading one concept to do too much: - Requiring every issue to have its own branch or PR - Requiring every project to configure code/workspace automation - Making workspaces a top-level global navigation primitive in V1 +- Requiring a local filesystem path or local git checkout to use workspace-aware execution ## Core Product Decisions @@ -86,6 +89,7 @@ Examples: - an isolated git worktree - a long-lived operator branch checkout - an adapter-managed remote sandbox +- a cloud agent provider's isolated branch/session environment This object must be recorded explicitly so that Paperclip can: @@ -107,7 +111,38 @@ Paperclip should treat PRs as a type of work product linked back to: Git-specific automation should live under workspace policy, not under the core issue abstraction. -### 5. Subissues remain planning and ownership structure +### 5. Existing users must upgrade automatically + +Paperclip already has users and existing project/task data. Any new model must preserve continuity. + +The product should default existing installs into a sensible compatibility mode: + +- existing projects without workspace configuration continue to work unchanged +- existing `project_workspaces` become the durable `project workspace` objects +- existing project execution workspace policy is mapped forward rather than discarded +- issues without explicit workspace fields continue to inherit current behavior + +This migration should feel additive, not like a mandatory re-onboarding flow. + +### 6. Cloud-hosted Paperclip must be a first-class deployment mode + +Paperclip cannot assume that it is running on the same machine as the code. + +In cloud deployments, Paperclip may: + +- run on Vercel or another serverless host +- have no long-lived local worker process +- delegate execution to a remote coding agent or provider-managed sandbox +- receive back a branch, PR, preview URL, or artifact from that remote environment + +The model therefore must be portable: + +- `project workspace` may be remote-managed, not local +- `execution workspace` may have no local `cwd` +- `runtime services` may be tracked by provider reference and URL rather than a host process +- work product harvesting must handle externally owned previews and PRs + +### 7. Subissues remain planning and ownership structure Subissues are for decomposition and parallel ownership. @@ -131,6 +166,11 @@ Use these terms consistently in product copy: - `Work product`: previews, PRs, branches, commits, artifacts, docs - `Runtime service`: a process or service Paperclip owns or tracks for a workspace +Use these terms consistently in migration and deployment messaging: + +- `Compatible mode`: existing behavior preserved without new workspace automation +- `Adapter-managed workspace`: workspace realized by a remote or cloud execution provider + Avoid teaching users that "workspace" always means "git worktree on my machine". ## Product Object Model @@ -176,6 +216,7 @@ from: - "what temporary execution environment did this issue run in?" That keeps the model simple for solo users while still supporting advanced automation. +It also lets cloud-hosted Paperclip deployments point at codebases and remotes without pretending the Paperclip host has direct filesystem access. ### Proposed fields @@ -206,6 +247,7 @@ That keeps the model simple for solo users while still supporting advanced autom - `sourceType=non_git_path` is important so non-git projects are first-class. - `setupCommand` and `cleanupCommand` should be allowed here for workspace-root bootstrap, even when isolated execution is not used. - For a monorepo, multiple project workspaces may point at different roots or packages under one repo. +- `sourceType=remote_managed` is important for cloud deployments where the durable codebase is defined by provider/repo metadata rather than a local checkout path. ## 3. Project Execution Workspace Policy @@ -220,6 +262,7 @@ This lets Paperclip support: - direct editing in a shared workspace - isolated workspaces for issue parallelism - long-lived integration branch workflows +- remote cloud-agent execution that returns a branch or PR without forcing every issue or agent to expose low-level runtime configuration. @@ -305,6 +348,7 @@ Examples: - if the project has no workspace automation, these fields may all be null - if the project has one primary workspace, issue creation should default to it silently - `reuse_existing` is advanced-only and should target active execution workspaces, not the whole workspace universe +- existing issues without these fields should behave as `inherit` during migration ## 5. Execution Workspace @@ -321,6 +365,7 @@ Without an explicit `execution workspace` record, Paperclip has nowhere stable t - PR linkage - cleanup state - "reuse this existing integration branch" behavior +- remote provider session identity ### Proposed new object @@ -354,6 +399,11 @@ Without an explicit `execution workspace` record, Paperclip has nowhere stable t - `baseRef` - `branchName` - `providerRef` +- `providerType` + - `local_fs` + - `git_worktree` + - `adapter_managed` + - `cloud_sandbox` - `derivedFromExecutionWorkspaceId` - `lastUsedAt` - `openedAt` @@ -368,6 +418,7 @@ Without an explicit `execution workspace` record, Paperclip has nowhere stable t - `sourceIssueId` is the issue that originally caused the workspace to be created, not necessarily the only issue linked to it later. - multiple issues may link to the same execution workspace in a long-lived branch workflow. +- `cwd` may be null for remote execution workspaces; provider identity and work product links still make the object useful. ## 6. Issue-to-Execution Workspace Link @@ -473,6 +524,7 @@ without turning issues into a raw dump of adapter details. - previews are stored here as `type=preview_url` or `runtime_service` - Paperclip-owned processes should update health/status automatically - external providers should at least store link, provider, external id, and latest known state +- cloud agents should be able to create work product records without Paperclip owning the execution host ## Page and UI Model @@ -550,6 +602,7 @@ Card/list columns: - active execution workspaces count - active issue count - active preview count +- hosting type / provider when remote-managed Actions: @@ -586,6 +639,7 @@ Fields: - `Derived workspace parent directory` Hide git-specific fields when the selected workspace is not git-backed. +Hide local-path-specific fields when the selected workspace is remote-managed. #### Section: `Pull Requests` @@ -637,6 +691,8 @@ Entry point: `Project > Code > Add workspace` - `Remote managed` - `Local path` - `Repository URL` +- `Remote provider` +- `Remote workspace reference` - `Default ref` - `Set as default workspace` - `Setup command` @@ -646,6 +702,7 @@ Entry point: `Project > Code > Add workspace` - if source type is non-git, hide branch/PR-specific setup - if source type is git, show ref and optional advanced branch fields +- if source type is remote-managed, show provider/reference fields and hide local-path-only configuration - for simple solo users, this can be one path field and one save button ## 4. Issue Create Flow @@ -695,6 +752,10 @@ not: - choose from a long mixed list of roots, derived worktrees, previews, and branch names +### Migration rule + +For existing users, issue creation should continue to look the same until a project explicitly enables richer workspace behavior. + ## 5. Issue Detail Issue detail should expose workspace and work product clearly, but without becoming a code host UI. @@ -790,6 +851,7 @@ It does not need to be in the main sidebar. - source issue - linked issues - branch/ref +- provider/session identity - active runtime services - previews - PRs @@ -812,6 +874,7 @@ Inbox should surface actionable work product events, not every implementation de - preview unhealthy - workspace cleanup failed - runtime service failed +- remote cloud-agent run returned PR or preview that needs review ### Do not show by default @@ -853,6 +916,101 @@ For issues with linked work product, show compact badges: - `Workspace mode` - `Codebase` +## Upgrade and Migration Plan + +## 1. Product-level migration stance + +Migration must be silent-by-default and compatibility-preserving. + +Existing users should not be forced to: + +- create new workspace objects by hand before they can keep working +- re-tag old issues +- learn new workspace concepts before basic issue flows continue to function + +## 2. Existing project migration + +On upgrade: + +- existing `project_workspaces` records are retained and shown as `Project Workspaces` +- the current primary workspace remains the default codebase +- existing project execution workspace policy is mapped into the new `Project Execution Workspace Policy` surface +- projects with no execution workspace policy stay in compatible/shared mode + +## 3. Existing issue migration + +On upgrade: + +- existing issues default to `executionWorkspacePreference=inherit` +- if an issue already has execution workspace settings, map them forward directly +- if an issue has no explicit workspace data, preserve existing behavior and do not force a user-visible choice + +## 4. Existing run/runtime migration + +On upgrade: + +- active or recent runtime services can be backfilled into execution workspace history where feasible +- missing history should not block rollout; forward correctness matters more than perfect historical reconstruction + +## 5. Rollout UX + +Use additive language in the UI: + +- `Code` +- `Workspace automation` +- `Optional` +- `Advanced` + +Avoid migration copy that implies users were previously using the product "wrong". + +## Cloud Deployment Requirements + +## 1. Paperclip host and execution host must be decoupled + +Paperclip may run: + +- locally with direct filesystem access +- in a cloud app host such as Vercel +- in a hybrid setup with external job runners + +The workspace model must work in all three. + +## 2. Remote execution must support first-class work product reporting + +A cloud agent should be able to: + +- resolve a project workspace +- realize an adapter-managed execution workspace remotely +- produce a branch +- open or update a PR +- emit preview URLs +- register artifacts + +without the Paperclip host itself running local git or local preview processes. + +## 3. Local-only assumptions must be optional + +The following must be optional, not required: + +- local `cwd` +- local git CLI +- host-managed worktree directories +- host-owned long-lived preview processes + +## 4. Same product surface, different provider behavior + +The UI should not split into "local mode" and "cloud mode" products. + +Instead: + +- local projects show path/git implementation details +- cloud projects show provider/reference details +- both surface the same high-level objects: + - project workspace + - execution workspace + - work product + - runtime service or preview + ## Behavior Rules ## 1. Cleanup must not depend on agents remembering `in_review` @@ -921,6 +1079,7 @@ That keeps Paperclip focused on coordination and visibility instead of splitting 1. Add `execution_workspaces` 2. Link runs, issues, previews, and PRs to it 3. Add simple execution workspace detail page +4. Make `cwd` optional and ensure provider-managed remote workspaces are supported from day one ## Phase 3: Add work product model @@ -928,6 +1087,7 @@ That keeps Paperclip focused on coordination and visibility instead of splitting 2. Ingest PRs, previews, branches, commits 3. Add issue `Work Product` tab 4. Add inbox items for actionable work product state changes +5. Support remote agent-created PR/preview reporting without local ownership ## Phase 4: Add advanced reuse and cleanup workflows @@ -935,6 +1095,7 @@ That keeps Paperclip focused on coordination and visibility instead of splitting 2. Add cleanup lifecycle UI 3. Add operator branch workflow shortcuts 4. Add richer external preview harvesting +5. Add migration tooling/backfill where it improves continuity for existing users ## Why This Model Is Right @@ -953,6 +1114,12 @@ Most importantly, it keeps the abstractions clean: - work product defines what came out of the work - PRs remain outputs, not the core task model +It also keeps the rollout practical: + +- existing users can upgrade without workflow breakage +- local-first installs stay simple +- cloud-hosted Paperclip deployments remain first-class + That is a better fit for Paperclip than either extreme: - hiding workspace behavior until nobody understands it From 25d3bf2c64eaad86f537379c9af2becf9832c42e Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 09:41:12 -0500 Subject: [PATCH 03/33] Incorporate Worktrunk patterns into workspace plan --- ...orkspace-product-model-and-work-product.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/doc/plans/workspace-product-model-and-work-product.md b/doc/plans/workspace-product-model-and-work-product.md index ae5b8e79..25c8c464 100644 --- a/doc/plans/workspace-product-model-and-work-product.md +++ b/doc/plans/workspace-product-model-and-work-product.md @@ -1011,6 +1011,143 @@ Instead: - work product - runtime service or preview +## Patterns Learned from Worktrunk + +Worktrunk is a useful reference point because it is unapologetically focused on git-worktree-based developer workflows. + +Paperclip should not copy its product framing wholesale, but there are several good patterns worth applying. + +References: + +- `https://worktrunk.dev/tips-patterns/` +- `https://github.com/max-sixty/worktrunk` + +## 1. Deterministic per-workspace resources + +Worktrunk treats a derived workspace as something that can deterministically own: + +- ports +- local URLs +- databases +- runtime process identity + +This is a strong pattern for Paperclip. + +### Recommendation + +Execution workspaces should be able to deterministically derive and expose: + +- preview URLs +- port allocations +- database/schema names +- runtime service reuse keys + +This makes previews and local runtime services more predictable and easier to manage across many parallel workspaces. + +## 2. Lifecycle hooks should stay simple and explicit + +Worktrunk uses practical lifecycle hooks such as create/start/remove/merge-oriented commands. + +The main lesson is not to build a huge workflow engine. The lesson is to give users a few well-defined lifecycle moments to attach automation to. + +### Recommendation + +Paperclip should keep workspace automation centered on a small set of hooks: + +- `setup` +- `cleanup` +- optionally `before_review` +- optionally `after_merge` or `after_close` + +These should remain project/workspace policy concerns, not agent-prompt conventions. + +## 3. Workspace status visibility is a real product feature + +Worktrunk's listing/status experience is doing important product work: + +- which workspaces exist +- what branch they are on +- what services or URLs they own +- whether they are active or stale + +### Recommendation + +Paperclip should provide the equivalent visibility in the project `Code` surface: + +- active execution workspaces +- linked issues +- linked PRs +- linked previews/runtime services +- cleanup eligibility + +This reinforces why `execution workspace` needs to be a first-class recorded object. + +## 4. Execution workspaces are runtime islands, not just checkouts + +One of Worktrunk's strongest implicit ideas is that a worktree is not only code. It often owns an entire local runtime environment. + +### Recommendation + +Paperclip should treat execution workspaces as the natural home for: + +- dev servers +- preview processes +- sandbox credentials or provider references +- branch/ref identity +- local or remote environment bootstrap + +This supports the `work product` model and the preview/runtime service model proposed above. + +## 5. Machine-readable workspace state matters + +Worktrunk exposes structured state that can be consumed by tools and automation. + +### Recommendation + +Paperclip should ensure that execution workspaces and work product have clean structured API surfaces, not just UI-only representation. + +That is important for: + +- agents +- CLIs +- dashboards +- future automation and cleanup tooling + +## 6. Cleanup should be first-class, not an afterthought + +Worktrunk makes create/remove/merge cleanup part of the workflow. + +### Recommendation + +Paperclip should continue treating cleanup policy as part of the core workspace model: + +- when is cleanup allowed +- what blocks cleanup +- what gets archived versus destroyed +- what happens when cleanup fails + +This validates the explicit cleanup policy proposed earlier in this plan. + +## 7. What not to copy + +There are also important limits to the analogy. + +Paperclip should not adopt these Worktrunk assumptions as universal product rules: + +- every execution workspace is a local git worktree +- the Paperclip host has direct shell and filesystem access +- every workflow is merge-centric +- every user wants developer-tool-level workspace detail in the main navigation + +### Product implication + +Paperclip should borrow Worktrunk's good execution patterns while keeping the broader Paperclip model: + +- project plans the work +- workspace defines where work happens +- work product defines what came out +- git worktree remains one implementation strategy, not the product itself + ## Behavior Rules ## 1. Cleanup must not depend on agents remembering `in_review` From 9da5358bb36d9f3c7ac8b36c0c0cb91aeafd253b Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 16:37:40 -0500 Subject: [PATCH 04/33] Add workspace technical implementation spec --- .../workspace-technical-implementation.md | 882 ++++++++++++++++++ 1 file changed, 882 insertions(+) create mode 100644 doc/plans/workspace-technical-implementation.md diff --git a/doc/plans/workspace-technical-implementation.md b/doc/plans/workspace-technical-implementation.md new file mode 100644 index 00000000..c60bc019 --- /dev/null +++ b/doc/plans/workspace-technical-implementation.md @@ -0,0 +1,882 @@ +# Workspace Technical Implementation Spec + +## Role of This Document + +This document translates [workspace-product-model-and-work-product.md](/Users/dotta/paperclip-subissues/doc/plans/workspace-product-model-and-work-product.md) into an implementation-ready engineering plan. + +It is intentionally concrete: + +- schema and migration shape +- shared contract updates +- route and service changes +- UI changes +- rollout and compatibility rules + +This is the implementation target for the first workspace-aware delivery slice. + +## Locked Decisions + +These decisions are treated as settled for this implementation: + +1. Add a new durable `execution_workspaces` table now. +2. Each issue has at most one current execution workspace at a time. +3. `issues` get explicit `project_workspace_id` and `execution_workspace_id`. +4. Workspace reuse is in scope for V1. +5. The feature is gated in the UI by `/instance/settings > Experimental > Workspaces`. +6. The gate is UI-only. Backend model changes and migrations always ship. +7. Existing users upgrade into compatibility-preserving defaults. +8. `project_workspaces` evolves in place rather than being replaced. +9. Work product is issue-first, with optional links to execution workspaces and runtime services. +10. GitHub is the only PR provider in the first slice. +11. Both `adapter_managed` and `cloud_sandbox` execution modes are in scope. +12. Workspace controls ship first inside existing project properties, not in a new global navigation area. +13. Subissues are out of scope for this implementation slice. + +## Non-Goals + +- Building a full code review system +- Solving subissue UX in this slice +- Implementing reusable shared workspace definitions across projects in this slice +- Reworking all current runtime service behavior before introducing execution workspaces + +## Existing Baseline + +The repo already has: + +- `project_workspaces` +- `projects.execution_workspace_policy` +- `issues.execution_workspace_settings` +- runtime service persistence in `workspace_runtime_services` +- local git-worktree realization in `workspace-runtime.ts` + +This implementation should build on that baseline rather than fork it. + +## Terminology + +- `Project workspace`: durable configured codebase/root for a project +- `Execution workspace`: actual runtime workspace used for one or more issues +- `Work product`: user-facing output such as PR, preview, branch, commit, artifact, document +- `Runtime service`: process or service owned or tracked for a workspace +- `Compatibility mode`: existing behavior preserved for upgraded installs with no explicit workspace opt-in + +## Architecture Summary + +The first slice should introduce three explicit layers: + +1. `Project workspace` + - existing durable project-scoped codebase record + - extended to support local, git, non-git, and remote-managed shapes + +2. `Execution workspace` + - new durable runtime record + - represents shared, isolated, operator-branch, or remote-managed execution context + +3. `Issue work product` + - new durable output record + - stores PRs, previews, branches, commits, artifacts, and documents + +The issue remains the planning and ownership unit. +The execution workspace remains the runtime unit. +The work product remains the deliverable/output unit. + +## Configuration and Deployment Topology + +## Important correction + +This repo already uses `PAPERCLIP_DEPLOYMENT_MODE` for auth/deployment behavior (`local_trusted | authenticated`). + +Do not overload that variable for workspace execution topology. + +## New env var + +Add a separate execution-host hint: + +- `PAPERCLIP_EXECUTION_TOPOLOGY=local|cloud|hybrid` + +Default: + +- if unset, treat as `local` + +Purpose: + +- influences defaults and validation for workspace configuration +- does not change current auth/deployment semantics +- does not break existing installs + +### Semantics + +- `local` + - Paperclip may create host-local worktrees, processes, and paths +- `cloud` + - Paperclip should assume no durable host-local execution workspace management + - adapter-managed and cloud-sandbox flows should be treated as first-class +- `hybrid` + - both local and remote execution strategies may exist + +This is a guardrail and defaulting aid, not a hard policy engine in the first slice. + +## Instance Settings + +Add a new `Experimental` section under `/instance/settings`. + +### New setting + +- `experimental.workspaces: boolean` + +Rules: + +- default `false` +- UI-only gate +- stored in instance config or instance settings API response +- backend routes and migrations remain available even when false + +### UI behavior when off + +- hide workspace-specific issue controls +- hide workspace-specific project configuration +- hide issue `Work Product` tab if it would otherwise be empty +- do not remove or invalidate any stored workspace data + +## Data Model + +## 1. Extend `project_workspaces` + +Current table exists and should evolve in place. + +### New columns + +- `source_type text not null default 'local_path'` + - `local_path | git_repo | non_git_path | remote_managed` +- `default_ref text null` +- `visibility text not null default 'default'` + - `default | advanced` +- `setup_command text null` +- `cleanup_command text null` +- `remote_provider text null` + - examples: `github`, `openai`, `anthropic`, `custom` +- `remote_workspace_ref text null` +- `shared_workspace_key text null` + - reserved for future cross-project shared workspace definitions + +### Backfill rules + +- if existing row has `repo_url`, backfill `source_type='git_repo'` +- else if existing row has `cwd`, backfill `source_type='local_path'` +- else backfill `source_type='remote_managed'` +- copy existing `repo_ref` into `default_ref` + +### Indexes + +- retain current indexes +- add `(project_id, source_type)` +- add `(company_id, shared_workspace_key)` non-unique for future support + +## 2. Add `execution_workspaces` + +Create a new durable table. + +### Columns + +- `id uuid pk` +- `company_id uuid not null` +- `project_id uuid not null` +- `project_workspace_id uuid null` +- `source_issue_id uuid null` +- `mode text not null` + - `shared_workspace | isolated_workspace | operator_branch | adapter_managed | cloud_sandbox` +- `strategy_type text not null` + - `project_primary | git_worktree | adapter_managed | cloud_sandbox` +- `name text not null` +- `status text not null default 'active'` + - `active | idle | in_review | archived | cleanup_failed` +- `cwd text null` +- `repo_url text null` +- `base_ref text null` +- `branch_name text null` +- `provider_type text not null default 'local_fs'` + - `local_fs | git_worktree | adapter_managed | cloud_sandbox` +- `provider_ref text null` +- `derived_from_execution_workspace_id uuid null` +- `last_used_at timestamptz not null default now()` +- `opened_at timestamptz not null default now()` +- `closed_at timestamptz null` +- `cleanup_eligible_at timestamptz null` +- `cleanup_reason text null` +- `metadata jsonb null` +- `created_at timestamptz not null default now()` +- `updated_at timestamptz not null default now()` + +### Foreign keys + +- `company_id -> companies.id` +- `project_id -> projects.id` +- `project_workspace_id -> project_workspaces.id on delete set null` +- `source_issue_id -> issues.id on delete set null` +- `derived_from_execution_workspace_id -> execution_workspaces.id on delete set null` + +### Indexes + +- `(company_id, project_id, status)` +- `(company_id, project_workspace_id, status)` +- `(company_id, source_issue_id)` +- `(company_id, last_used_at desc)` +- `(company_id, branch_name)` non-unique + +## 3. Extend `issues` + +Add explicit workspace linkage. + +### New columns + +- `project_workspace_id uuid null` +- `execution_workspace_id uuid null` +- `execution_workspace_preference text null` + - `inherit | shared_workspace | isolated_workspace | operator_branch | reuse_existing` + +### Foreign keys + +- `project_workspace_id -> project_workspaces.id on delete set null` +- `execution_workspace_id -> execution_workspaces.id on delete set null` + +### Backfill rules + +- all existing issues get null values +- null should be interpreted as compatibility/inherit behavior + +### Invariants + +- if `project_workspace_id` is set, it must belong to the issue's project and company +- if `execution_workspace_id` is set, it must belong to the issue's company +- if `execution_workspace_id` is set, the referenced workspace's `project_id` must match the issue's `project_id` + +## 4. Add `issue_work_products` + +Create a new durable table for outputs. + +### Columns + +- `id uuid pk` +- `company_id uuid not null` +- `project_id uuid null` +- `issue_id uuid not null` +- `execution_workspace_id uuid null` +- `runtime_service_id uuid null` +- `type text not null` + - `preview_url | runtime_service | pull_request | branch | commit | artifact | document` +- `provider text not null` + - `paperclip | github | vercel | s3 | custom` +- `external_id text null` +- `title text not null` +- `url text null` +- `status text not null` + - `active | ready_for_review | approved | changes_requested | merged | closed | failed | archived` +- `review_state text not null default 'none'` + - `none | needs_board_review | approved | changes_requested` +- `is_primary boolean not null default false` +- `health_status text not null default 'unknown'` + - `unknown | healthy | unhealthy` +- `summary text null` +- `metadata jsonb null` +- `created_by_run_id uuid null` +- `created_at timestamptz not null default now()` +- `updated_at timestamptz not null default now()` + +### Foreign keys + +- `company_id -> companies.id` +- `project_id -> projects.id on delete set null` +- `issue_id -> issues.id on delete cascade` +- `execution_workspace_id -> execution_workspaces.id on delete set null` +- `runtime_service_id -> workspace_runtime_services.id on delete set null` +- `created_by_run_id -> heartbeat_runs.id on delete set null` + +### Indexes + +- `(company_id, issue_id, type)` +- `(company_id, execution_workspace_id, type)` +- `(company_id, provider, external_id)` +- `(company_id, updated_at desc)` + +## 5. Extend `workspace_runtime_services` + +This table already exists and should remain the system of record for owned/tracked services. + +### New column + +- `execution_workspace_id uuid null` + +### Foreign key + +- `execution_workspace_id -> execution_workspaces.id on delete set null` + +### Behavior + +- runtime services remain workspace-first +- issue UIs should surface them through linked execution workspaces and work products + +## Shared Contracts + +## 1. `packages/shared` + +### Update project workspace types and validators + +Add fields: + +- `sourceType` +- `defaultRef` +- `visibility` +- `setupCommand` +- `cleanupCommand` +- `remoteProvider` +- `remoteWorkspaceRef` +- `sharedWorkspaceKey` + +### Add execution workspace types and validators + +New shared types: + +- `ExecutionWorkspace` +- `ExecutionWorkspaceMode` +- `ExecutionWorkspaceStatus` +- `ExecutionWorkspaceProviderType` + +### Add work product types and validators + +New shared types: + +- `IssueWorkProduct` +- `IssueWorkProductType` +- `IssueWorkProductStatus` +- `IssueWorkProductReviewState` + +### Update issue types and validators + +Add: + +- `projectWorkspaceId` +- `executionWorkspaceId` +- `executionWorkspacePreference` +- `workProducts?: IssueWorkProduct[]` + +### Extend project execution policy contract + +Replace the current narrow policy with a more explicit shape: + +- `enabled` +- `defaultMode` + - `shared_workspace | isolated_workspace | operator_branch | adapter_default` +- `allowIssueOverride` +- `defaultProjectWorkspaceId` +- `workspaceStrategy` +- `branchPolicy` +- `pullRequestPolicy` +- `runtimePolicy` +- `cleanupPolicy` + +Do not try to encode every possible provider-specific field in V1. Keep provider-specific extensibility in nested JSON where needed. + +## Service Layer Changes + +## 1. Project service + +Update project workspace CRUD to handle the extended schema. + +### Required rules + +- when setting a primary workspace, clear `is_primary` on siblings +- `source_type=remote_managed` may have null `cwd` +- local/git-backed workspaces should still require one of `cwd` or `repo_url` +- preserve current behavior for existing callers that only send `cwd/repoUrl/repoRef` + +## 2. Issue service + +Update create/update flows to handle explicit workspace binding. + +### Create behavior + +Resolve defaults in this order: + +1. explicit `projectWorkspaceId` from request +2. `project.executionWorkspacePolicy.defaultProjectWorkspaceId` +3. project's primary workspace +4. null + +Resolve `executionWorkspacePreference`: + +1. explicit request field +2. project policy default +3. compatibility fallback to `inherit` + +Do not create an execution workspace at issue creation time unless: + +- `reuse_existing` is explicitly chosen and `executionWorkspaceId` is provided + +Otherwise, workspace realization happens when execution starts. + +### Update behavior + +- allow changing `projectWorkspaceId` only if the workspace belongs to the same project +- allow setting `executionWorkspaceId` only if it belongs to the same company and project +- do not automatically destroy or relink historical work products when workspace linkage changes + +## 3. Workspace realization service + +Refactor `workspace-runtime.ts` so realization produces or reuses an `execution_workspaces` row. + +### New flow + +Input: + +- issue +- project workspace +- project execution policy +- execution topology hint +- adapter/runtime configuration + +Output: + +- realized execution workspace record +- runtime cwd/provider metadata + +### Required modes + +- `shared_workspace` + - reuse a stable execution workspace representing the project primary/shared workspace +- `isolated_workspace` + - create or reuse a derived isolated execution workspace +- `operator_branch` + - create or reuse a long-lived branch workspace +- `adapter_managed` + - create an execution workspace with provider references and optional null `cwd` +- `cloud_sandbox` + - same as adapter-managed, but explicit remote sandbox semantics + +### Reuse rules + +When `reuse_existing` is requested: + +- only list active or recently used execution workspaces +- only for the same project +- only for the same project workspace if one is specified +- exclude archived and cleanup-failed workspaces + +### Shared workspace realization + +For compatibility mode and shared-workspace projects: + +- create a stable execution workspace per project workspace when first needed +- reuse it for subsequent runs + +This avoids a special-case branch in later work product linkage. + +## 4. Runtime service integration + +When runtime services are started or reused: + +- populate `execution_workspace_id` +- continue populating `project_workspace_id`, `project_id`, and `issue_id` + +When a runtime service yields a URL: + +- optionally create or update a linked `issue_work_products` row of type `runtime_service` or `preview_url` + +## 5. PR and preview reporting + +Add a service for creating/updating `issue_work_products`. + +### Supported V1 product types + +- `pull_request` +- `preview_url` +- `runtime_service` +- `branch` +- `commit` +- `artifact` +- `document` + +### GitHub PR reporting + +For V1, GitHub is the only provider with richer semantics. + +Supported statuses: + +- `draft` +- `ready_for_review` +- `approved` +- `changes_requested` +- `merged` +- `closed` + +Represent these in `status` and `review_state` rather than inventing a separate PR table in V1. + +## Routes and API + +## 1. Project workspace routes + +Extend existing routes: + +- `GET /projects/:id/workspaces` +- `POST /projects/:id/workspaces` +- `PATCH /projects/:id/workspaces/:workspaceId` +- `DELETE /projects/:id/workspaces/:workspaceId` + +### New accepted/returned fields + +- `sourceType` +- `defaultRef` +- `visibility` +- `setupCommand` +- `cleanupCommand` +- `remoteProvider` +- `remoteWorkspaceRef` + +## 2. Execution workspace routes + +Add: + +- `GET /companies/:companyId/execution-workspaces` + - filters: + - `projectId` + - `projectWorkspaceId` + - `status` + - `issueId` + - `reuseEligible=true` +- `GET /execution-workspaces/:id` +- `PATCH /execution-workspaces/:id` + - update status/metadata/cleanup fields only in V1 + +Do not add top-level navigation for these routes yet. + +## 3. Work product routes + +Add: + +- `GET /issues/:id/work-products` +- `POST /issues/:id/work-products` +- `PATCH /work-products/:id` +- `DELETE /work-products/:id` + +### V1 mutation permissions + +- board can create/update/delete all +- agents can create/update for issues they are assigned or currently executing +- deletion should generally archive rather than hard-delete once linked to historical output + +## 4. Issue routes + +Extend existing create/update payloads to accept: + +- `projectWorkspaceId` +- `executionWorkspacePreference` +- `executionWorkspaceId` + +Extend `GET /issues/:id` to return: + +- `projectWorkspaceId` +- `executionWorkspaceId` +- `executionWorkspacePreference` +- `currentExecutionWorkspace` +- `workProducts[]` + +## 5. Instance settings routes + +Add support for: + +- reading/writing `experimental.workspaces` + +This is a UI gate only. + +If there is no generic instance settings storage yet, the first slice can store this in the existing config/instance settings mechanism used by `/instance/settings`. + +## UI Changes + +## 1. `/instance/settings` + +Add section: + +- `Experimental` + - `Enable Workspaces` + +When off: + +- hide new workspace-specific affordances +- do not alter existing project or issue behavior + +## 2. Project properties + +Do not create a separate `Code` tab yet. +Ship inside existing project properties first. + +### Add or re-enable sections + +- `Project Workspaces` +- `Execution Defaults` +- `Provisioning` +- `Pull Requests` +- `Previews and Runtime` +- `Cleanup` + +### Display rules + +- only show when `experimental.workspaces=true` +- keep wording generic enough for local and remote setups +- only show git-specific fields when `sourceType=git_repo` +- only show local-path-specific fields when not `remote_managed` + +## 3. Issue create dialog + +When the workspace experimental flag is on and the selected project has workspace automation or workspaces: + +### Basic fields + +- `Codebase` + - select from project workspaces + - default to policy default or primary workspace +- `Execution mode` + - `Project default` + - `Shared workspace` + - `Isolated workspace` + - `Operator branch` + +### Advanced section + +- `Reuse existing execution workspace` + +This control should query only: + +- same project +- same codebase if selected +- active/recent workspaces +- compact labels with branch or workspace name + +Do not expose all execution workspaces in a noisy unfiltered list. + +## 4. Issue detail + +Add a `Work Product` tab when: + +- the experimental flag is on, or +- the issue already has work products + +### Show + +- current execution workspace summary +- PR cards +- preview cards +- branch/commit rows +- artifacts/documents + +Add compact header chips: + +- codebase +- workspace +- PR count/status +- preview status + +## 5. Execution workspace detail page + +Add a detail route but no nav item. + +Linked from: + +- issue work product tab +- project workspace/execution panels + +### Show + +- identity and status +- project workspace origin +- source issue +- linked issues +- branch/ref/provider info +- runtime services +- work products +- cleanup state + +## Runtime and Adapter Behavior + +## 1. Local adapters + +For local adapters: + +- continue to use existing cwd/worktree realization paths +- persist the result as execution workspaces +- attach runtime services and work product to the execution workspace and issue + +## 2. Remote or cloud adapters + +For remote adapters: + +- allow execution workspaces with null `cwd` +- require provider metadata sufficient to identify the remote workspace/session +- allow work product creation without any host-local process ownership + +Examples: + +- cloud coding agent opens a branch and PR on GitHub +- Vercel preview URL is reported back as a preview work product +- remote sandbox emits artifact URLs + +## 3. Approval-aware PR workflow + +V1 should support richer PR state tracking, but not a full review engine. + +### Required actions + +- `open_pr` +- `mark_ready` + +### Required review states + +- `draft` +- `ready_for_review` +- `approved` +- `changes_requested` +- `merged` +- `closed` + +### Storage approach + +- represent these as `issue_work_products` with `type='pull_request'` +- use `status` and `review_state` +- store provider-specific details in `metadata` + +## Migration Plan + +## 1. Existing installs + +The migration posture is backward-compatible by default. + +### Guarantees + +- no existing project must be edited before it keeps working +- no existing issue flow should start requiring workspace input +- all new nullable columns must preserve current behavior when absent + +## 2. Project workspace migration + +Migrate `project_workspaces` in place. + +### Backfill + +- derive `source_type` +- copy `repo_ref` to `default_ref` +- leave new optional fields null + +## 3. Issue migration + +Do not backfill `project_workspace_id` or `execution_workspace_id` on all existing issues. + +Reason: + +- the safest migration is to preserve current runtime behavior and bind explicitly only when new workspace-aware flows are used + +Interpret old issues as: + +- `executionWorkspacePreference = inherit` +- compatibility/shared behavior + +## 4. Runtime history migration + +Do not attempt a perfect historical reconstruction of execution workspaces in the migration itself. + +Instead: + +- create execution workspace records forward from first new run +- optionally add a later backfill tool for recent runtime services if it proves valuable + +## Rollout Order + +## Phase 1: Schema and shared contracts + +1. extend `project_workspaces` +2. add `execution_workspaces` +3. add `issue_work_products` +4. extend `issues` +5. extend `workspace_runtime_services` +6. update shared types and validators + +## Phase 2: Service wiring + +1. update project workspace CRUD +2. update issue create/update resolution +3. refactor workspace realization to persist execution workspaces +4. attach runtime services to execution workspaces +5. add work product service and persistence + +## Phase 3: API and UI + +1. add execution workspace routes +2. add work product routes +3. add instance experimental settings toggle +4. re-enable and revise project workspace UI behind the flag +5. add issue create/update controls behind the flag +6. add issue work product tab +7. add execution workspace detail page + +## Phase 4: Provider integrations + +1. GitHub PR reporting +2. preview URL reporting +3. runtime-service-to-work-product linking +4. remote/cloud provider references + +## Acceptance Criteria + +1. Existing installs continue to behave predictably with no required reconfiguration. +2. Projects can define local, git, non-git, and remote-managed project workspaces. +3. Issues can explicitly select a project workspace and execution preference. +4. Each issue can point to one current execution workspace. +5. Multiple issues can intentionally reuse the same execution workspace. +6. Execution workspaces are persisted for both local and remote execution flows. +7. Work products can be attached to issues with optional execution workspace linkage. +8. GitHub PRs can be represented with richer lifecycle states. +9. The main UI remains simple when the experimental flag is off. +10. No top-level workspace navigation is required for this first slice. + +## Risks and Mitigations + +## Risk: too many overlapping workspace concepts + +Mitigation: + +- keep issue UI to `Codebase` and `Execution mode` +- reserve execution workspace details for advanced pages + +## Risk: breaking current projects on upgrade + +Mitigation: + +- nullable schema additions +- in-place `project_workspaces` migration +- compatibility defaults + +## Risk: local-only assumptions leaking into cloud mode + +Mitigation: + +- make `cwd` optional for execution workspaces +- use `provider_type` and `provider_ref` +- use `PAPERCLIP_EXECUTION_TOPOLOGY` as a defaulting guardrail + +## Risk: turning PRs into a bespoke subsystem too early + +Mitigation: + +- represent PRs as work products in V1 +- keep provider-specific details in metadata +- defer a dedicated PR table unless usage proves it necessary + +## Recommended First Engineering Slice + +If we want the narrowest useful implementation: + +1. extend `project_workspaces` +2. add `execution_workspaces` +3. extend `issues` with explicit workspace fields +4. persist execution workspaces from existing local workspace realization +5. add `issue_work_products` +6. show project workspace controls and issue workspace controls behind the experimental flag +7. add issue `Work Product` tab with PR/preview/runtime service display + +This slice is enough to validate the model without yet building every provider integration or cleanup workflow. From 920bc4c70f730b93d0ccb7fdc28ef229d23ba1b3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 17:12:25 -0500 Subject: [PATCH 05/33] Implement execution workspaces and work products --- cli/src/commands/worktree.ts | 1 + packages/db/src/migration-runtime.ts | 1 + .../migrations/0028_unusual_the_hunter.sql | 91 + .../db/src/migrations/meta/0028_snapshot.json | 7125 +++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + .../db/src/schema/execution_workspaces.ts | 68 + packages/db/src/schema/index.ts | 2 + packages/db/src/schema/issue_work_products.ts | 64 + packages/db/src/schema/issues.ts | 8 + packages/db/src/schema/project_workspaces.ts | 13 + .../src/schema/workspace_runtime_services.ts | 7 + packages/shared/src/index.ts | 19 + packages/shared/src/types/index.ts | 11 + packages/shared/src/types/issue.ts | 8 +- packages/shared/src/types/project.ts | 11 + packages/shared/src/types/work-product.ts | 55 + .../shared/src/types/workspace-runtime.ts | 65 +- .../src/validators/execution-workspace.ts | 18 + packages/shared/src/validators/index.ts | 16 + packages/shared/src/validators/issue.ts | 14 +- packages/shared/src/validators/project.ts | 43 +- .../shared/src/validators/work-product.ts | 54 + .../execution-workspace-policy.test.ts | 32 +- server/src/app.ts | 2 + server/src/routes/execution-workspaces.ts | 68 + server/src/routes/issues.ts | 111 + .../services/execution-workspace-policy.ts | 65 +- server/src/services/execution-workspaces.ts | 99 + server/src/services/heartbeat.ts | 70 + server/src/services/index.ts | 2 + server/src/services/issues.ts | 73 + server/src/services/projects.ts | 57 +- server/src/services/work-products.ts | 113 + ui/src/App.tsx | 2 + ui/src/api/execution-workspaces.ts | 26 + ui/src/api/issues.ts | 8 +- ui/src/components/IssueProperties.tsx | 191 +- ui/src/components/NewIssueDialog.tsx | 238 +- ui/src/components/ProjectProperties.tsx | 15 +- ui/src/lib/experimentalSettings.ts | 39 + ui/src/lib/inbox.test.ts | 3 + ui/src/lib/queryKeys.ts | 6 + ui/src/pages/ExecutionWorkspaceDetail.tsx | 70 + ui/src/pages/InstanceSettings.tsx | 30 + ui/src/pages/IssueDetail.tsx | 276 +- 45 files changed, 9157 insertions(+), 140 deletions(-) create mode 100644 packages/db/src/migrations/0028_unusual_the_hunter.sql create mode 100644 packages/db/src/migrations/meta/0028_snapshot.json create mode 100644 packages/db/src/schema/execution_workspaces.ts create mode 100644 packages/db/src/schema/issue_work_products.ts create mode 100644 packages/shared/src/types/work-product.ts create mode 100644 packages/shared/src/validators/execution-workspace.ts create mode 100644 packages/shared/src/validators/work-product.ts create mode 100644 server/src/routes/execution-workspaces.ts create mode 100644 server/src/services/execution-workspaces.ts create mode 100644 server/src/services/work-products.ts create mode 100644 ui/src/api/execution-workspaces.ts create mode 100644 ui/src/lib/experimentalSettings.ts create mode 100644 ui/src/pages/ExecutionWorkspaceDetail.tsx diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index 7311793b..e807dafb 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -83,6 +83,7 @@ type EmbeddedPostgresCtor = new (opts: { password: string; port: number; persistent: boolean; + initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; diff --git a/packages/db/src/migration-runtime.ts b/packages/db/src/migration-runtime.ts index 10b7b9b1..e07bdf04 100644 --- a/packages/db/src/migration-runtime.ts +++ b/packages/db/src/migration-runtime.ts @@ -17,6 +17,7 @@ type EmbeddedPostgresCtor = new (opts: { password: string; port: number; persistent: boolean; + initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; diff --git a/packages/db/src/migrations/0028_unusual_the_hunter.sql b/packages/db/src/migrations/0028_unusual_the_hunter.sql new file mode 100644 index 00000000..e19de785 --- /dev/null +++ b/packages/db/src/migrations/0028_unusual_the_hunter.sql @@ -0,0 +1,91 @@ +CREATE TABLE "execution_workspaces" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "project_id" uuid NOT NULL, + "project_workspace_id" uuid, + "source_issue_id" uuid, + "mode" text NOT NULL, + "strategy_type" text NOT NULL, + "name" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "cwd" text, + "repo_url" text, + "base_ref" text, + "branch_name" text, + "provider_type" text DEFAULT 'local_fs' NOT NULL, + "provider_ref" text, + "derived_from_execution_workspace_id" uuid, + "last_used_at" timestamp with time zone DEFAULT now() NOT NULL, + "opened_at" timestamp with time zone DEFAULT now() NOT NULL, + "closed_at" timestamp with time zone, + "cleanup_eligible_at" timestamp with time zone, + "cleanup_reason" text, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "issue_work_products" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "project_id" uuid, + "issue_id" uuid NOT NULL, + "execution_workspace_id" uuid, + "runtime_service_id" uuid, + "type" text NOT NULL, + "provider" text NOT NULL, + "external_id" text, + "title" text NOT NULL, + "url" text, + "status" text NOT NULL, + "review_state" text DEFAULT 'none' NOT NULL, + "is_primary" boolean DEFAULT false NOT NULL, + "health_status" text DEFAULT 'unknown' NOT NULL, + "summary" text, + "metadata" jsonb, + "created_by_run_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "issues" ADD COLUMN "project_workspace_id" uuid;--> statement-breakpoint +ALTER TABLE "issues" ADD COLUMN "execution_workspace_id" uuid;--> statement-breakpoint +ALTER TABLE "issues" ADD COLUMN "execution_workspace_preference" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "source_type" text DEFAULT 'local_path' NOT NULL;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "default_ref" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "visibility" text DEFAULT 'default' NOT NULL;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "setup_command" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "cleanup_command" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "remote_provider" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "remote_workspace_ref" text;--> statement-breakpoint +ALTER TABLE "project_workspaces" ADD COLUMN "shared_workspace_key" text;--> statement-breakpoint +ALTER TABLE "workspace_runtime_services" ADD COLUMN "execution_workspace_id" uuid;--> statement-breakpoint +ALTER TABLE "execution_workspaces" ADD CONSTRAINT "execution_workspaces_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "execution_workspaces" ADD CONSTRAINT "execution_workspaces_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "execution_workspaces" ADD CONSTRAINT "execution_workspaces_project_workspace_id_project_workspaces_id_fk" FOREIGN KEY ("project_workspace_id") REFERENCES "public"."project_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "execution_workspaces" ADD CONSTRAINT "execution_workspaces_source_issue_id_issues_id_fk" FOREIGN KEY ("source_issue_id") REFERENCES "public"."issues"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "execution_workspaces" ADD CONSTRAINT "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk" FOREIGN KEY ("derived_from_execution_workspace_id") REFERENCES "public"."execution_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_execution_workspace_id_execution_workspaces_id_fk" FOREIGN KEY ("execution_workspace_id") REFERENCES "public"."execution_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk" FOREIGN KEY ("runtime_service_id") REFERENCES "public"."workspace_runtime_services"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issue_work_products" ADD CONSTRAINT "issue_work_products_created_by_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("created_by_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "execution_workspaces_company_project_status_idx" ON "execution_workspaces" USING btree ("company_id","project_id","status");--> statement-breakpoint +CREATE INDEX "execution_workspaces_company_project_workspace_status_idx" ON "execution_workspaces" USING btree ("company_id","project_workspace_id","status");--> statement-breakpoint +CREATE INDEX "execution_workspaces_company_source_issue_idx" ON "execution_workspaces" USING btree ("company_id","source_issue_id");--> statement-breakpoint +CREATE INDEX "execution_workspaces_company_last_used_idx" ON "execution_workspaces" USING btree ("company_id","last_used_at");--> statement-breakpoint +CREATE INDEX "execution_workspaces_company_branch_idx" ON "execution_workspaces" USING btree ("company_id","branch_name");--> statement-breakpoint +CREATE INDEX "issue_work_products_company_issue_type_idx" ON "issue_work_products" USING btree ("company_id","issue_id","type");--> statement-breakpoint +CREATE INDEX "issue_work_products_company_execution_workspace_type_idx" ON "issue_work_products" USING btree ("company_id","execution_workspace_id","type");--> statement-breakpoint +CREATE INDEX "issue_work_products_company_provider_external_id_idx" ON "issue_work_products" USING btree ("company_id","provider","external_id");--> statement-breakpoint +CREATE INDEX "issue_work_products_company_updated_idx" ON "issue_work_products" USING btree ("company_id","updated_at");--> statement-breakpoint +ALTER TABLE "issues" ADD CONSTRAINT "issues_project_workspace_id_project_workspaces_id_fk" FOREIGN KEY ("project_workspace_id") REFERENCES "public"."project_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issues" ADD CONSTRAINT "issues_execution_workspace_id_execution_workspaces_id_fk" FOREIGN KEY ("execution_workspace_id") REFERENCES "public"."execution_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_runtime_services" ADD CONSTRAINT "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk" FOREIGN KEY ("execution_workspace_id") REFERENCES "public"."execution_workspaces"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "issues_company_project_workspace_idx" ON "issues" USING btree ("company_id","project_workspace_id");--> statement-breakpoint +CREATE INDEX "issues_company_execution_workspace_idx" ON "issues" USING btree ("company_id","execution_workspace_id");--> statement-breakpoint +CREATE INDEX "project_workspaces_project_source_type_idx" ON "project_workspaces" USING btree ("project_id","source_type");--> statement-breakpoint +CREATE INDEX "project_workspaces_company_shared_key_idx" ON "project_workspaces" USING btree ("company_id","shared_workspace_key");--> statement-breakpoint +CREATE UNIQUE INDEX "project_workspaces_project_remote_ref_idx" ON "project_workspaces" USING btree ("project_id","remote_provider","remote_workspace_ref");--> statement-breakpoint +CREATE INDEX "workspace_runtime_services_company_execution_workspace_status_idx" ON "workspace_runtime_services" USING btree ("company_id","execution_workspace_id","status"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0028_snapshot.json b/packages/db/src/migrations/meta/0028_snapshot.json new file mode 100644 index 00000000..ef9b8494 --- /dev/null +++ b/packages/db/src/migrations/meta/0028_snapshot.json @@ -0,0 +1,7125 @@ +{ + "id": "d25020c0-808b-45bb-9e28-bfc354d6289b", + "prevId": "8186209d-f7ec-4048-bd4f-c96530f45304", + "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 + }, + "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.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'" + }, + "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 + }, + "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_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.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 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "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": {} + } + }, + "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" + } + }, + "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.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 + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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_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 + }, + "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_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": {} + } + }, + "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.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 + }, + "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.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 80a1dfbd..56e8c493 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1773150731736, "tag": "0027_tranquil_tenebrous", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1773439626334, + "tag": "0028_unusual_the_hunter", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/execution_workspaces.ts b/packages/db/src/schema/execution_workspaces.ts new file mode 100644 index 00000000..72e63d5b --- /dev/null +++ b/packages/db/src/schema/execution_workspaces.ts @@ -0,0 +1,68 @@ +import { + type AnyPgColumn, + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { issues } from "./issues.js"; +import { projectWorkspaces } from "./project_workspaces.js"; +import { projects } from "./projects.js"; + +export const executionWorkspaces = pgTable( + "execution_workspaces", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }), + projectWorkspaceId: uuid("project_workspace_id").references(() => projectWorkspaces.id, { onDelete: "set null" }), + sourceIssueId: uuid("source_issue_id").references((): AnyPgColumn => issues.id, { onDelete: "set null" }), + mode: text("mode").notNull(), + strategyType: text("strategy_type").notNull(), + name: text("name").notNull(), + status: text("status").notNull().default("active"), + cwd: text("cwd"), + repoUrl: text("repo_url"), + baseRef: text("base_ref"), + branchName: text("branch_name"), + providerType: text("provider_type").notNull().default("local_fs"), + providerRef: text("provider_ref"), + derivedFromExecutionWorkspaceId: uuid("derived_from_execution_workspace_id") + .references((): AnyPgColumn => executionWorkspaces.id, { onDelete: "set null" }), + lastUsedAt: timestamp("last_used_at", { withTimezone: true }).notNull().defaultNow(), + openedAt: timestamp("opened_at", { withTimezone: true }).notNull().defaultNow(), + closedAt: timestamp("closed_at", { withTimezone: true }), + cleanupEligibleAt: timestamp("cleanup_eligible_at", { withTimezone: true }), + cleanupReason: text("cleanup_reason"), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyProjectStatusIdx: index("execution_workspaces_company_project_status_idx").on( + table.companyId, + table.projectId, + table.status, + ), + companyProjectWorkspaceStatusIdx: index("execution_workspaces_company_project_workspace_status_idx").on( + table.companyId, + table.projectWorkspaceId, + table.status, + ), + companySourceIssueIdx: index("execution_workspaces_company_source_issue_idx").on( + table.companyId, + table.sourceIssueId, + ), + companyLastUsedIdx: index("execution_workspaces_company_last_used_idx").on( + table.companyId, + table.lastUsedAt, + ), + companyBranchIdx: index("execution_workspaces_company_branch_idx").on( + table.companyId, + table.branchName, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 3416ea9a..7a7a110d 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -13,10 +13,12 @@ export { agentTaskSessions } from "./agent_task_sessions.js"; export { agentWakeupRequests } from "./agent_wakeup_requests.js"; export { projects } from "./projects.js"; export { projectWorkspaces } from "./project_workspaces.js"; +export { executionWorkspaces } from "./execution_workspaces.js"; export { workspaceRuntimeServices } from "./workspace_runtime_services.js"; export { projectGoals } from "./project_goals.js"; export { goals } from "./goals.js"; export { issues } from "./issues.js"; +export { issueWorkProducts } from "./issue_work_products.js"; export { labels } from "./labels.js"; export { issueLabels } from "./issue_labels.js"; export { issueApprovals } from "./issue_approvals.js"; diff --git a/packages/db/src/schema/issue_work_products.ts b/packages/db/src/schema/issue_work_products.ts new file mode 100644 index 00000000..788317d2 --- /dev/null +++ b/packages/db/src/schema/issue_work_products.ts @@ -0,0 +1,64 @@ +import { + boolean, + index, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { executionWorkspaces } from "./execution_workspaces.js"; +import { heartbeatRuns } from "./heartbeat_runs.js"; +import { issues } from "./issues.js"; +import { projects } from "./projects.js"; +import { workspaceRuntimeServices } from "./workspace_runtime_services.js"; + +export const issueWorkProducts = pgTable( + "issue_work_products", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }), + issueId: uuid("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), + executionWorkspaceId: uuid("execution_workspace_id") + .references(() => executionWorkspaces.id, { onDelete: "set null" }), + runtimeServiceId: uuid("runtime_service_id") + .references(() => workspaceRuntimeServices.id, { onDelete: "set null" }), + type: text("type").notNull(), + provider: text("provider").notNull(), + externalId: text("external_id"), + title: text("title").notNull(), + url: text("url"), + status: text("status").notNull(), + reviewState: text("review_state").notNull().default("none"), + isPrimary: boolean("is_primary").notNull().default(false), + healthStatus: text("health_status").notNull().default("unknown"), + summary: text("summary"), + metadata: jsonb("metadata").$type>(), + createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyIssueTypeIdx: index("issue_work_products_company_issue_type_idx").on( + table.companyId, + table.issueId, + table.type, + ), + companyExecutionWorkspaceTypeIdx: index("issue_work_products_company_execution_workspace_type_idx").on( + table.companyId, + table.executionWorkspaceId, + table.type, + ), + companyProviderExternalIdIdx: index("issue_work_products_company_provider_external_id_idx").on( + table.companyId, + table.provider, + table.externalId, + ), + companyUpdatedIdx: index("issue_work_products_company_updated_idx").on( + table.companyId, + table.updatedAt, + ), + }), +); diff --git a/packages/db/src/schema/issues.ts b/packages/db/src/schema/issues.ts index 80093e67..cd63cfe8 100644 --- a/packages/db/src/schema/issues.ts +++ b/packages/db/src/schema/issues.ts @@ -14,6 +14,8 @@ import { projects } from "./projects.js"; import { goals } from "./goals.js"; import { companies } from "./companies.js"; import { heartbeatRuns } from "./heartbeat_runs.js"; +import { projectWorkspaces } from "./project_workspaces.js"; +import { executionWorkspaces } from "./execution_workspaces.js"; export const issues = pgTable( "issues", @@ -21,6 +23,7 @@ export const issues = pgTable( id: uuid("id").primaryKey().defaultRandom(), companyId: uuid("company_id").notNull().references(() => companies.id), projectId: uuid("project_id").references(() => projects.id), + projectWorkspaceId: uuid("project_workspace_id").references(() => projectWorkspaces.id, { onDelete: "set null" }), goalId: uuid("goal_id").references(() => goals.id), parentId: uuid("parent_id").references((): AnyPgColumn => issues.id), title: text("title").notNull(), @@ -40,6 +43,9 @@ export const issues = pgTable( requestDepth: integer("request_depth").notNull().default(0), billingCode: text("billing_code"), assigneeAdapterOverrides: jsonb("assignee_adapter_overrides").$type>(), + executionWorkspaceId: uuid("execution_workspace_id") + .references((): AnyPgColumn => executionWorkspaces.id, { onDelete: "set null" }), + executionWorkspacePreference: text("execution_workspace_preference"), executionWorkspaceSettings: jsonb("execution_workspace_settings").$type>(), startedAt: timestamp("started_at", { withTimezone: true }), completedAt: timestamp("completed_at", { withTimezone: true }), @@ -62,6 +68,8 @@ export const issues = pgTable( ), parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId), projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId), + projectWorkspaceIdx: index("issues_company_project_workspace_idx").on(table.companyId, table.projectWorkspaceId), + executionWorkspaceIdx: index("issues_company_execution_workspace_idx").on(table.companyId, table.executionWorkspaceId), identifierIdx: uniqueIndex("issues_identifier_idx").on(table.identifier), }), ); diff --git a/packages/db/src/schema/project_workspaces.ts b/packages/db/src/schema/project_workspaces.ts index 8ff52739..7f247ffe 100644 --- a/packages/db/src/schema/project_workspaces.ts +++ b/packages/db/src/schema/project_workspaces.ts @@ -5,6 +5,7 @@ import { pgTable, text, timestamp, + uniqueIndex, uuid, } from "drizzle-orm/pg-core"; import { companies } from "./companies.js"; @@ -17,9 +18,17 @@ export const projectWorkspaces = pgTable( companyId: uuid("company_id").notNull().references(() => companies.id), projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }), name: text("name").notNull(), + sourceType: text("source_type").notNull().default("local_path"), cwd: text("cwd"), repoUrl: text("repo_url"), repoRef: text("repo_ref"), + defaultRef: text("default_ref"), + visibility: text("visibility").notNull().default("default"), + setupCommand: text("setup_command"), + cleanupCommand: text("cleanup_command"), + remoteProvider: text("remote_provider"), + remoteWorkspaceRef: text("remote_workspace_ref"), + sharedWorkspaceKey: text("shared_workspace_key"), metadata: jsonb("metadata").$type>(), isPrimary: boolean("is_primary").notNull().default(false), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), @@ -28,5 +37,9 @@ export const projectWorkspaces = pgTable( (table) => ({ companyProjectIdx: index("project_workspaces_company_project_idx").on(table.companyId, table.projectId), projectPrimaryIdx: index("project_workspaces_project_primary_idx").on(table.projectId, table.isPrimary), + projectSourceTypeIdx: index("project_workspaces_project_source_type_idx").on(table.projectId, table.sourceType), + companySharedKeyIdx: index("project_workspaces_company_shared_key_idx").on(table.companyId, table.sharedWorkspaceKey), + projectRemoteRefIdx: uniqueIndex("project_workspaces_project_remote_ref_idx") + .on(table.projectId, table.remoteProvider, table.remoteWorkspaceRef), }), ); diff --git a/packages/db/src/schema/workspace_runtime_services.ts b/packages/db/src/schema/workspace_runtime_services.ts index 0837855f..150c332d 100644 --- a/packages/db/src/schema/workspace_runtime_services.ts +++ b/packages/db/src/schema/workspace_runtime_services.ts @@ -10,6 +10,7 @@ import { import { companies } from "./companies.js"; import { projects } from "./projects.js"; import { projectWorkspaces } from "./project_workspaces.js"; +import { executionWorkspaces } from "./execution_workspaces.js"; import { issues } from "./issues.js"; import { agents } from "./agents.js"; import { heartbeatRuns } from "./heartbeat_runs.js"; @@ -21,6 +22,7 @@ export const workspaceRuntimeServices = pgTable( companyId: uuid("company_id").notNull().references(() => companies.id), projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }), projectWorkspaceId: uuid("project_workspace_id").references(() => projectWorkspaces.id, { onDelete: "set null" }), + executionWorkspaceId: uuid("execution_workspace_id").references(() => executionWorkspaces.id, { onDelete: "set null" }), issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }), scopeType: text("scope_type").notNull(), scopeId: text("scope_id"), @@ -50,6 +52,11 @@ export const workspaceRuntimeServices = pgTable( table.projectWorkspaceId, table.status, ), + companyExecutionWorkspaceStatusIdx: index("workspace_runtime_services_company_execution_workspace_status_idx").on( + table.companyId, + table.executionWorkspaceId, + table.status, + ), companyProjectStatusIdx: index("workspace_runtime_services_company_project_status_idx").on( table.companyId, table.projectId, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 1a222f27..7ba01e40 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -77,12 +77,21 @@ export type { Project, ProjectGoalRef, ProjectWorkspace, + ExecutionWorkspace, WorkspaceRuntimeService, ExecutionWorkspaceStrategyType, ExecutionWorkspaceMode, + ExecutionWorkspaceProviderType, + ExecutionWorkspaceStatus, ExecutionWorkspaceStrategy, ProjectExecutionWorkspacePolicy, + ProjectExecutionWorkspaceDefaultMode, IssueExecutionWorkspaceSettings, + IssueWorkProduct, + IssueWorkProductType, + IssueWorkProductProvider, + IssueWorkProductStatus, + IssueWorkProductReviewState, Issue, IssueAssigneeAdapterOverrides, IssueComment, @@ -172,6 +181,13 @@ export { addIssueCommentSchema, linkIssueApprovalSchema, createIssueAttachmentMetadataSchema, + createIssueWorkProductSchema, + updateIssueWorkProductSchema, + issueWorkProductTypeSchema, + issueWorkProductStatusSchema, + issueWorkProductReviewStateSchema, + updateExecutionWorkspaceSchema, + executionWorkspaceStatusSchema, type CreateIssue, type CreateIssueLabel, type UpdateIssue, @@ -179,6 +195,9 @@ export { type AddIssueComment, type LinkIssueApproval, type CreateIssueAttachmentMetadata, + type CreateIssueWorkProduct, + type UpdateIssueWorkProduct, + type UpdateExecutionWorkspace, createGoalSchema, updateGoalSchema, type CreateGoal, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 07862c58..fa95125f 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -12,13 +12,24 @@ export type { export type { AssetImage } from "./asset.js"; export type { Project, ProjectGoalRef, ProjectWorkspace } from "./project.js"; export type { + ExecutionWorkspace, WorkspaceRuntimeService, ExecutionWorkspaceStrategyType, ExecutionWorkspaceMode, + ExecutionWorkspaceProviderType, + ExecutionWorkspaceStatus, ExecutionWorkspaceStrategy, ProjectExecutionWorkspacePolicy, + ProjectExecutionWorkspaceDefaultMode, IssueExecutionWorkspaceSettings, } from "./workspace-runtime.js"; +export type { + IssueWorkProduct, + IssueWorkProductType, + IssueWorkProductProvider, + IssueWorkProductStatus, + IssueWorkProductReviewState, +} from "./work-product.js"; export type { Issue, IssueAssigneeAdapterOverrides, diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index 550e8d24..a8bc63d5 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -1,7 +1,8 @@ import type { IssuePriority, IssueStatus } from "../constants.js"; import type { Goal } from "./goal.js"; import type { Project, ProjectWorkspace } from "./project.js"; -import type { IssueExecutionWorkspaceSettings } from "./workspace-runtime.js"; +import type { ExecutionWorkspace, IssueExecutionWorkspaceSettings } from "./workspace-runtime.js"; +import type { IssueWorkProduct } from "./work-product.js"; export interface IssueAncestorProject { id: string; @@ -54,6 +55,7 @@ export interface Issue { id: string; companyId: string; projectId: string | null; + projectWorkspaceId: string | null; goalId: string | null; parentId: string | null; ancestors?: IssueAncestor[]; @@ -74,6 +76,8 @@ export interface Issue { requestDepth: number; billingCode: string | null; assigneeAdapterOverrides: IssueAssigneeAdapterOverrides | null; + executionWorkspaceId: string | null; + executionWorkspacePreference: string | null; executionWorkspaceSettings: IssueExecutionWorkspaceSettings | null; startedAt: Date | null; completedAt: Date | null; @@ -83,6 +87,8 @@ export interface Issue { labels?: IssueLabel[]; project?: Project | null; goal?: Goal | null; + currentExecutionWorkspace?: ExecutionWorkspace | null; + workProducts?: IssueWorkProduct[]; mentionedProjects?: Project[]; myLastTouchAt?: Date | null; lastExternalCommentAt?: Date | null; diff --git a/packages/shared/src/types/project.ts b/packages/shared/src/types/project.ts index e9981ff7..60d0d25f 100644 --- a/packages/shared/src/types/project.ts +++ b/packages/shared/src/types/project.ts @@ -1,6 +1,9 @@ import type { ProjectStatus } from "../constants.js"; import type { ProjectExecutionWorkspacePolicy, WorkspaceRuntimeService } from "./workspace-runtime.js"; +export type ProjectWorkspaceSourceType = "local_path" | "git_repo" | "remote_managed" | "non_git_path"; +export type ProjectWorkspaceVisibility = "default" | "advanced"; + export interface ProjectGoalRef { id: string; title: string; @@ -11,9 +14,17 @@ export interface ProjectWorkspace { companyId: string; projectId: string; name: string; + sourceType: ProjectWorkspaceSourceType; cwd: string | null; repoUrl: string | null; repoRef: string | null; + defaultRef: string | null; + visibility: ProjectWorkspaceVisibility; + setupCommand: string | null; + cleanupCommand: string | null; + remoteProvider: string | null; + remoteWorkspaceRef: string | null; + sharedWorkspaceKey: string | null; metadata: Record | null; isPrimary: boolean; runtimeServices?: WorkspaceRuntimeService[]; diff --git a/packages/shared/src/types/work-product.ts b/packages/shared/src/types/work-product.ts new file mode 100644 index 00000000..297a2463 --- /dev/null +++ b/packages/shared/src/types/work-product.ts @@ -0,0 +1,55 @@ +export type IssueWorkProductType = + | "preview_url" + | "runtime_service" + | "pull_request" + | "branch" + | "commit" + | "artifact" + | "document"; + +export type IssueWorkProductProvider = + | "paperclip" + | "github" + | "vercel" + | "s3" + | "custom"; + +export type IssueWorkProductStatus = + | "active" + | "ready_for_review" + | "approved" + | "changes_requested" + | "merged" + | "closed" + | "failed" + | "archived" + | "draft"; + +export type IssueWorkProductReviewState = + | "none" + | "needs_board_review" + | "approved" + | "changes_requested"; + +export interface IssueWorkProduct { + id: string; + companyId: string; + projectId: string | null; + issueId: string; + executionWorkspaceId: string | null; + runtimeServiceId: string | null; + type: IssueWorkProductType; + provider: IssueWorkProductProvider | string; + externalId: string | null; + title: string; + url: string | null; + status: IssueWorkProductStatus | string; + reviewState: IssueWorkProductReviewState; + isPrimary: boolean; + healthStatus: "unknown" | "healthy" | "unhealthy"; + summary: string | null; + metadata: Record | null; + createdByRunId: string | null; + createdAt: Date; + updatedAt: Date; +} diff --git a/packages/shared/src/types/workspace-runtime.ts b/packages/shared/src/types/workspace-runtime.ts index f2aa023c..47ed9494 100644 --- a/packages/shared/src/types/workspace-runtime.ts +++ b/packages/shared/src/types/workspace-runtime.ts @@ -1,6 +1,35 @@ -export type ExecutionWorkspaceStrategyType = "project_primary" | "git_worktree"; +export type ExecutionWorkspaceStrategyType = + | "project_primary" + | "git_worktree" + | "adapter_managed" + | "cloud_sandbox"; -export type ExecutionWorkspaceMode = "inherit" | "project_primary" | "isolated" | "agent_default"; +export type ProjectExecutionWorkspaceDefaultMode = + | "shared_workspace" + | "isolated_workspace" + | "operator_branch" + | "adapter_default"; + +export type ExecutionWorkspaceMode = + | "inherit" + | "shared_workspace" + | "isolated_workspace" + | "operator_branch" + | "reuse_existing" + | "agent_default"; + +export type ExecutionWorkspaceProviderType = + | "local_fs" + | "git_worktree" + | "adapter_managed" + | "cloud_sandbox"; + +export type ExecutionWorkspaceStatus = + | "active" + | "idle" + | "in_review" + | "archived" + | "cleanup_failed"; export interface ExecutionWorkspaceStrategy { type: ExecutionWorkspaceStrategyType; @@ -13,12 +42,14 @@ export interface ExecutionWorkspaceStrategy { export interface ProjectExecutionWorkspacePolicy { enabled: boolean; - defaultMode?: "project_primary" | "isolated"; + defaultMode?: ProjectExecutionWorkspaceDefaultMode; allowIssueOverride?: boolean; + defaultProjectWorkspaceId?: string | null; workspaceStrategy?: ExecutionWorkspaceStrategy | null; workspaceRuntime?: Record | null; branchPolicy?: Record | null; pullRequestPolicy?: Record | null; + runtimePolicy?: Record | null; cleanupPolicy?: Record | null; } @@ -28,11 +59,39 @@ export interface IssueExecutionWorkspaceSettings { workspaceRuntime?: Record | null; } +export interface ExecutionWorkspace { + id: string; + companyId: string; + projectId: string; + projectWorkspaceId: string | null; + sourceIssueId: string | null; + mode: Exclude | "adapter_managed" | "cloud_sandbox"; + strategyType: ExecutionWorkspaceStrategyType; + name: string; + status: ExecutionWorkspaceStatus; + cwd: string | null; + repoUrl: string | null; + baseRef: string | null; + branchName: string | null; + providerType: ExecutionWorkspaceProviderType; + providerRef: string | null; + derivedFromExecutionWorkspaceId: string | null; + lastUsedAt: Date; + openedAt: Date; + closedAt: Date | null; + cleanupEligibleAt: Date | null; + cleanupReason: string | null; + metadata: Record | null; + createdAt: Date; + updatedAt: Date; +} + export interface WorkspaceRuntimeService { id: string; companyId: string; projectId: string | null; projectWorkspaceId: string | null; + executionWorkspaceId: string | null; issueId: string | null; scopeType: "project_workspace" | "execution_workspace" | "run" | "agent"; scopeId: string | null; diff --git a/packages/shared/src/validators/execution-workspace.ts b/packages/shared/src/validators/execution-workspace.ts new file mode 100644 index 00000000..53a74036 --- /dev/null +++ b/packages/shared/src/validators/execution-workspace.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const executionWorkspaceStatusSchema = z.enum([ + "active", + "idle", + "in_review", + "archived", + "cleanup_failed", +]); + +export const updateExecutionWorkspaceSchema = z.object({ + status: executionWorkspaceStatusSchema.optional(), + cleanupEligibleAt: z.string().datetime().optional().nullable(), + cleanupReason: z.string().optional().nullable(), + metadata: z.record(z.unknown()).optional().nullable(), +}).strict(); + +export type UpdateExecutionWorkspace = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index ad74a1e8..44779c99 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -76,6 +76,22 @@ export { type CreateIssueAttachmentMetadata, } from "./issue.js"; +export { + createIssueWorkProductSchema, + updateIssueWorkProductSchema, + issueWorkProductTypeSchema, + issueWorkProductStatusSchema, + issueWorkProductReviewStateSchema, + type CreateIssueWorkProduct, + type UpdateIssueWorkProduct, +} from "./work-product.js"; + +export { + updateExecutionWorkspaceSchema, + executionWorkspaceStatusSchema, + type UpdateExecutionWorkspace, +} from "./execution-workspace.js"; + export { createGoalSchema, updateGoalSchema, diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts index 3e269e04..e2c60c9d 100644 --- a/packages/shared/src/validators/issue.ts +++ b/packages/shared/src/validators/issue.ts @@ -3,7 +3,7 @@ import { ISSUE_PRIORITIES, ISSUE_STATUSES } from "../constants.js"; const executionWorkspaceStrategySchema = z .object({ - type: z.enum(["project_primary", "git_worktree"]).optional(), + type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(), baseRef: z.string().optional().nullable(), branchTemplate: z.string().optional().nullable(), worktreeParentDir: z.string().optional().nullable(), @@ -14,7 +14,7 @@ const executionWorkspaceStrategySchema = z export const issueExecutionWorkspaceSettingsSchema = z .object({ - mode: z.enum(["inherit", "project_primary", "isolated", "agent_default"]).optional(), + mode: z.enum(["inherit", "shared_workspace", "isolated_workspace", "operator_branch", "reuse_existing", "agent_default"]).optional(), workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(), workspaceRuntime: z.record(z.unknown()).optional().nullable(), }) @@ -29,6 +29,7 @@ export const issueAssigneeAdapterOverridesSchema = z export const createIssueSchema = z.object({ projectId: z.string().uuid().optional().nullable(), + projectWorkspaceId: z.string().uuid().optional().nullable(), goalId: z.string().uuid().optional().nullable(), parentId: z.string().uuid().optional().nullable(), title: z.string().min(1), @@ -40,6 +41,15 @@ export const createIssueSchema = z.object({ requestDepth: z.number().int().nonnegative().optional().default(0), billingCode: z.string().optional().nullable(), assigneeAdapterOverrides: issueAssigneeAdapterOverridesSchema.optional().nullable(), + executionWorkspaceId: z.string().uuid().optional().nullable(), + executionWorkspacePreference: z.enum([ + "inherit", + "shared_workspace", + "isolated_workspace", + "operator_branch", + "reuse_existing", + "agent_default", + ]).optional().nullable(), executionWorkspaceSettings: issueExecutionWorkspaceSettingsSchema.optional().nullable(), labelIds: z.array(z.string().uuid()).optional(), }); diff --git a/packages/shared/src/validators/project.ts b/packages/shared/src/validators/project.ts index da375495..cf5aba8a 100644 --- a/packages/shared/src/validators/project.ts +++ b/packages/shared/src/validators/project.ts @@ -3,7 +3,7 @@ import { PROJECT_STATUSES } from "../constants.js"; const executionWorkspaceStrategySchema = z .object({ - type: z.enum(["project_primary", "git_worktree"]).optional(), + type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(), baseRef: z.string().optional().nullable(), branchTemplate: z.string().optional().nullable(), worktreeParentDir: z.string().optional().nullable(), @@ -15,30 +15,54 @@ const executionWorkspaceStrategySchema = z export const projectExecutionWorkspacePolicySchema = z .object({ enabled: z.boolean(), - defaultMode: z.enum(["project_primary", "isolated"]).optional(), + defaultMode: z.enum(["shared_workspace", "isolated_workspace", "operator_branch", "adapter_default"]).optional(), allowIssueOverride: z.boolean().optional(), + defaultProjectWorkspaceId: z.string().uuid().optional().nullable(), workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(), workspaceRuntime: z.record(z.unknown()).optional().nullable(), branchPolicy: z.record(z.unknown()).optional().nullable(), pullRequestPolicy: z.record(z.unknown()).optional().nullable(), + runtimePolicy: z.record(z.unknown()).optional().nullable(), cleanupPolicy: z.record(z.unknown()).optional().nullable(), }) .strict(); +const projectWorkspaceSourceTypeSchema = z.enum(["local_path", "git_repo", "remote_managed", "non_git_path"]); +const projectWorkspaceVisibilitySchema = z.enum(["default", "advanced"]); + const projectWorkspaceFields = { name: z.string().min(1).optional(), + sourceType: projectWorkspaceSourceTypeSchema.optional(), cwd: z.string().min(1).optional().nullable(), repoUrl: z.string().url().optional().nullable(), repoRef: z.string().optional().nullable(), + defaultRef: z.string().optional().nullable(), + visibility: projectWorkspaceVisibilitySchema.optional(), + setupCommand: z.string().optional().nullable(), + cleanupCommand: z.string().optional().nullable(), + remoteProvider: z.string().optional().nullable(), + remoteWorkspaceRef: z.string().optional().nullable(), + sharedWorkspaceKey: z.string().optional().nullable(), metadata: z.record(z.unknown()).optional().nullable(), }; -export const createProjectWorkspaceSchema = z.object({ - ...projectWorkspaceFields, - isPrimary: z.boolean().optional().default(false), -}).superRefine((value, ctx) => { +function validateProjectWorkspace(value: Record, ctx: z.RefinementCtx) { + const sourceType = value.sourceType ?? "local_path"; const hasCwd = typeof value.cwd === "string" && value.cwd.trim().length > 0; const hasRepo = typeof value.repoUrl === "string" && value.repoUrl.trim().length > 0; + const hasRemoteRef = typeof value.remoteWorkspaceRef === "string" && value.remoteWorkspaceRef.trim().length > 0; + + if (sourceType === "remote_managed") { + if (!hasRemoteRef && !hasRepo) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Remote-managed workspace requires remoteWorkspaceRef or repoUrl.", + path: ["remoteWorkspaceRef"], + }); + } + return; + } + if (!hasCwd && !hasRepo) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -46,7 +70,12 @@ export const createProjectWorkspaceSchema = z.object({ path: ["cwd"], }); } -}); +} + +export const createProjectWorkspaceSchema = z.object({ + ...projectWorkspaceFields, + isPrimary: z.boolean().optional().default(false), +}).superRefine(validateProjectWorkspace); export type CreateProjectWorkspace = z.infer; diff --git a/packages/shared/src/validators/work-product.ts b/packages/shared/src/validators/work-product.ts new file mode 100644 index 00000000..839cc15a --- /dev/null +++ b/packages/shared/src/validators/work-product.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +export const issueWorkProductTypeSchema = z.enum([ + "preview_url", + "runtime_service", + "pull_request", + "branch", + "commit", + "artifact", + "document", +]); + +export const issueWorkProductStatusSchema = z.enum([ + "active", + "ready_for_review", + "approved", + "changes_requested", + "merged", + "closed", + "failed", + "archived", + "draft", +]); + +export const issueWorkProductReviewStateSchema = z.enum([ + "none", + "needs_board_review", + "approved", + "changes_requested", +]); + +export const createIssueWorkProductSchema = z.object({ + projectId: z.string().uuid().optional().nullable(), + executionWorkspaceId: z.string().uuid().optional().nullable(), + runtimeServiceId: z.string().uuid().optional().nullable(), + type: issueWorkProductTypeSchema, + provider: z.string().min(1), + externalId: z.string().optional().nullable(), + title: z.string().min(1), + url: z.string().url().optional().nullable(), + status: issueWorkProductStatusSchema.default("active"), + reviewState: issueWorkProductReviewStateSchema.optional().default("none"), + isPrimary: z.boolean().optional().default(false), + healthStatus: z.enum(["unknown", "healthy", "unhealthy"]).optional().default("unknown"), + summary: z.string().optional().nullable(), + metadata: z.record(z.unknown()).optional().nullable(), + createdByRunId: z.string().uuid().optional().nullable(), +}); + +export type CreateIssueWorkProduct = z.infer; + +export const updateIssueWorkProductSchema = createIssueWorkProductSchema.partial(); + +export type UpdateIssueWorkProduct = z.infer; diff --git a/server/src/__tests__/execution-workspace-policy.test.ts b/server/src/__tests__/execution-workspace-policy.test.ts index a4afe287..72f2837e 100644 --- a/server/src/__tests__/execution-workspace-policy.test.ts +++ b/server/src/__tests__/execution-workspace-policy.test.ts @@ -12,36 +12,36 @@ describe("execution workspace policy helpers", () => { expect( defaultIssueExecutionWorkspaceSettingsForProject({ enabled: true, - defaultMode: "isolated", + defaultMode: "isolated_workspace", }), - ).toEqual({ mode: "isolated" }); + ).toEqual({ mode: "isolated_workspace" }); expect( defaultIssueExecutionWorkspaceSettingsForProject({ enabled: true, - defaultMode: "project_primary", + defaultMode: "shared_workspace", }), - ).toEqual({ mode: "project_primary" }); + ).toEqual({ mode: "shared_workspace" }); expect(defaultIssueExecutionWorkspaceSettingsForProject(null)).toBeNull(); }); it("prefers explicit issue mode over project policy and legacy overrides", () => { expect( resolveExecutionWorkspaceMode({ - projectPolicy: { enabled: true, defaultMode: "project_primary" }, - issueSettings: { mode: "isolated" }, + projectPolicy: { enabled: true, defaultMode: "shared_workspace" }, + issueSettings: { mode: "isolated_workspace" }, legacyUseProjectWorkspace: false, }), - ).toBe("isolated"); + ).toBe("isolated_workspace"); }); it("falls back to project policy before legacy project-workspace compatibility flag", () => { expect( resolveExecutionWorkspaceMode({ - projectPolicy: { enabled: true, defaultMode: "isolated" }, + projectPolicy: { enabled: true, defaultMode: "isolated_workspace" }, issueSettings: null, legacyUseProjectWorkspace: false, }), - ).toBe("isolated"); + ).toBe("isolated_workspace"); expect( resolveExecutionWorkspaceMode({ projectPolicy: null, @@ -58,7 +58,7 @@ describe("execution workspace policy helpers", () => { }, projectPolicy: { enabled: true, - defaultMode: "isolated", + defaultMode: "isolated_workspace", workspaceStrategy: { type: "git_worktree", baseRef: "origin/main", @@ -69,7 +69,7 @@ describe("execution workspace policy helpers", () => { }, }, issueSettings: null, - mode: "isolated", + mode: "isolated_workspace", legacyUseProjectWorkspace: null, }); @@ -92,9 +92,9 @@ describe("execution workspace policy helpers", () => { expect( buildExecutionWorkspaceAdapterConfig({ agentConfig: baseConfig, - projectPolicy: { enabled: true, defaultMode: "isolated" }, - issueSettings: { mode: "project_primary" }, - mode: "project_primary", + projectPolicy: { enabled: true, defaultMode: "isolated_workspace" }, + issueSettings: { mode: "shared_workspace" }, + mode: "shared_workspace", legacyUseProjectWorkspace: null, }).workspaceStrategy, ).toBeUndefined(); @@ -124,7 +124,7 @@ describe("execution workspace policy helpers", () => { }), ).toEqual({ enabled: true, - defaultMode: "isolated", + defaultMode: "isolated_workspace", workspaceStrategy: { type: "git_worktree", worktreeParentDir: ".paperclip/worktrees", @@ -137,7 +137,7 @@ describe("execution workspace policy helpers", () => { mode: "project_primary", }), ).toEqual({ - mode: "project_primary", + mode: "shared_workspace", }); }); }); diff --git a/server/src/app.ts b/server/src/app.ts index 6871552a..7133a3a3 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -14,6 +14,7 @@ import { companyRoutes } from "./routes/companies.js"; import { agentRoutes } from "./routes/agents.js"; import { projectRoutes } from "./routes/projects.js"; import { issueRoutes } from "./routes/issues.js"; +import { executionWorkspaceRoutes } from "./routes/execution-workspaces.js"; import { goalRoutes } from "./routes/goals.js"; import { approvalRoutes } from "./routes/approvals.js"; import { secretRoutes } from "./routes/secrets.js"; @@ -107,6 +108,7 @@ export async function createApp( api.use(assetRoutes(db, opts.storageService)); api.use(projectRoutes(db)); api.use(issueRoutes(db, opts.storageService)); + api.use(executionWorkspaceRoutes(db)); api.use(goalRoutes(db)); api.use(approvalRoutes(db)); api.use(secretRoutes(db)); diff --git a/server/src/routes/execution-workspaces.ts b/server/src/routes/execution-workspaces.ts new file mode 100644 index 00000000..5c5b6bbe --- /dev/null +++ b/server/src/routes/execution-workspaces.ts @@ -0,0 +1,68 @@ +import { Router } from "express"; +import type { Db } from "@paperclipai/db"; +import { updateExecutionWorkspaceSchema } from "@paperclipai/shared"; +import { validate } from "../middleware/validate.js"; +import { executionWorkspaceService, logActivity } from "../services/index.js"; +import { assertCompanyAccess, getActorInfo } from "./authz.js"; + +export function executionWorkspaceRoutes(db: Db) { + const router = Router(); + const svc = executionWorkspaceService(db); + + router.get("/companies/:companyId/execution-workspaces", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const workspaces = await svc.list(companyId, { + projectId: req.query.projectId as string | undefined, + projectWorkspaceId: req.query.projectWorkspaceId as string | undefined, + issueId: req.query.issueId as string | undefined, + status: req.query.status as string | undefined, + reuseEligible: req.query.reuseEligible === "true", + }); + res.json(workspaces); + }); + + router.get("/execution-workspaces/:id", async (req, res) => { + const id = req.params.id as string; + const workspace = await svc.getById(id); + if (!workspace) { + res.status(404).json({ error: "Execution workspace not found" }); + return; + } + assertCompanyAccess(req, workspace.companyId); + res.json(workspace); + }); + + router.patch("/execution-workspaces/:id", validate(updateExecutionWorkspaceSchema), async (req, res) => { + const id = req.params.id as string; + const existing = await svc.getById(id); + if (!existing) { + res.status(404).json({ error: "Execution workspace not found" }); + return; + } + assertCompanyAccess(req, existing.companyId); + const workspace = await svc.update(id, { + ...req.body, + ...(req.body.cleanupEligibleAt ? { cleanupEligibleAt: new Date(req.body.cleanupEligibleAt) } : {}), + }); + if (!workspace) { + res.status(404).json({ error: "Execution workspace not found" }); + return; + } + const actor = getActorInfo(req); + await logActivity(db, { + companyId: existing.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "execution_workspace.updated", + entityType: "execution_workspace", + entityId: workspace.id, + details: { changedKeys: Object.keys(req.body).sort() }, + }); + res.json(workspace); + }); + + return router; +} diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index f02067a6..3aaa03fa 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -4,10 +4,12 @@ import type { Db } from "@paperclipai/db"; import { addIssueCommentSchema, createIssueAttachmentMetadataSchema, + createIssueWorkProductSchema, createIssueLabelSchema, checkoutIssueSchema, createIssueSchema, linkIssueApprovalSchema, + updateIssueWorkProductSchema, updateIssueSchema, } from "@paperclipai/shared"; import type { StorageService } from "../storage/types.js"; @@ -15,12 +17,14 @@ import { validate } from "../middleware/validate.js"; import { accessService, agentService, + executionWorkspaceService, goalService, heartbeatService, issueApprovalService, issueService, logActivity, projectService, + workProductService, } from "../services/index.js"; import { logger } from "../middleware/logger.js"; import { forbidden, HttpError, unauthorized } from "../errors.js"; @@ -37,6 +41,8 @@ export function issueRoutes(db: Db, storage: StorageService) { const projectsSvc = projectService(db); const goalsSvc = goalService(db); const issueApprovalsSvc = issueApprovalService(db); + const executionWorkspacesSvc = executionWorkspaceService(db); + const workProductsSvc = workProductService(db); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }, @@ -304,6 +310,10 @@ export function issueRoutes(db: Db, storage: StorageService) { const mentionedProjects = mentionedProjectIds.length > 0 ? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds) : []; + const currentExecutionWorkspace = issue.executionWorkspaceId + ? await executionWorkspacesSvc.getById(issue.executionWorkspaceId) + : null; + const workProducts = await workProductsSvc.listForIssue(issue.id); res.json({ ...issue, goalId: goal?.id ?? issue.goalId, @@ -311,9 +321,110 @@ export function issueRoutes(db: Db, storage: StorageService) { project: project ?? null, goal: goal ?? null, mentionedProjects, + currentExecutionWorkspace, + workProducts, }); }); + router.get("/issues/:id/work-products", async (req, res) => { + const id = req.params.id as string; + const issue = await svc.getById(id); + if (!issue) { + res.status(404).json({ error: "Issue not found" }); + return; + } + assertCompanyAccess(req, issue.companyId); + const workProducts = await workProductsSvc.listForIssue(issue.id); + res.json(workProducts); + }); + + router.post("/issues/:id/work-products", validate(createIssueWorkProductSchema), async (req, res) => { + const id = req.params.id as string; + const issue = await svc.getById(id); + if (!issue) { + res.status(404).json({ error: "Issue not found" }); + return; + } + assertCompanyAccess(req, issue.companyId); + const product = await workProductsSvc.createForIssue(issue.id, issue.companyId, { + ...req.body, + projectId: req.body.projectId ?? issue.projectId ?? null, + }); + if (!product) { + res.status(422).json({ error: "Invalid work product payload" }); + return; + } + const actor = getActorInfo(req); + await logActivity(db, { + companyId: issue.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.work_product_created", + entityType: "issue", + entityId: issue.id, + details: { workProductId: product.id, type: product.type, provider: product.provider }, + }); + res.status(201).json(product); + }); + + router.patch("/work-products/:id", validate(updateIssueWorkProductSchema), async (req, res) => { + const id = req.params.id as string; + const existing = await workProductsSvc.getById(id); + if (!existing) { + res.status(404).json({ error: "Work product not found" }); + return; + } + assertCompanyAccess(req, existing.companyId); + const product = await workProductsSvc.update(id, req.body); + if (!product) { + res.status(404).json({ error: "Work product not found" }); + return; + } + const actor = getActorInfo(req); + await logActivity(db, { + companyId: existing.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.work_product_updated", + entityType: "issue", + entityId: existing.issueId, + details: { workProductId: product.id, changedKeys: Object.keys(req.body).sort() }, + }); + res.json(product); + }); + + router.delete("/work-products/:id", async (req, res) => { + const id = req.params.id as string; + const existing = await workProductsSvc.getById(id); + if (!existing) { + res.status(404).json({ error: "Work product not found" }); + return; + } + assertCompanyAccess(req, existing.companyId); + const removed = await workProductsSvc.remove(id); + if (!removed) { + res.status(404).json({ error: "Work product not found" }); + return; + } + const actor = getActorInfo(req); + await logActivity(db, { + companyId: existing.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.work_product_deleted", + entityType: "issue", + entityId: existing.issueId, + details: { workProductId: removed.id, type: removed.type }, + }); + res.json(removed); + }); + router.post("/issues/:id/read", async (req, res) => { const id = req.params.id as string; const issue = await svc.getById(id); diff --git a/server/src/services/execution-workspace-policy.ts b/server/src/services/execution-workspace-policy.ts index f4552af3..584f44e9 100644 --- a/server/src/services/execution-workspace-policy.ts +++ b/server/src/services/execution-workspace-policy.ts @@ -2,11 +2,12 @@ import type { ExecutionWorkspaceMode, ExecutionWorkspaceStrategy, IssueExecutionWorkspaceSettings, + ProjectExecutionWorkspaceDefaultMode, ProjectExecutionWorkspacePolicy, } from "@paperclipai/shared"; import { asString, parseObject } from "../adapters/utils.js"; -type ParsedExecutionWorkspaceMode = Exclude; +type ParsedExecutionWorkspaceMode = Exclude; function cloneRecord(value: Record | null | undefined): Record | null { if (!value) return null; @@ -16,7 +17,7 @@ function cloneRecord(value: Record | null | undefined): Record< function parseExecutionWorkspaceStrategy(raw: unknown): ExecutionWorkspaceStrategy | null { const parsed = parseObject(raw); const type = asString(parsed.type, ""); - if (type !== "project_primary" && type !== "git_worktree") { + if (type !== "project_primary" && type !== "git_worktree" && type !== "adapter_managed" && type !== "cloud_sandbox") { return null; } return { @@ -34,12 +35,28 @@ export function parseProjectExecutionWorkspacePolicy(raw: unknown): ProjectExecu if (Object.keys(parsed).length === 0) return null; const enabled = typeof parsed.enabled === "boolean" ? parsed.enabled : false; const defaultMode = asString(parsed.defaultMode, ""); + const defaultProjectWorkspaceId = + typeof parsed.defaultProjectWorkspaceId === "string" ? parsed.defaultProjectWorkspaceId : undefined; const allowIssueOverride = typeof parsed.allowIssueOverride === "boolean" ? parsed.allowIssueOverride : undefined; + const normalizedDefaultMode = (() => { + if ( + defaultMode === "shared_workspace" || + defaultMode === "isolated_workspace" || + defaultMode === "operator_branch" || + defaultMode === "adapter_default" + ) { + return defaultMode as ProjectExecutionWorkspaceDefaultMode; + } + if (defaultMode === "project_primary") return "shared_workspace"; + if (defaultMode === "isolated") return "isolated_workspace"; + return undefined; + })(); return { enabled, - ...(defaultMode === "project_primary" || defaultMode === "isolated" ? { defaultMode } : {}), + ...(normalizedDefaultMode ? { defaultMode: normalizedDefaultMode } : {}), ...(allowIssueOverride !== undefined ? { allowIssueOverride } : {}), + ...(defaultProjectWorkspaceId ? { defaultProjectWorkspaceId } : {}), ...(parseExecutionWorkspaceStrategy(parsed.workspaceStrategy) ? { workspaceStrategy: parseExecutionWorkspaceStrategy(parsed.workspaceStrategy) } : {}), @@ -52,6 +69,9 @@ export function parseProjectExecutionWorkspacePolicy(raw: unknown): ProjectExecu ...(parsed.pullRequestPolicy && typeof parsed.pullRequestPolicy === "object" && !Array.isArray(parsed.pullRequestPolicy) ? { pullRequestPolicy: { ...(parsed.pullRequestPolicy as Record) } } : {}), + ...(parsed.runtimePolicy && typeof parsed.runtimePolicy === "object" && !Array.isArray(parsed.runtimePolicy) + ? { runtimePolicy: { ...(parsed.runtimePolicy as Record) } } + : {}), ...(parsed.cleanupPolicy && typeof parsed.cleanupPolicy === "object" && !Array.isArray(parsed.cleanupPolicy) ? { cleanupPolicy: { ...(parsed.cleanupPolicy as Record) } } : {}), @@ -62,9 +82,24 @@ export function parseIssueExecutionWorkspaceSettings(raw: unknown): IssueExecuti const parsed = parseObject(raw); if (Object.keys(parsed).length === 0) return null; const mode = asString(parsed.mode, ""); + const normalizedMode = (() => { + if ( + mode === "inherit" || + mode === "shared_workspace" || + mode === "isolated_workspace" || + mode === "operator_branch" || + mode === "reuse_existing" || + mode === "agent_default" + ) { + return mode; + } + if (mode === "project_primary") return "shared_workspace"; + if (mode === "isolated") return "isolated_workspace"; + return ""; + })(); return { - ...(mode === "inherit" || mode === "project_primary" || mode === "isolated" || mode === "agent_default" - ? { mode } + ...(normalizedMode + ? { mode: normalizedMode as IssueExecutionWorkspaceSettings["mode"] } : {}), ...(parseExecutionWorkspaceStrategy(parsed.workspaceStrategy) ? { workspaceStrategy: parseExecutionWorkspaceStrategy(parsed.workspaceStrategy) } @@ -80,7 +115,14 @@ export function defaultIssueExecutionWorkspaceSettingsForProject( ): IssueExecutionWorkspaceSettings | null { if (!projectPolicy?.enabled) return null; return { - mode: projectPolicy.defaultMode === "isolated" ? "isolated" : "project_primary", + mode: + projectPolicy.defaultMode === "isolated_workspace" + ? "isolated_workspace" + : projectPolicy.defaultMode === "operator_branch" + ? "operator_branch" + : projectPolicy.defaultMode === "adapter_default" + ? "agent_default" + : "shared_workspace", }; } @@ -90,16 +132,19 @@ export function resolveExecutionWorkspaceMode(input: { legacyUseProjectWorkspace: boolean | null; }): ParsedExecutionWorkspaceMode { const issueMode = input.issueSettings?.mode; - if (issueMode && issueMode !== "inherit") { + if (issueMode && issueMode !== "inherit" && issueMode !== "reuse_existing") { return issueMode; } if (input.projectPolicy?.enabled) { - return input.projectPolicy.defaultMode === "isolated" ? "isolated" : "project_primary"; + if (input.projectPolicy.defaultMode === "isolated_workspace") return "isolated_workspace"; + if (input.projectPolicy.defaultMode === "operator_branch") return "operator_branch"; + if (input.projectPolicy.defaultMode === "adapter_default") return "agent_default"; + return "shared_workspace"; } if (input.legacyUseProjectWorkspace === false) { return "agent_default"; } - return "project_primary"; + return "shared_workspace"; } export function buildExecutionWorkspaceAdapterConfig(input: { @@ -119,7 +164,7 @@ export function buildExecutionWorkspaceAdapterConfig(input: { const hasWorkspaceControl = projectHasPolicy || issueHasWorkspaceOverrides || input.legacyUseProjectWorkspace === false; if (hasWorkspaceControl) { - if (input.mode === "isolated") { + if (input.mode === "isolated_workspace") { const strategy = input.issueSettings?.workspaceStrategy ?? input.projectPolicy?.workspaceStrategy ?? diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts new file mode 100644 index 00000000..ea4dd163 --- /dev/null +++ b/server/src/services/execution-workspaces.ts @@ -0,0 +1,99 @@ +import { and, desc, eq, inArray } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { executionWorkspaces } from "@paperclipai/db"; +import type { ExecutionWorkspace } from "@paperclipai/shared"; + +type ExecutionWorkspaceRow = typeof executionWorkspaces.$inferSelect; + +function toExecutionWorkspace(row: ExecutionWorkspaceRow): ExecutionWorkspace { + return { + id: row.id, + companyId: row.companyId, + projectId: row.projectId, + projectWorkspaceId: row.projectWorkspaceId ?? null, + sourceIssueId: row.sourceIssueId ?? null, + mode: row.mode as ExecutionWorkspace["mode"], + strategyType: row.strategyType as ExecutionWorkspace["strategyType"], + name: row.name, + status: row.status as ExecutionWorkspace["status"], + cwd: row.cwd ?? null, + repoUrl: row.repoUrl ?? null, + baseRef: row.baseRef ?? null, + branchName: row.branchName ?? null, + providerType: row.providerType as ExecutionWorkspace["providerType"], + providerRef: row.providerRef ?? null, + derivedFromExecutionWorkspaceId: row.derivedFromExecutionWorkspaceId ?? null, + lastUsedAt: row.lastUsedAt, + openedAt: row.openedAt, + closedAt: row.closedAt ?? null, + cleanupEligibleAt: row.cleanupEligibleAt ?? null, + cleanupReason: row.cleanupReason ?? null, + metadata: (row.metadata as Record | null) ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export function executionWorkspaceService(db: Db) { + return { + list: async (companyId: string, filters?: { + projectId?: string; + projectWorkspaceId?: string; + issueId?: string; + status?: string; + reuseEligible?: boolean; + }) => { + const conditions = [eq(executionWorkspaces.companyId, companyId)]; + if (filters?.projectId) conditions.push(eq(executionWorkspaces.projectId, filters.projectId)); + if (filters?.projectWorkspaceId) { + conditions.push(eq(executionWorkspaces.projectWorkspaceId, filters.projectWorkspaceId)); + } + if (filters?.issueId) conditions.push(eq(executionWorkspaces.sourceIssueId, filters.issueId)); + if (filters?.status) { + const statuses = filters.status.split(",").map((value) => value.trim()).filter(Boolean); + if (statuses.length === 1) conditions.push(eq(executionWorkspaces.status, statuses[0]!)); + else if (statuses.length > 1) conditions.push(inArray(executionWorkspaces.status, statuses)); + } + if (filters?.reuseEligible) { + conditions.push(inArray(executionWorkspaces.status, ["active", "idle", "in_review"])); + } + + const rows = await db + .select() + .from(executionWorkspaces) + .where(and(...conditions)) + .orderBy(desc(executionWorkspaces.lastUsedAt), desc(executionWorkspaces.createdAt)); + return rows.map(toExecutionWorkspace); + }, + + getById: async (id: string) => { + const row = await db + .select() + .from(executionWorkspaces) + .where(eq(executionWorkspaces.id, id)) + .then((rows) => rows[0] ?? null); + return row ? toExecutionWorkspace(row) : null; + }, + + create: async (data: typeof executionWorkspaces.$inferInsert) => { + const row = await db + .insert(executionWorkspaces) + .values(data) + .returning() + .then((rows) => rows[0] ?? null); + return row ? toExecutionWorkspace(row) : null; + }, + + update: async (id: string, patch: Partial) => { + const row = await db + .update(executionWorkspaces) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(executionWorkspaces.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + return row ? toExecutionWorkspace(row) : null; + }, + }; +} + +export { toExecutionWorkspace }; diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index f0665c9a..7ca4949d 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -33,6 +33,7 @@ import { releaseRuntimeServicesForRun, } from "./workspace-runtime.js"; import { issueService } from "./issues.js"; +import { executionWorkspaceService } from "./execution-workspaces.js"; import { buildExecutionWorkspaceAdapterConfig, parseIssueExecutionWorkspaceSettings, @@ -455,6 +456,7 @@ export function heartbeatService(db: Db) { const runLogStore = getRunLogStore(); const secretsSvc = secretService(db); const issuesSvc = issueService(db); + const executionWorkspacesSvc = executionWorkspaceService(db); const activeRunExecutions = new Set(); async function getAgent(agentId: string) { @@ -1130,6 +1132,9 @@ export function heartbeatService(db: Db) { ? await db .select({ projectId: issues.projectId, + projectWorkspaceId: issues.projectWorkspaceId, + executionWorkspaceId: issues.executionWorkspaceId, + executionWorkspacePreference: issues.executionWorkspacePreference, assigneeAgentId: issues.assigneeAgentId, assigneeAdapterOverrides: issues.assigneeAdapterOverrides, executionWorkspaceSettings: issues.executionWorkspaceSettings, @@ -1197,6 +1202,10 @@ export function heartbeatService(db: Db) { id: issues.id, identifier: issues.identifier, title: issues.title, + projectId: issues.projectId, + projectWorkspaceId: issues.projectWorkspaceId, + executionWorkspaceId: issues.executionWorkspaceId, + executionWorkspacePreference: issues.executionWorkspacePreference, }) .from(issues) .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId))) @@ -1219,6 +1228,67 @@ export function heartbeatService(db: Db) { companyId: agent.companyId, }, }); + const existingExecutionWorkspace = + issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null; + const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null; + const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null; + const shouldReuseExisting = + issueRef?.executionWorkspacePreference === "reuse_existing" && + existingExecutionWorkspace && + existingExecutionWorkspace.status !== "archived"; + const persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace + ? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, { + cwd: executionWorkspace.cwd, + repoUrl: executionWorkspace.repoUrl, + baseRef: executionWorkspace.repoRef, + branchName: executionWorkspace.branchName, + providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs", + providerRef: executionWorkspace.worktreePath, + status: "active", + lastUsedAt: new Date(), + metadata: { + ...(existingExecutionWorkspace.metadata ?? {}), + source: executionWorkspace.source, + createdByRuntime: executionWorkspace.created, + }, + }) + : resolvedProjectId + ? await executionWorkspacesSvc.create({ + companyId: agent.companyId, + projectId: resolvedProjectId, + projectWorkspaceId: resolvedProjectWorkspaceId, + sourceIssueId: issueRef?.id ?? null, + mode: + executionWorkspaceMode === "isolated_workspace" + ? "isolated_workspace" + : executionWorkspaceMode === "operator_branch" + ? "operator_branch" + : executionWorkspaceMode === "agent_default" + ? "adapter_managed" + : "shared_workspace", + strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary", + name: executionWorkspace.branchName ?? issueRef?.identifier ?? `workspace-${agent.id.slice(0, 8)}`, + status: "active", + cwd: executionWorkspace.cwd, + repoUrl: executionWorkspace.repoUrl, + baseRef: executionWorkspace.repoRef, + branchName: executionWorkspace.branchName, + providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs", + providerRef: executionWorkspace.worktreePath, + lastUsedAt: new Date(), + openedAt: new Date(), + metadata: { + source: executionWorkspace.source, + createdByRuntime: executionWorkspace.created, + }, + }) + : null; + if (issueId && persistedExecutionWorkspace && issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) { + await issuesSvc.update(issueId, { + executionWorkspaceId: persistedExecutionWorkspace.id, + ...(resolvedProjectWorkspaceId ? { projectWorkspaceId: resolvedProjectWorkspaceId } : {}), + }); + } const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({ agentId: agent.id, previousSessionParams, diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 99a950c5..f68290e7 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -14,6 +14,8 @@ export { dashboardService } from "./dashboard.js"; export { sidebarBadgeService } from "./sidebar-badges.js"; export { accessService } from "./access.js"; export { companyPortabilityService } from "./company-portability.js"; +export { executionWorkspaceService } from "./execution-workspaces.js"; +export { workProductService } from "./work-products.js"; export { logActivity, type LogActivityInput } from "./activity-log.js"; export { notifyHireApproved, type NotifyHireApprovedInput } from "./hire-hook.js"; export { publishLiveEvent, subscribeCompanyLiveEvents } from "./live-events.js"; diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 807a97eb..a76258c7 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -7,6 +7,7 @@ import { companyMemberships, goals, heartbeatRuns, + executionWorkspaces, issueAttachments, issueLabels, issueComments, @@ -353,6 +354,40 @@ export function issueService(db: Db) { } } + async function assertValidProjectWorkspace(companyId: string, projectId: string | null | undefined, projectWorkspaceId: string) { + const workspace = await db + .select({ + id: projectWorkspaces.id, + companyId: projectWorkspaces.companyId, + projectId: projectWorkspaces.projectId, + }) + .from(projectWorkspaces) + .where(eq(projectWorkspaces.id, projectWorkspaceId)) + .then((rows) => rows[0] ?? null); + if (!workspace) throw notFound("Project workspace not found"); + if (workspace.companyId !== companyId) throw unprocessable("Project workspace must belong to same company"); + if (projectId && workspace.projectId !== projectId) { + throw unprocessable("Project workspace must belong to the selected project"); + } + } + + async function assertValidExecutionWorkspace(companyId: string, projectId: string | null | undefined, executionWorkspaceId: string) { + const workspace = await db + .select({ + id: executionWorkspaces.id, + companyId: executionWorkspaces.companyId, + projectId: executionWorkspaces.projectId, + }) + .from(executionWorkspaces) + .where(eq(executionWorkspaces.id, executionWorkspaceId)) + .then((rows) => rows[0] ?? null); + if (!workspace) throw notFound("Execution workspace not found"); + if (workspace.companyId !== companyId) throw unprocessable("Execution workspace must belong to same company"); + if (projectId && workspace.projectId !== projectId) { + throw unprocessable("Execution workspace must belong to the selected project"); + } + } + async function assertValidLabelIds(companyId: string, labelIds: string[], dbOrTx: any = db) { if (labelIds.length === 0) return; const existing = await dbOrTx @@ -647,6 +682,12 @@ export function issueService(db: Db) { if (data.assigneeUserId) { await assertAssignableUser(companyId, data.assigneeUserId); } + if (data.projectWorkspaceId) { + await assertValidProjectWorkspace(companyId, data.projectId, data.projectWorkspaceId); + } + if (data.executionWorkspaceId) { + await assertValidExecutionWorkspace(companyId, data.projectId, data.executionWorkspaceId); + } if (data.status === "in_progress" && !data.assigneeAgentId && !data.assigneeUserId) { throw unprocessable("in_progress issues require an assignee"); } @@ -665,6 +706,26 @@ export function issueService(db: Db) { parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy), ) as Record | null; } + let projectWorkspaceId = issueData.projectWorkspaceId ?? null; + if (!projectWorkspaceId && issueData.projectId) { + const project = await tx + .select({ + executionWorkspacePolicy: projects.executionWorkspacePolicy, + }) + .from(projects) + .where(and(eq(projects.id, issueData.projectId), eq(projects.companyId, companyId))) + .then((rows) => rows[0] ?? null); + const projectPolicy = parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy); + projectWorkspaceId = projectPolicy?.defaultProjectWorkspaceId ?? null; + if (!projectWorkspaceId) { + projectWorkspaceId = await tx + .select({ id: projectWorkspaces.id }) + .from(projectWorkspaces) + .where(and(eq(projectWorkspaces.projectId, issueData.projectId), eq(projectWorkspaces.companyId, companyId))) + .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id)) + .then((rows) => rows[0]?.id ?? null); + } + } const [company] = await tx .update(companies) .set({ issueCounter: sql`${companies.issueCounter} + 1` }) @@ -681,6 +742,7 @@ export function issueService(db: Db) { goalId: issueData.goalId, defaultGoalId: defaultCompanyGoal?.id ?? null, }), + ...(projectWorkspaceId ? { projectWorkspaceId } : {}), ...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}), companyId, issueNumber, @@ -741,6 +803,17 @@ export function issueService(db: Db) { if (issueData.assigneeUserId) { await assertAssignableUser(existing.companyId, issueData.assigneeUserId); } + const nextProjectId = issueData.projectId !== undefined ? issueData.projectId : existing.projectId; + const nextProjectWorkspaceId = + issueData.projectWorkspaceId !== undefined ? issueData.projectWorkspaceId : existing.projectWorkspaceId; + const nextExecutionWorkspaceId = + issueData.executionWorkspaceId !== undefined ? issueData.executionWorkspaceId : existing.executionWorkspaceId; + if (nextProjectWorkspaceId) { + await assertValidProjectWorkspace(existing.companyId, nextProjectId, nextProjectWorkspaceId); + } + if (nextExecutionWorkspaceId) { + await assertValidExecutionWorkspace(existing.companyId, nextProjectId, nextExecutionWorkspaceId); + } applyStatusSideEffects(issueData.status, patch); if (issueData.status && issueData.status !== "done") { diff --git a/server/src/services/projects.ts b/server/src/services/projects.ts index bb8e180e..62be240b 100644 --- a/server/src/services/projects.ts +++ b/server/src/services/projects.ts @@ -20,9 +20,17 @@ type WorkspaceRuntimeServiceRow = typeof workspaceRuntimeServices.$inferSelect; const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__"; type CreateWorkspaceInput = { name?: string | null; + sourceType?: string | null; cwd?: string | null; repoUrl?: string | null; repoRef?: string | null; + defaultRef?: string | null; + visibility?: string | null; + setupCommand?: string | null; + cleanupCommand?: string | null; + remoteProvider?: string | null; + remoteWorkspaceRef?: string | null; + sharedWorkspaceKey?: string | null; metadata?: Record | null; isPrimary?: boolean; }; @@ -91,6 +99,7 @@ function toRuntimeService(row: WorkspaceRuntimeServiceRow): WorkspaceRuntimeServ companyId: row.companyId, projectId: row.projectId ?? null, projectWorkspaceId: row.projectWorkspaceId ?? null, + executionWorkspaceId: row.executionWorkspaceId ?? null, issueId: row.issueId ?? null, scopeType: row.scopeType as WorkspaceRuntimeService["scopeType"], scopeId: row.scopeId ?? null, @@ -125,9 +134,17 @@ function toWorkspace( companyId: row.companyId, projectId: row.projectId, name: row.name, + sourceType: row.sourceType as ProjectWorkspace["sourceType"], cwd: row.cwd, repoUrl: row.repoUrl ?? null, repoRef: row.repoRef ?? null, + defaultRef: row.defaultRef ?? row.repoRef ?? null, + visibility: row.visibility as ProjectWorkspace["visibility"], + setupCommand: row.setupCommand ?? null, + cleanupCommand: row.cleanupCommand ?? null, + remoteProvider: row.remoteProvider ?? null, + remoteWorkspaceRef: row.remoteWorkspaceRef ?? null, + sharedWorkspaceKey: row.sharedWorkspaceKey ?? null, metadata: (row.metadata as Record | null) ?? null, isPrimary: row.isPrimary, runtimeServices, @@ -491,7 +508,13 @@ export function projectService(db: Db) { const cwd = normalizeWorkspaceCwd(data.cwd); const repoUrl = readNonEmptyString(data.repoUrl); - if (!cwd && !repoUrl) return null; + const sourceType = readNonEmptyString(data.sourceType) ?? (repoUrl ? "git_repo" : cwd ? "local_path" : "remote_managed"); + const remoteWorkspaceRef = readNonEmptyString(data.remoteWorkspaceRef); + if (sourceType === "remote_managed") { + if (!remoteWorkspaceRef && !repoUrl) return null; + } else if (!cwd && !repoUrl) { + return null; + } const name = deriveWorkspaceName({ name: data.name, cwd, @@ -525,9 +548,17 @@ export function projectService(db: Db) { companyId: project.companyId, projectId, name, + sourceType, cwd: cwd ?? null, repoUrl: repoUrl ?? null, repoRef: readNonEmptyString(data.repoRef), + defaultRef: readNonEmptyString(data.defaultRef) ?? readNonEmptyString(data.repoRef), + visibility: readNonEmptyString(data.visibility) ?? "default", + setupCommand: readNonEmptyString(data.setupCommand), + cleanupCommand: readNonEmptyString(data.cleanupCommand), + remoteProvider: readNonEmptyString(data.remoteProvider), + remoteWorkspaceRef, + sharedWorkspaceKey: readNonEmptyString(data.sharedWorkspaceKey), metadata: (data.metadata as Record | null | undefined) ?? null, isPrimary: shouldBePrimary, }) @@ -564,7 +595,19 @@ export function projectService(db: Db) { data.repoUrl !== undefined ? readNonEmptyString(data.repoUrl) : readNonEmptyString(existing.repoUrl); - if (!nextCwd && !nextRepoUrl) return null; + const nextSourceType = + data.sourceType !== undefined + ? readNonEmptyString(data.sourceType) + : readNonEmptyString(existing.sourceType); + const nextRemoteWorkspaceRef = + data.remoteWorkspaceRef !== undefined + ? readNonEmptyString(data.remoteWorkspaceRef) + : readNonEmptyString(existing.remoteWorkspaceRef); + if (nextSourceType === "remote_managed") { + if (!nextRemoteWorkspaceRef && !nextRepoUrl) return null; + } else if (!nextCwd && !nextRepoUrl) { + return null; + } const patch: Partial = { updatedAt: new Date(), @@ -576,6 +619,16 @@ export function projectService(db: Db) { if (data.cwd !== undefined) patch.cwd = nextCwd ?? null; if (data.repoUrl !== undefined) patch.repoUrl = nextRepoUrl ?? null; if (data.repoRef !== undefined) patch.repoRef = readNonEmptyString(data.repoRef); + if (data.sourceType !== undefined && nextSourceType) patch.sourceType = nextSourceType; + if (data.defaultRef !== undefined) patch.defaultRef = readNonEmptyString(data.defaultRef); + if (data.visibility !== undefined && readNonEmptyString(data.visibility)) { + patch.visibility = readNonEmptyString(data.visibility)!; + } + if (data.setupCommand !== undefined) patch.setupCommand = readNonEmptyString(data.setupCommand); + if (data.cleanupCommand !== undefined) patch.cleanupCommand = readNonEmptyString(data.cleanupCommand); + if (data.remoteProvider !== undefined) patch.remoteProvider = readNonEmptyString(data.remoteProvider); + if (data.remoteWorkspaceRef !== undefined) patch.remoteWorkspaceRef = nextRemoteWorkspaceRef; + if (data.sharedWorkspaceKey !== undefined) patch.sharedWorkspaceKey = readNonEmptyString(data.sharedWorkspaceKey); if (data.metadata !== undefined) patch.metadata = data.metadata; const updated = await db.transaction(async (tx) => { diff --git a/server/src/services/work-products.ts b/server/src/services/work-products.ts new file mode 100644 index 00000000..e5e4cc63 --- /dev/null +++ b/server/src/services/work-products.ts @@ -0,0 +1,113 @@ +import { and, desc, eq } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { issueWorkProducts } from "@paperclipai/db"; +import type { IssueWorkProduct } from "@paperclipai/shared"; + +type IssueWorkProductRow = typeof issueWorkProducts.$inferSelect; + +function toIssueWorkProduct(row: IssueWorkProductRow): IssueWorkProduct { + return { + id: row.id, + companyId: row.companyId, + projectId: row.projectId ?? null, + issueId: row.issueId, + executionWorkspaceId: row.executionWorkspaceId ?? null, + runtimeServiceId: row.runtimeServiceId ?? null, + type: row.type as IssueWorkProduct["type"], + provider: row.provider, + externalId: row.externalId ?? null, + title: row.title, + url: row.url ?? null, + status: row.status, + reviewState: row.reviewState as IssueWorkProduct["reviewState"], + isPrimary: row.isPrimary, + healthStatus: row.healthStatus as IssueWorkProduct["healthStatus"], + summary: row.summary ?? null, + metadata: (row.metadata as Record | null) ?? null, + createdByRunId: row.createdByRunId ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export function workProductService(db: Db) { + return { + listForIssue: async (issueId: string) => { + const rows = await db + .select() + .from(issueWorkProducts) + .where(eq(issueWorkProducts.issueId, issueId)) + .orderBy(desc(issueWorkProducts.isPrimary), desc(issueWorkProducts.updatedAt)); + return rows.map(toIssueWorkProduct); + }, + + getById: async (id: string) => { + const row = await db + .select() + .from(issueWorkProducts) + .where(eq(issueWorkProducts.id, id)) + .then((rows) => rows[0] ?? null); + return row ? toIssueWorkProduct(row) : null; + }, + + createForIssue: async (issueId: string, companyId: string, data: Omit) => { + if (data.isPrimary) { + await db + .update(issueWorkProducts) + .set({ isPrimary: false, updatedAt: new Date() }) + .where(and(eq(issueWorkProducts.companyId, companyId), eq(issueWorkProducts.issueId, issueId), eq(issueWorkProducts.type, data.type))); + } + const row = await db + .insert(issueWorkProducts) + .values({ + ...data, + companyId, + issueId, + }) + .returning() + .then((rows) => rows[0] ?? null); + return row ? toIssueWorkProduct(row) : null; + }, + + update: async (id: string, patch: Partial) => { + const existing = await db + .select() + .from(issueWorkProducts) + .where(eq(issueWorkProducts.id, id)) + .then((rows) => rows[0] ?? null); + if (!existing) return null; + + if (patch.isPrimary === true) { + await db + .update(issueWorkProducts) + .set({ isPrimary: false, updatedAt: new Date() }) + .where( + and( + eq(issueWorkProducts.companyId, existing.companyId), + eq(issueWorkProducts.issueId, existing.issueId), + eq(issueWorkProducts.type, existing.type), + ), + ); + } + + const row = await db + .update(issueWorkProducts) + .set({ ...patch, updatedAt: new Date() }) + .where(eq(issueWorkProducts.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + return row ? toIssueWorkProduct(row) : null; + }, + + remove: async (id: string) => { + const row = await db + .delete(issueWorkProducts) + .where(eq(issueWorkProducts.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + return row ? toIssueWorkProduct(row) : null; + }, + }; +} + +export { toIssueWorkProduct }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1cfdd9df..da3c9833 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -14,6 +14,7 @@ import { Projects } from "./pages/Projects"; import { ProjectDetail } from "./pages/ProjectDetail"; import { Issues } from "./pages/Issues"; import { IssueDetail } from "./pages/IssueDetail"; +import { ExecutionWorkspaceDetail } from "./pages/ExecutionWorkspaceDetail"; import { Goals } from "./pages/Goals"; import { GoalDetail } from "./pages/GoalDetail"; import { Approvals } from "./pages/Approvals"; @@ -136,6 +137,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/api/execution-workspaces.ts b/ui/src/api/execution-workspaces.ts new file mode 100644 index 00000000..bf83999c --- /dev/null +++ b/ui/src/api/execution-workspaces.ts @@ -0,0 +1,26 @@ +import type { ExecutionWorkspace } from "@paperclipai/shared"; +import { api } from "./client"; + +export const executionWorkspacesApi = { + list: ( + companyId: string, + filters?: { + projectId?: string; + projectWorkspaceId?: string; + issueId?: string; + status?: string; + reuseEligible?: boolean; + }, + ) => { + const params = new URLSearchParams(); + if (filters?.projectId) params.set("projectId", filters.projectId); + if (filters?.projectWorkspaceId) params.set("projectWorkspaceId", filters.projectWorkspaceId); + if (filters?.issueId) params.set("issueId", filters.issueId); + if (filters?.status) params.set("status", filters.status); + if (filters?.reuseEligible) params.set("reuseEligible", "true"); + const qs = params.toString(); + return api.get(`/companies/${companyId}/execution-workspaces${qs ? `?${qs}` : ""}`); + }, + get: (id: string) => api.get(`/execution-workspaces/${id}`), + update: (id: string, data: Record) => api.patch(`/execution-workspaces/${id}`, data), +}; diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 941294e6..1d99d9fc 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -1,4 +1,4 @@ -import type { Approval, Issue, IssueAttachment, IssueComment, IssueLabel } from "@paperclipai/shared"; +import type { Approval, Issue, IssueAttachment, IssueComment, IssueLabel, IssueWorkProduct } from "@paperclipai/shared"; import { api } from "./client"; export const issuesApi = { @@ -73,4 +73,10 @@ export const issuesApi = { api.post(`/issues/${id}/approvals`, { approvalId }), unlinkApproval: (id: string, approvalId: string) => api.delete<{ ok: true }>(`/issues/${id}/approvals/${approvalId}`), + listWorkProducts: (id: string) => api.get(`/issues/${id}/work-products`), + createWorkProduct: (id: string, data: Record) => + api.post(`/issues/${id}/work-products`, data), + updateWorkProduct: (id: string, data: Record) => + api.patch(`/work-products/${id}`, data), + deleteWorkProduct: (id: string) => api.delete(`/work-products/${id}`), }; diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index ca8e1bd4..60cce40d 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -4,6 +4,7 @@ import type { Issue } from "@paperclipai/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; +import { executionWorkspacesApi } from "../api/execution-workspaces"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; @@ -15,13 +16,43 @@ import { PriorityIcon } from "./PriorityIcon"; import { Identity } from "./Identity"; import { formatDate, cn, projectUrl } from "../lib/utils"; import { timeAgo } from "../lib/timeAgo"; +import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings"; 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 { AgentIcon } from "./AgentIconPicker"; -// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. -const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; +const EXECUTION_WORKSPACE_OPTIONS = [ + { value: "shared_workspace", label: "Project default" }, + { value: "isolated_workspace", label: "New isolated workspace" }, + { value: "reuse_existing", label: "Reuse existing workspace" }, + { value: "operator_branch", label: "Operator branch" }, + { value: "agent_default", label: "Agent default" }, +] as const; + +function defaultProjectWorkspaceIdForProject(project: { + workspaces?: Array<{ id: string; isPrimary: boolean }>; + executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null; +} | null | undefined) { + if (!project) return null; + return project.executionWorkspacePolicy?.defaultProjectWorkspaceId + ?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id + ?? project.workspaces?.[0]?.id + ?? null; +} + +function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined) { + const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null; + if (defaultMode === "isolated_workspace" || defaultMode === "operator_branch") return defaultMode; + if (defaultMode === "adapter_default") return "agent_default"; + return "shared_workspace"; +} + +function issueModeForExistingWorkspace(mode: string | null | undefined) { + if (mode === "isolated_workspace" || mode === "operator_branch" || mode === "shared_workspace") return mode; + if (mode === "adapter_managed" || mode === "cloud_sandbox") return "agent_default"; + return "shared_workspace"; +} interface IssuePropertiesProps { issue: Issue; @@ -102,6 +133,7 @@ function PropertyPicker({ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProps) { const { selectedCompanyId } = useCompany(); + const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled(); const queryClient = useQueryClient(); const companyId = issue.companyId ?? selectedCompanyId; const [assigneeOpen, setAssigneeOpen] = useState(false); @@ -182,15 +214,32 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp const currentProject = issue.projectId ? orderedProjects.find((project) => project.id === issue.projectId) ?? null : null; - const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI + const currentProjectExecutionWorkspacePolicy = showExperimentalWorkspaceUi ? currentProject?.executionWorkspacePolicy ?? null : null; const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled); - const usesIsolatedExecutionWorkspace = issue.executionWorkspaceSettings?.mode === "isolated" - ? true - : issue.executionWorkspaceSettings?.mode === "project_primary" - ? false - : currentProjectExecutionWorkspacePolicy?.defaultMode === "isolated"; + const currentProjectWorkspaces = currentProject?.workspaces ?? []; + const currentExecutionWorkspaceSelection = + issue.executionWorkspacePreference + ?? issue.executionWorkspaceSettings?.mode + ?? defaultExecutionWorkspaceModeForProject(currentProject); + const { data: reusableExecutionWorkspaces } = useQuery({ + queryKey: queryKeys.executionWorkspaces.list(companyId!, { + projectId: issue.projectId ?? undefined, + projectWorkspaceId: issue.projectWorkspaceId ?? undefined, + reuseEligible: true, + }), + queryFn: () => + executionWorkspacesApi.list(companyId!, { + projectId: issue.projectId ?? undefined, + projectWorkspaceId: issue.projectWorkspaceId ?? undefined, + reuseEligible: true, + }), + enabled: Boolean(companyId) && showExperimentalWorkspaceUi && Boolean(issue.projectId), + }); + const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find( + (workspace) => workspace.id === issue.executionWorkspaceId, + ); const projectLink = (id: string | null) => { if (!id) return null; const project = projects?.find((p) => p.id === id) ?? null; @@ -418,7 +467,13 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp !issue.projectId && "bg-accent" )} onClick={() => { - onUpdate({ projectId: null, executionWorkspaceSettings: null }); + onUpdate({ + projectId: null, + projectWorkspaceId: null, + executionWorkspaceId: null, + executionWorkspacePreference: null, + executionWorkspaceSettings: null, + }); setProjectOpen(false); }} > @@ -438,10 +493,14 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp p.id === issue.projectId && "bg-accent" )} onClick={() => { + const defaultMode = defaultExecutionWorkspaceModeForProject(p); onUpdate({ projectId: p.id, - executionWorkspaceSettings: SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && p.executionWorkspacePolicy?.enabled - ? { mode: p.executionWorkspacePolicy.defaultMode === "isolated" ? "isolated" : "project_primary" } + projectWorkspaceId: showExperimentalWorkspaceUi ? defaultProjectWorkspaceIdForProject(p) : null, + executionWorkspaceId: null, + executionWorkspacePreference: showExperimentalWorkspaceUi ? defaultMode : null, + executionWorkspaceSettings: showExperimentalWorkspaceUi && p.executionWorkspacePolicy?.enabled + ? { mode: defaultMode } : null, }); setProjectOpen(false); @@ -530,38 +589,94 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp {projectContent} - {currentProjectSupportsExecutionWorkspace && ( + {showExperimentalWorkspaceUi && currentProjectWorkspaces.length > 0 && ( + + + + )} + + {showExperimentalWorkspaceUi && currentProjectSupportsExecutionWorkspace && ( -
-
-
- {usesIsolatedExecutionWorkspace ? "Isolated issue checkout" : "Project primary checkout"} -
-
- Toggle whether this issue runs in its own execution workspace. -
-
-
- {currentProjectSupportsExecutionWorkspace && ( -
-
-
-
Use isolated issue checkout
+ {showExperimentalWorkspaceUi && currentProject && ( +
+ {currentProjectWorkspaces.length > 0 && ( +
+
Codebase
- Create an issue-specific execution workspace instead of using the project's primary checkout. + Choose which project workspace this issue should use.
+
- -
+ {executionWorkspaceMode === "reuse_existing" && selectedReusableExecutionWorkspace && ( +
+ Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}. +
+ )} +
+ )}
)} diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 9237f5e3..6a809f33 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -17,6 +17,7 @@ import { AlertCircle, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } fr import { ChoosePathButton } from "./PathInstructionsModal"; import { DraftInput } from "./agent-config-primitives"; import { InlineEditor } from "./InlineEditor"; +import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings"; const PROJECT_STATUSES = [ { value: "backlog", label: "Backlog" }, @@ -26,9 +27,6 @@ const PROJECT_STATUSES = [ { value: "cancelled", label: "Cancelled" }, ]; -// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. -const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; - interface ProjectPropertiesProps { project: Project; onUpdate?: (data: Record) => void; @@ -154,6 +152,7 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: ( export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState }: ProjectPropertiesProps) { const { selectedCompanyId } = useCompany(); + const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled(); const queryClient = useQueryClient(); const [goalOpen, setGoalOpen] = useState(false); const [executionWorkspaceAdvancedOpen, setExecutionWorkspaceAdvancedOpen] = useState(false); @@ -195,7 +194,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa const executionWorkspacePolicy = project.executionWorkspacePolicy ?? null; const executionWorkspacesEnabled = executionWorkspacePolicy?.enabled === true; const executionWorkspaceDefaultMode = - executionWorkspacePolicy?.defaultMode === "isolated" ? "isolated" : "project_primary"; + executionWorkspacePolicy?.defaultMode === "isolated_workspace" ? "isolated_workspace" : "shared_workspace"; const executionWorkspaceStrategy = executionWorkspacePolicy?.workspaceStrategy ?? { type: "git_worktree", baseRef: "", @@ -710,7 +709,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )}
- {SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && ( + {showExperimentalWorkspaceUi && ( <> @@ -785,21 +784,21 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa diff --git a/ui/src/lib/experimentalSettings.ts b/ui/src/lib/experimentalSettings.ts new file mode 100644 index 00000000..a48d7c06 --- /dev/null +++ b/ui/src/lib/experimentalSettings.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; + +const WORKSPACES_KEY = "paperclip:experimental:workspaces"; + +export function loadExperimentalWorkspacesEnabled(): boolean { + if (typeof window === "undefined") return false; + return window.localStorage.getItem(WORKSPACES_KEY) === "true"; +} + +export function saveExperimentalWorkspacesEnabled(enabled: boolean) { + if (typeof window === "undefined") return; + window.localStorage.setItem(WORKSPACES_KEY, enabled ? "true" : "false"); + window.dispatchEvent(new CustomEvent("paperclip:experimental:workspaces", { detail: enabled })); +} + +export function useExperimentalWorkspacesEnabled() { + const [enabled, setEnabled] = useState(loadExperimentalWorkspacesEnabled); + + useEffect(() => { + const handleStorage = (event: StorageEvent) => { + if (event.key && event.key !== WORKSPACES_KEY) return; + setEnabled(loadExperimentalWorkspacesEnabled()); + }; + const handleCustom = () => setEnabled(loadExperimentalWorkspacesEnabled()); + window.addEventListener("storage", handleStorage); + window.addEventListener("paperclip:experimental:workspaces", handleCustom as EventListener); + return () => { + window.removeEventListener("storage", handleStorage); + window.removeEventListener("paperclip:experimental:workspaces", handleCustom as EventListener); + }; + }, []); + + const update = (next: boolean) => { + saveExperimentalWorkspacesEnabled(next); + setEnabled(next); + }; + + return { enabled, setEnabled: update }; +} diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index a8480828..9e14a76e 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -110,6 +110,7 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue { id, companyId: "company-1", projectId: null, + projectWorkspaceId: null, goalId: null, parentId: null, title: `Issue ${id}`, @@ -125,6 +126,8 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue { requestDepth: 0, billingCode: null, assigneeAdapterOverrides: null, + executionWorkspaceId: null, + executionWorkspacePreference: null, executionWorkspaceSettings: null, checkoutRunId: null, executionRunId: null, diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index c500afdc..a057fd7e 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -32,6 +32,12 @@ export const queryKeys = { approvals: (issueId: string) => ["issues", "approvals", issueId] as const, liveRuns: (issueId: string) => ["issues", "live-runs", issueId] as const, activeRun: (issueId: string) => ["issues", "active-run", issueId] as const, + workProducts: (issueId: string) => ["issues", "work-products", issueId] as const, + }, + executionWorkspaces: { + list: (companyId: string, filters?: Record) => + ["execution-workspaces", companyId, filters ?? {}] as const, + detail: (id: string) => ["execution-workspaces", "detail", id] as const, }, projects: { list: (companyId: string) => ["projects", companyId] as const, diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx new file mode 100644 index 00000000..03f2715d --- /dev/null +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -0,0 +1,70 @@ +import { Link, useParams } from "@/lib/router"; +import { useQuery } from "@tanstack/react-query"; +import { ExternalLink } from "lucide-react"; +import { executionWorkspacesApi } from "../api/execution-workspaces"; +import { queryKeys } from "../lib/queryKeys"; + +function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +export function ExecutionWorkspaceDetail() { + const { workspaceId } = useParams<{ workspaceId: string }>(); + + const { data: workspace, isLoading, error } = useQuery({ + queryKey: queryKeys.executionWorkspaces.detail(workspaceId!), + queryFn: () => executionWorkspacesApi.get(workspaceId!), + enabled: Boolean(workspaceId), + }); + + if (isLoading) return

Loading...

; + if (error) return

{error instanceof Error ? error.message : "Failed to load workspace"}

; + if (!workspace) return null; + + return ( +
+
+
Execution workspace
+

{workspace.name}

+
+ {workspace.status} · {workspace.mode} · {workspace.providerType} +
+
+ +
+ + {workspace.projectId ? {workspace.projectId} : "None"} + + + {workspace.sourceIssueId ? {workspace.sourceIssueId} : "None"} + + {workspace.branchName ?? "None"} + {workspace.baseRef ?? "None"} + + {workspace.cwd ?? "None"} + + + {workspace.providerRef ?? "None"} + + + {workspace.repoUrl ? ( + + {workspace.repoUrl} + + + ) : "None"} + + {new Date(workspace.openedAt).toLocaleString()} + {new Date(workspace.lastUsedAt).toLocaleString()} + + {workspace.cleanupEligibleAt ? `${new Date(workspace.cleanupEligibleAt).toLocaleString()}${workspace.cleanupReason ? ` · ${workspace.cleanupReason}` : ""}` : "Not scheduled"} + +
+
+ ); +} diff --git a/ui/src/pages/InstanceSettings.tsx b/ui/src/pages/InstanceSettings.tsx index a4781e1f..ab77a177 100644 --- a/ui/src/pages/InstanceSettings.tsx +++ b/ui/src/pages/InstanceSettings.tsx @@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { queryKeys } from "../lib/queryKeys"; import { formatDateTime, relativeTime } from "../lib/utils"; +import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings"; function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; @@ -30,6 +31,7 @@ export function InstanceSettings() { const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); + const { enabled: workspacesEnabled, setEnabled: setWorkspacesEnabled } = useExperimentalWorkspacesEnabled(); useEffect(() => { setBreadcrumbs([ @@ -110,6 +112,34 @@ export function InstanceSettings() { return (
+
+
+
+ +

Experimental

+
+

+ UI-only feature flags for in-progress product surfaces. +

+
+
+
+
Workspaces
+
+ Show workspace, execution workspace, and work product controls in project and issue UI. +
+
+ +
+
+
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index bb152e17..fb4d50fb 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -11,6 +11,7 @@ import { useCompany } from "../context/CompanyContext"; import { usePanel } from "../context/PanelContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; +import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings"; import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb"; import { useProjectOrder } from "../hooks/useProjectOrder"; import { relativeTime, cn, formatTokens } from "../lib/utils"; @@ -36,15 +37,21 @@ import { ChevronDown, ChevronRight, EyeOff, + ExternalLink, + FileText, + GitBranch, + GitPullRequest, Hexagon, ListTree, MessageSquare, MoreHorizontal, + Package, Paperclip, + Rocket, SlidersHorizontal, Trash2, } from "lucide-react"; -import type { ActivityEvent } from "@paperclipai/shared"; +import type { ActivityEvent, IssueWorkProduct } from "@paperclipai/shared"; import type { Agent, IssueAttachment } from "@paperclipai/shared"; type CommentReassignment = { @@ -133,6 +140,24 @@ function formatAction(action: string, details?: Record | null): return ACTION_LABELS[action] ?? action.replace(/[._]/g, " "); } +function workProductIcon(product: IssueWorkProduct) { + switch (product.type) { + case "pull_request": + return ; + case "branch": + case "commit": + return ; + case "artifact": + return ; + case "document": + return ; + case "runtime_service": + return ; + default: + return ; + } +} + function ActorIdentity({ evt, agentMap }: { evt: ActivityEvent; agentMap: Map }) { const id = evt.actorId; if (evt.actorType === "agent") { @@ -147,6 +172,7 @@ function ActorIdentity({ evt, agentMap }: { evt: ActivityEvent; agentMap: Map(); const { selectedCompanyId } = useCompany(); + const { enabled: experimentalWorkspacesEnabled } = useExperimentalWorkspacesEnabled(); const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); @@ -160,6 +186,13 @@ export function IssueDetail() { cost: false, }); const [attachmentError, setAttachmentError] = useState(null); + const [newWorkProductType, setNewWorkProductType] = useState("preview_url"); + const [newWorkProductProvider, setNewWorkProductProvider] = useState("paperclip"); + const [newWorkProductTitle, setNewWorkProductTitle] = useState(""); + const [newWorkProductUrl, setNewWorkProductUrl] = useState(""); + const [newWorkProductStatus, setNewWorkProductStatus] = useState("active"); + const [newWorkProductReviewState, setNewWorkProductReviewState] = useState("none"); + const [newWorkProductSummary, setNewWorkProductSummary] = useState(""); const fileInputRef = useRef(null); const lastMarkedReadIssueIdRef = useRef(null); @@ -387,6 +420,7 @@ export function IssueDetail() { queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId!) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.workProducts(issueId!) }); if (selectedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) }); @@ -471,6 +505,42 @@ export function IssueDetail() { }, }); + const createWorkProduct = useMutation({ + mutationFn: () => + issuesApi.createWorkProduct(issueId!, { + type: newWorkProductType, + provider: newWorkProductProvider, + title: newWorkProductTitle.trim(), + url: newWorkProductUrl.trim() || null, + status: newWorkProductStatus, + reviewState: newWorkProductReviewState, + summary: newWorkProductSummary.trim() || null, + projectId: issue?.projectId ?? null, + executionWorkspaceId: issue?.currentExecutionWorkspace?.id ?? issue?.executionWorkspaceId ?? null, + }), + onSuccess: () => { + setNewWorkProductTitle(""); + setNewWorkProductUrl(""); + setNewWorkProductSummary(""); + setNewWorkProductType("preview_url"); + setNewWorkProductProvider("paperclip"); + setNewWorkProductStatus("active"); + setNewWorkProductReviewState("none"); + invalidateIssue(); + }, + }); + + const updateWorkProduct = useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => + issuesApi.updateWorkProduct(id, data), + onSuccess: () => invalidateIssue(), + }); + + const deleteWorkProduct = useMutation({ + mutationFn: (id: string) => issuesApi.deleteWorkProduct(id), + onSuccess: () => invalidateIssue(), + }); + useEffect(() => { const titleLabel = issue?.title ?? issueId ?? "Issue"; setBreadcrumbs([ @@ -508,6 +578,11 @@ export function IssueDetail() { // Ancestors are returned oldest-first from the server (root at end, immediate parent at start) const ancestors = issue.ancestors ?? []; + const workProducts = issue.workProducts ?? []; + const showOutputsTab = + experimentalWorkspacesEnabled || + Boolean(issue.currentExecutionWorkspace) || + workProducts.length > 0; const handleFilePicked = async (evt: ChangeEvent) => { const file = evt.target.files?.[0]; @@ -759,6 +834,12 @@ export function IssueDetail() { Comments + {showOutputsTab && ( + + + Outputs + + )} Sub-issues @@ -798,6 +879,199 @@ export function IssueDetail() { /> + {showOutputsTab && ( + + {issue.currentExecutionWorkspace && ( +
+
+
+
Execution workspace
+
+ {issue.currentExecutionWorkspace.status} · {issue.currentExecutionWorkspace.mode} +
+
+ + Open + + +
+
+ {issue.currentExecutionWorkspace.branchName ?? issue.currentExecutionWorkspace.cwd ?? "No workspace path recorded."} +
+
+ )} + +
+
Work product
+
+ + setNewWorkProductProvider(e.target.value)} + placeholder="Provider" + /> + setNewWorkProductTitle(e.target.value)} + placeholder="Title" + /> + setNewWorkProductUrl(e.target.value)} + placeholder="URL" + /> + + +