From 9f9a8cfa2576ce819395705b17d450ecf903c5ad Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 07:09:27 -0500 Subject: [PATCH 01/74] skills: add prcheckloop CI remediation loop --- .agents/skills/prcheckloop/SKILL.md | 209 ++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 .agents/skills/prcheckloop/SKILL.md diff --git a/.agents/skills/prcheckloop/SKILL.md b/.agents/skills/prcheckloop/SKILL.md new file mode 100644 index 00000000..70d9e19a --- /dev/null +++ b/.agents/skills/prcheckloop/SKILL.md @@ -0,0 +1,209 @@ +--- +name: prcheckloop +description: > + Iteratively gets a GitHub pull request's checks green. Detects the PR for the + current branch or uses a provided PR number, waits for every check on the + latest head SHA to appear and finish, investigates failing checks, fixes + actionable code or test issues, pushes, and repeats. Escalates with a precise + blocker when failures are external, flaky, or not safely fixable. Use when a + PR still has unsuccessful checks after review fixes, including after greploop. +--- + +# PRCheckloop + +Get a GitHub PR to a fully green check state, or exit with a concrete blocker. + +## Scope + +- GitHub PRs only. If the repo is GitLab, stop and use `check-pr`. +- Focus on checks for the latest PR head SHA, not old commits. +- Focus on CI/status checks, not review comments or PR template cleanup. +- If the user also wants review-comment cleanup, pair this with `check-pr`. + +## Inputs + +- **PR number** (optional): If not provided, detect the PR for the current branch. +- **Max iterations**: default `5`. + +## Workflow + +### 1. Identify the PR + +If no PR number is provided, detect it from the current branch: + +```bash +gh pr view --json number,headRefName,headRefOid,url,isDraft +``` + +If needed, switch to the PR branch before making changes. + +Stop early if: + +- `gh` is not authenticated +- there is no PR for the branch +- the repo is not hosted on GitHub + +### 2. Track the latest head SHA + +Always work against the current PR head SHA: + +```bash +PR_JSON=$(gh pr view "$PR_NUMBER" --json number,headRefName,headRefOid,url) +HEAD_SHA=$(echo "$PR_JSON" | jq -r .headRefOid) +PR_URL=$(echo "$PR_JSON" | jq -r .url) +``` + +Ignore failing checks from older SHAs. After every push, refresh `HEAD_SHA` and +restart the inspection loop. + +### 3. Inventory checks for that SHA + +Fetch both GitHub check runs and legacy commit status contexts: + +```bash +gh api "repos/{owner}/{repo}/commits/$HEAD_SHA/check-runs?per_page=100" +gh api "repos/{owner}/{repo}/commits/$HEAD_SHA/status" +``` + +For a compact PR-level view, this GraphQL payload is useful: + +```bash +gh api graphql -f query=' +query($owner:String!, $repo:String!, $pr:Int!) { + repository(owner:$owner, name:$repo) { + pullRequest(number:$pr) { + headRefOid + url + statusCheckRollup { + contexts(first:100) { + nodes { + __typename + ... on CheckRun { name status conclusion detailsUrl workflowName } + ... on StatusContext { context state targetUrl description } + } + } + } + } + } +}' -F owner=OWNER -F repo=REPO -F pr="$PR_NUMBER" +``` + +### 4. Wait for checks to actually run + +After a new push, checks can take a moment to appear. Poll every 15-30 seconds +until one of these is true: + +- checks have appeared and every item is in a terminal state +- checks have appeared and at least one failed +- no checks appear after a reasonable wait, usually 2 minutes + +Treat these as terminal success states: + +- check runs: `SUCCESS`, `NEUTRAL`, `SKIPPED` +- status contexts: `SUCCESS` + +Treat these as pending: + +- check runs: `QUEUED`, `PENDING`, `WAITING`, `REQUESTED`, `IN_PROGRESS` +- status contexts: `PENDING` + +Treat these as failures: + +- check runs: `FAILURE`, `TIMED_OUT`, `CANCELLED`, `ACTION_REQUIRED`, `STARTUP_FAILURE`, `STALE` +- status contexts: `FAILURE`, `ERROR` + +If no checks appear for the latest SHA, inspect `.github/workflows/`, workflow +path filters, and branch protection expectations. If the missing check cannot be +caused or fixed from the repo, escalate. + +### 5. Investigate failing checks + +For GitHub Actions failures, inspect runs and failed logs for the current SHA: + +```bash +gh run list --commit "$HEAD_SHA" --json databaseId,workflowName,status,conclusion,url,headSha +gh run view --json databaseId,name,workflowName,status,conclusion,jobs,url,headSha +gh run view --log-failed +``` + +For each failing check, classify it: + +| Failure type | Action | +|---|---| +| Code/test regression | Reproduce locally, fix, and verify | +| Lint/type/build mismatch | Run the matching local command from the workflow and fix it | +| Flake or transient infra issue | Rerun once if evidence supports flakiness | +| External service/status app failure | Escalate with the details URL and owner guess | +| Missing secret/permission/branch protection issue | Escalate immediately | + +Only rerun a failed job once without code changes. Do not loop on reruns. + +### 6. Fix actionable failures + +If the failure is actionable from the checked-out code: + +1. Read the workflow or failing command to identify the real gate. +2. Reproduce locally where reasonable. +3. Make the smallest correct fix. +4. Run focused verification first, then broader verification if needed. +5. Commit in a logical commit. +6. Push before re-checking the PR. + +Do not stop at a local fix. The loop is only complete when the remote PR checks +for the new head SHA are green. + +### 7. Push and repeat + +After each fix: + +```bash +git push +sleep 5 +``` + +Then refresh the PR metadata, get the new `HEAD_SHA`, and restart from Step 3. + +Exit the loop only when: + +- all checks for the latest head SHA are green, or +- a blocker remains after reasonable repair effort, or +- the max iteration count is reached + +### 8. Escalate blockers precisely + +If you cannot get the PR green, report: + +- PR URL +- latest head SHA +- exact failing or missing check names +- details URLs +- what you already tried +- why it is blocked +- who should likely unblock it +- the next concrete action + +Good blocker examples: + +- external status app outage +- missing GitHub secret or permission +- required check name mismatch in branch protection +- persistent flake after one rerun +- failure needs credentials or infrastructure access you do not have + +## Output + +When the skill completes, report: + +- PR URL and branch +- final head SHA +- green/pending/failing check summary +- fixes made and verification run +- whether changes were pushed +- blocker summary if not fully green + +## Notes + +- This skill is intentionally narrower than `check-pr`: it is a repair loop for + PR checks. +- This skill complements `greploop`: Greptile can be perfect while CI is still + red. From 46892ded18cda3fc4aa0edd7f5c0fd2111bd768c Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 07:48:22 -0500 Subject: [PATCH 02/74] Add worktree reseed command Co-Authored-By: Paperclip --- cli/src/__tests__/worktree.test.ts | 78 ++++++++++--- cli/src/commands/worktree.ts | 178 +++++++++++++++++++++++++++++ doc/DEVELOPING.md | 45 +++++--- 3 files changed, 268 insertions(+), 33 deletions(-) diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index 1c8ed5e1..2089c032 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -9,6 +9,8 @@ import { readSourceAttachmentBody, rebindWorkspaceCwd, resolveSourceConfigPath, + resolveWorktreeReseedSource, + resolveWorktreeReseedTargetPaths, resolveGitWorktreeAddArgs, resolveWorktreeMakeTargetPath, worktreeInitCommand, @@ -482,27 +484,69 @@ describe("worktree helpers", () => { } }); - it("requires an explicit source for worktree reseed", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-source-")); - const repoRoot = path.join(tempRoot, "repo"); - const originalCwd = process.cwd(); - const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG; + it("requires an explicit reseed source", () => { + expect(() => resolveWorktreeReseedSource({})).toThrow( + "Pass --from or --from-config/--from-instance explicitly so the reseed source is unambiguous.", + ); + }); + + it("rejects mixed reseed source selectors", () => { + expect(() => resolveWorktreeReseedSource({ + from: "current", + fromInstance: "default", + })).toThrow( + "Use either --from or --from-config/--from-data-dir/--from-instance, not both.", + ); + }); + + it("derives worktree reseed target paths from the adjacent env file", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-")); + const worktreeRoot = path.join(tempRoot, "repo"); + const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); + const envPath = path.join(worktreeRoot, ".paperclip", ".env"); try { - fs.mkdirSync(repoRoot, { recursive: true }); - delete process.env.PAPERCLIP_CONFIG; - process.chdir(repoRoot); - - await expect(worktreeReseedCommand({ seed: false, yes: true })).rejects.toThrow( - "Reseed requires an explicit source.", + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8"); + fs.writeFileSync( + envPath, + [ + "PAPERCLIP_HOME=/tmp/paperclip-worktrees", + "PAPERCLIP_INSTANCE_ID=pap-1132-chat", + ].join("\n"), + "utf8", ); + expect( + resolveWorktreeReseedTargetPaths({ + configPath, + rootPath: worktreeRoot, + }), + ).toMatchObject({ + cwd: worktreeRoot, + homeDir: "/tmp/paperclip-worktrees", + instanceId: "pap-1132-chat", + }); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects reseed targets without worktree env metadata", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-")); + const worktreeRoot = path.join(tempRoot, "repo"); + const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); + + try { + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8"); + fs.writeFileSync(path.join(worktreeRoot, ".paperclip", ".env"), "", "utf8"); + + expect(() => + resolveWorktreeReseedTargetPaths({ + configPath, + rootPath: worktreeRoot, + })).toThrow("does not look like a worktree-local Paperclip instance"); } finally { - process.chdir(originalCwd); - if (originalPaperclipConfig === undefined) { - delete process.env.PAPERCLIP_CONFIG; - } else { - process.env.PAPERCLIP_CONFIG = originalPaperclipConfig; - } fs.rmSync(tempRoot, { recursive: true, force: true }); } }); diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index 3025e955..23eafe11 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -133,6 +133,17 @@ type WorktreeMergeHistoryOptions = { yes?: boolean; }; +type WorktreeReseedOptions = { + from?: string; + to?: string; + fromConfig?: string; + fromDataDir?: string; + fromInstance?: string; + seedMode?: string; + yes?: boolean; + allowLiveTarget?: boolean; +}; + type EmbeddedPostgresInstance = { initialise(): Promise; start(): Promise; @@ -738,6 +749,65 @@ export function resolveSourceConfigPath(opts: WorktreeInitOptions): string { return path.resolve(sourceHome, "instances", sourceInstanceId, "config.json"); } +export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): ResolvedWorktreeReseedSource { + const fromSelector = nonEmpty(input.from); + const fromConfig = nonEmpty(input.fromConfig); + const fromDataDir = nonEmpty(input.fromDataDir); + const fromInstance = nonEmpty(input.fromInstance); + const hasExplicitConfigSource = Boolean(fromConfig || fromDataDir || fromInstance); + + if (fromSelector && hasExplicitConfigSource) { + throw new Error( + "Use either --from or --from-config/--from-data-dir/--from-instance, not both.", + ); + } + + if (fromSelector) { + const endpoint = resolveWorktreeEndpointFromSelector(fromSelector, { allowCurrent: true }); + return { + configPath: endpoint.configPath, + label: endpoint.label, + }; + } + + if (hasExplicitConfigSource) { + const configPath = resolveSourceConfigPath({ + fromConfig: fromConfig ?? undefined, + fromDataDir: fromDataDir ?? undefined, + fromInstance: fromInstance ?? undefined, + }); + return { + configPath, + label: configPath, + }; + } + + throw new Error( + "Pass --from or --from-config/--from-instance explicitly so the reseed source is unambiguous.", + ); +} + +export function resolveWorktreeReseedTargetPaths(input: { + configPath: string; + rootPath: string; +}): WorktreeLocalPaths { + const envEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(input.configPath)); + const homeDir = nonEmpty(envEntries.PAPERCLIP_HOME); + const instanceId = nonEmpty(envEntries.PAPERCLIP_INSTANCE_ID); + + if (!homeDir || !instanceId) { + throw new Error( + `Target config ${input.configPath} does not look like a worktree-local Paperclip instance. Expected PAPERCLIP_HOME and PAPERCLIP_INSTANCE_ID in the adjacent .env.`, + ); + } + + return resolveWorktreeLocalPaths({ + cwd: input.rootPath, + homeDir, + instanceId, + }); +} + function resolveSourceConnectionString(config: PaperclipConfig, envEntries: Record, portOverride?: number): string { if (config.database.mode === "postgres") { const connectionString = nonEmpty(envEntries.DATABASE_URL) ?? nonEmpty(config.database.connectionString); @@ -1326,6 +1396,11 @@ type ResolvedWorktreeEndpoint = { isCurrent: boolean; }; +type ResolvedWorktreeReseedSource = { + configPath: string; + label: string; +}; + function parseGitWorktreeList(cwd: string): GitWorktreeListEntry[] { const raw = execFileSync("git", ["worktree", "list", "--porcelain"], { cwd, @@ -1819,6 +1894,13 @@ function renderMergePlan(plan: Awaited>["pla return lines.join("\n"); } +function resolveRunningEmbeddedPostgresPid(config: PaperclipConfig): number | null { + if (config.database.mode !== "embedded-postgres") { + return null; + } + return readRunningPostmasterPid(path.resolve(config.database.embeddedPostgresDataDir, "postmaster.pid")); +} + async function collectMergePlan(input: { sourceDb: ClosableDb; targetDb: ClosableDb; @@ -2760,6 +2842,89 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, } } +export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise { + printPaperclipCliBanner(); + p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed "))); + + const seedMode = opts.seedMode ?? "full"; + if (!isWorktreeSeedMode(seedMode)) { + throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`); + } + + const targetEndpoint = opts.to + ? resolveWorktreeEndpointFromSelector(opts.to, { allowCurrent: true }) + : resolveCurrentEndpoint(); + const source = resolveWorktreeReseedSource(opts); + + if (path.resolve(source.configPath) === path.resolve(targetEndpoint.configPath)) { + throw new Error("Source and target Paperclip configs are the same. Choose different --from/--to values."); + } + if (!existsSync(source.configPath)) { + throw new Error(`Source config not found at ${source.configPath}.`); + } + + const targetConfig = readConfig(targetEndpoint.configPath); + if (!targetConfig) { + throw new Error(`Target config not found at ${targetEndpoint.configPath}.`); + } + const sourceConfig = readConfig(source.configPath); + if (!sourceConfig) { + throw new Error(`Source config not found at ${source.configPath}.`); + } + + const targetPaths = resolveWorktreeReseedTargetPaths({ + configPath: targetEndpoint.configPath, + rootPath: targetEndpoint.rootPath, + }); + const runningTargetPid = resolveRunningEmbeddedPostgresPid(targetConfig); + if (runningTargetPid && !opts.allowLiveTarget) { + throw new Error( + `Target worktree database appears to be running (pid ${runningTargetPid}). Stop Paperclip in ${targetEndpoint.rootPath} before reseeding, or re-run with --allow-live-target if you want to override this guard.`, + ); + } + + const confirmed = opts.yes + ? true + : await p.confirm({ + message: `Overwrite the isolated Paperclip DB for ${targetEndpoint.label} from ${source.label} using ${seedMode} seed mode?`, + initialValue: false, + }); + if (p.isCancel(confirmed) || !confirmed) { + p.log.warn("Reseed cancelled."); + return; + } + + if (runningTargetPid && opts.allowLiveTarget) { + p.log.warning(`Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`); + } + + const spinner = p.spinner(); + spinner.start(`Reseeding ${targetEndpoint.label} from ${source.label} (${seedMode})...`); + try { + const seeded = await seedWorktreeDatabase({ + sourceConfigPath: source.configPath, + sourceConfig, + targetConfig, + targetPaths, + instanceId: targetPaths.instanceId, + seedMode, + }); + spinner.stop(`Reseeded ${targetEndpoint.label} (${seedMode}).`); + p.log.message(pc.dim(`Source: ${source.configPath}`)); + p.log.message(pc.dim(`Target: ${targetEndpoint.configPath}`)); + p.log.message(pc.dim(`Seed snapshot: ${seeded.backupSummary}`)); + for (const rebound of seeded.reboundWorkspaces) { + p.log.message( + pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`), + ); + } + p.outro(pc.green(`Reseed complete for ${targetEndpoint.label}.`)); + } catch (error) { + spinner.stop(pc.red("Failed to reseed worktree database.")); + throw error; + } +} + export function registerWorktreeCommands(program: Command): void { const worktree = program.command("worktree").description("Worktree-local Paperclip instance helpers"); @@ -2833,6 +2998,19 @@ export function registerWorktreeCommands(program: Command): void { .option("--yes", "Skip the interactive confirmation prompt when applying", false) .action(worktreeMergeHistoryCommand); + worktree + .command("reseed") + .description("Re-seed an existing worktree-local instance from another Paperclip instance or worktree") + .option("--from ", "Source worktree path, directory name, branch name, or current") + .option("--to ", "Target worktree path, directory name, branch name, or current (defaults to current)") + .option("--from-config ", "Source config.json to seed from") + .option("--from-data-dir ", "Source PAPERCLIP_HOME used when deriving the source config") + .option("--from-instance ", "Source instance id when deriving the source config") + .option("--seed-mode ", "Seed profile: minimal or full (default: full)", "full") + .option("--yes", "Skip the destructive confirmation prompt", false) + .option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false) + .action(worktreeReseedCommand); + program .command("worktree:cleanup") .description("Safely remove a worktree, its branch, and its isolated instance data") diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 6aa30237..724496e9 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -232,14 +232,38 @@ pnpm paperclipai worktree init --force --seed-mode minimal \ That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances//`, and preserves the git worktree contents themselves. -For existing worktrees, prefer the dedicated reseed command instead of rebuilding the `worktree init --force` flags manually: +For an already-created worktree where you want to keep the existing repo-local config/env and only overwrite the isolated database, use `worktree reseed` instead. Stop the target worktree's Paperclip server first so the command can replace the DB safely. + +**`pnpm paperclipai worktree reseed [options]`** — Re-seed an existing worktree-local instance from another Paperclip instance or worktree while preserving the target worktree's current config, ports, and instance identity. + +| Option | Description | +|---|---| +| `--from ` | Source worktree path, directory name, branch name, or `current` | +| `--to ` | Target worktree path, directory name, branch name, or `current` (defaults to `current`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source `PAPERCLIP_HOME` used when deriving the source config | +| `--from-instance ` | Source instance id when deriving the source config | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `full`) | +| `--yes` | Skip the destructive confirmation prompt | +| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | + +Examples: ```sh -cd /path/to/existing/worktree -pnpm paperclipai worktree reseed --from-config /path/to/source/.paperclip/config.json --seed-mode full -``` +# From the main repo, reseed a worktree from the current default/master instance. +cd /path/to/paperclip +pnpm paperclipai worktree reseed \ + --from current \ + --to PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat \ + --seed-mode full \ + --yes -`worktree reseed` preserves the current worktree's instance id, ports, and branding while replacing only that worktree's isolated Paperclip instance data from the chosen source. +# From inside a worktree, reseed it from the default instance config. +cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat +pnpm paperclipai worktree reseed \ + --from-instance default \ + --seed-mode full +``` **`pnpm paperclipai worktree:make [options]`** — Create `~/NAME` as a git worktree, then initialize an isolated Paperclip instance inside it. This combines `git worktree add` with `worktree init` in a single step. @@ -267,17 +291,6 @@ pnpm paperclipai worktree:make experiment --no-seed **`pnpm paperclipai worktree env [options]`** — Print shell exports for the current worktree-local Paperclip instance. -**`pnpm paperclipai worktree reseed [options]`** — Replace the current worktree instance with a fresh seed from another Paperclip source while preserving the current worktree's ports and instance id. - -| Option | Description | -|---|---| -| `--from-config ` | Source config.json to seed from | -| `--from-data-dir ` | Source `PAPERCLIP_HOME` used when deriving the source config | -| `--from-instance ` | Source instance id when deriving the source config | -| `--home ` | Home root for worktree instances (default: `~/.paperclip-worktrees`) | -| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | -| `--yes` | Skip the destructive confirmation prompt | - | Option | Description | |---|---| | `-c, --config ` | Path to config file | From 735c591bad69246403a41d4bf750fb4bd902853d Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 08:15:26 -0500 Subject: [PATCH 03/74] docs: add manual mcp-server publish steps Co-Authored-By: Paperclip --- doc/PUBLISHING.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/PUBLISHING.md b/doc/PUBLISHING.md index 83a6c4a7..db56540d 100644 --- a/doc/PUBLISHING.md +++ b/doc/PUBLISHING.md @@ -115,6 +115,38 @@ If the first real publish returns npm `E404`, check npm-side prerequisites befor - The initial publish must include `--access public` for a public scoped package. - npm also requires either account 2FA for publishing or a granular token that is allowed to bypass 2FA. +### Manual first publish for `@paperclipai/mcp-server` + +If you need to publish only the MCP server package once by hand, use: + +- `@paperclipai/mcp-server` + +Recommended flow from the repo root: + +```bash +# optional sanity check: this 404s until the first publish exists +npm view @paperclipai/mcp-server version + +# make sure the build output is fresh +pnpm --filter @paperclipai/mcp-server build + +# confirm your local npm auth before the real publish +npm whoami + +# safe preview of the exact publish payload +cd packages/mcp-server +pnpm publish --dry-run --no-git-checks --access public + +# real publish +pnpm publish --no-git-checks --access public +``` + +Notes: + +- Publish from `packages/mcp-server/`, not the repo root. +- If `npm view @paperclipai/mcp-server version` already returns the same version that is in [`packages/mcp-server/package.json`](../packages/mcp-server/package.json), do not republish. Bump the version or use the normal repo-wide release flow in [`scripts/release.sh`](../scripts/release.sh). +- The same npm-side prerequisites apply as above: valid npm auth, permission to publish to the `@paperclipai` scope, `--access public`, and the required publish auth/2FA policy. + ## Version formats Paperclip uses calendar versions: From 87db949d3f01e4bcb1dec5d0adae343f8338de18 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 14:01:13 -0500 Subject: [PATCH 04/74] docs: survey pi and pi-mono hook surfaces --- doc/plans/2026-04-07-pi-hooks-survey.md | 248 ++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 doc/plans/2026-04-07-pi-hooks-survey.md diff --git a/doc/plans/2026-04-07-pi-hooks-survey.md b/doc/plans/2026-04-07-pi-hooks-survey.md new file mode 100644 index 00000000..bc67a704 --- /dev/null +++ b/doc/plans/2026-04-07-pi-hooks-survey.md @@ -0,0 +1,248 @@ +# Pi Hook Survey + +Status: investigation note +Date: 2026-04-07 + +## Why this exists + +We were asked to find the hook surfaces exposed by `pi` and `pi-mono`, then decide which ideas transfer cleanly into Paperclip. + +This note is based on direct source inspection of: + +- `badlogic/pi` default branch and `pi2` branch +- `badlogic/pi-mono` `packages/coding-agent` +- current Paperclip plugin and adapter surfaces in this repo + +## Short answer + +- Current `pi` does not expose a comparable extension hook API. What it exposes today is a JSON event stream from `pi-agent`. +- `pi-mono` does expose a real extension hook system. It is broad, typed, and intentionally allows mutation of agent/runtime behavior. +- Paperclip should copy only the safe subset: + - typed event subscriptions + - read-only run lifecycle events + - explicit worker lifecycle hooks + - plugin-to-plugin events +- Paperclip should not copy the dangerous subset: + - arbitrary mutation hooks on core control-plane decisions + - project-local plugin loading + - built-in tool shadowing by name collision + +## What `pi` has today + +Current `badlogic/pi` is primarily a GPU pod manager plus a lightweight agent runner. It does not expose a `pi.on(...)`-style extension API like `pi-mono`. + +The closest thing to hooks is the `pi-agent --json` event stream: + +- `session_start` +- `user_message` +- `assistant_start` +- `assistant_message` +- `thinking` +- `tool_call` +- `tool_result` +- `token_usage` +- `error` +- `interrupted` + +That makes `pi` useful as an event producer, but not as a host for third-party runtime interception. + +## What `pi-mono` has + +`pi-mono` exposes a real extension API through `packages/coding-agent/src/core/extensions/types.ts`. + +### Extension event hooks + +Verified `pi.on(...)` hook names: + +- `resources_discover` +- `session_start` +- `session_before_switch` +- `session_before_fork` +- `session_before_compact` +- `session_compact` +- `session_shutdown` +- `session_before_tree` +- `session_tree` +- `context` +- `before_provider_request` +- `before_agent_start` +- `agent_start` +- `agent_end` +- `turn_start` +- `turn_end` +- `message_start` +- `message_update` +- `message_end` +- `tool_execution_start` +- `tool_execution_update` +- `tool_execution_end` +- `model_select` +- `tool_call` +- `tool_result` +- `user_bash` +- `input` + +### Other extension surfaces + +`pi-mono` extensions can also: + +- `registerTool(...)` +- `registerCommand(...)` +- `registerShortcut(...)` +- `registerFlag(...)` +- `registerMessageRenderer(...)` +- `registerProvider(...)` +- `unregisterProvider(...)` +- use an inter-extension event bus via `pi.events` + +### Important behavior + +`pi-mono` hooks are not just observers. Several can actively mutate behavior: + +- `before_agent_start` can rewrite the effective system prompt and inject messages +- `context` can replace the message set before an LLM call +- `before_provider_request` can rewrite the serialized provider payload +- `tool_call` can mutate tool inputs and block execution +- `tool_result` can rewrite tool output +- `user_bash` can replace shell execution entirely +- `input` can transform or fully handle user input before normal processing + +That is a good fit for a local coding harness. It is not automatically a good fit for a company control plane. + +## What Paperclip already has + +Paperclip already has several hook-like surfaces, but they are much narrower and safer: + +- plugin worker lifecycle hooks such as `setup()` and `onHealth()` +- declared webhook endpoints for plugins +- scheduled jobs +- a typed plugin event bus with filtering and plugin namespacing +- adapter runtime hooks for logs/status/usage in the run pipeline + +The plugin event bus is already pointed in the right direction: + +- core domain events can be subscribed to +- filters are applied server-side +- plugin-emitted events are namespaced under `plugin..*` +- plugins do not override core behavior by name collision + +## What transfers well to Paperclip + +These ideas from `pi-mono` fit Paperclip with little conceptual risk: + +### 1. Read-only run lifecycle subscriptions + +Paperclip should continue exposing run and transcript events to plugins, for example: + +- run started / finished +- tool started / finished +- usage reported +- issue comment created + +This matches Paperclip's control-plane posture: observe, react, automate. + +### 2. Plugin-to-plugin events + +Paperclip already has this. It is worth keeping and extending. + +This is the clean replacement for many ad hoc hook chains. + +### 3. Explicit worker lifecycle hooks + +Paperclip already has `setup()` and `onHealth()`. That is the right shape. + +If more lifecycle is needed, it should stay explicit and host-controlled. + +### 4. Trusted adapter-level prompt/runtime middleware + +Some `pi-mono` ideas do belong in Paperclip, but only inside trusted adapter/runtime code: + +- prompt shaping before a run starts +- provider request customization +- tool execution wrappers for local coding adapters + +This should be an adapter surface, not a general company plugin surface. + +## What should not transfer directly + +These `pi-mono` capabilities are a bad fit for Paperclip core: + +### 1. Arbitrary mutation hooks on control-plane decisions + +Paperclip should not let general plugins rewrite: + +- issue checkout semantics +- approval outcomes +- budget enforcement +- assignment rules +- company scoping + +Those are core invariants. + +### 2. Tool shadowing by name collision + +`pi-mono`'s low-friction override model is great for a personal coding harness. + +Paperclip should keep plugin tools namespaced and non-shadowing. + +### 3. Project-local plugin loading + +Paperclip is an operator-controlled control plane. Repo-local plugin auto-loading would make behavior too implicit and too hard to govern. + +### 4. UI-session-specific hooks as first-class product surface + +Hooks like: + +- `session_before_switch` +- `session_before_fork` +- `session_before_tree` +- `model_select` +- `input` +- `user_bash` + +are tied to `pi-mono` being an interactive terminal coding harness. + +They do not map directly to Paperclip's board-and-issues model. + +## Recommended Paperclip direction + +If we want a "hooks" story inspired by `pi-mono`, it should split into two layers: + +### Layer 1: safe control-plane plugins + +Allowed surfaces: + +- typed domain event subscriptions +- jobs +- webhooks +- plugin-to-plugin events +- UI slots and bridge actions +- plugin-owned tools and data endpoints + +Disallowed: + +- mutation of core issue/approval/budget invariants + +### Layer 2: trusted runtime middleware + +For adapters and other trusted runtime packages only: + +- prompt assembly hooks +- provider payload hooks +- tool execution wrappers +- transcript rendering helpers + +This is where the best `pi-mono` runtime ideas belong. + +## Bottom line + +If the question is "what hooks do `pi` and `pi-mono` have?": + +- `pi`: JSON output events, not a general extension hook system +- `pi-mono`: a broad extension hook API with 27 named event hooks plus tool/command/provider registration + +If the question is "what works for Paperclip too?": + +- yes: typed event subscriptions, worker lifecycle hooks, namespaced plugin events, read-only run lifecycle events +- maybe, but trusted-only: prompt/provider/tool middleware around adapter execution +- no: arbitrary mutation hooks on control-plane invariants, project-local plugin loading, tool shadowing From 56ee63bfd04633dc072245c67609d82572589955 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 18:52:56 -0500 Subject: [PATCH 05/74] docs: add issue detail speed inventory plan --- ...e-detail-speed-and-optimistic-inventory.md | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 doc/plans/2026-04-07-issue-detail-speed-and-optimistic-inventory.md diff --git a/doc/plans/2026-04-07-issue-detail-speed-and-optimistic-inventory.md b/doc/plans/2026-04-07-issue-detail-speed-and-optimistic-inventory.md new file mode 100644 index 00000000..b9c2f0a8 --- /dev/null +++ b/doc/plans/2026-04-07-issue-detail-speed-and-optimistic-inventory.md @@ -0,0 +1,302 @@ +# 2026-04-07 Issue Detail Speed And Optimistic Inventory + +Status: Proposed +Date: 2026-04-07 +Audience: Product and engineering +Related: +- `ui/src/pages/IssueDetail.tsx` +- `ui/src/components/IssueProperties.tsx` +- `ui/src/api/issues.ts` +- `ui/src/lib/queryKeys.ts` +- `server/src/routes/issues.ts` +- `server/src/services/issues.ts` +- [PAP-1192](/PAP/issues/PAP-1192) +- [PAP-1191](/PAP/issues/PAP-1191) +- [PAP-1188](/PAP/issues/PAP-1188) +- [PAP-1119](/PAP/issues/PAP-1119) +- [PAP-945](/PAP/issues/PAP-945) +- [PAP-1165](/PAP/issues/PAP-1165) +- [PAP-890](/PAP/issues/PAP-890) +- [PAP-254](/PAP/issues/PAP-254) +- [PAP-138](/PAP/issues/PAP-138) + +## 1. Purpose + +This note inventories the Paperclip issues that point to the same UX class of problem: + +- pages feel slow because they over-fetch or refetch too much +- actions feel slow because the UI waits for the round trip before reflecting obvious local intent +- optimistic updates exist in some places, but not in a consistent system + +The immediate trigger is [PAP-1192](/PAP/issues/PAP-1192): the issue detail page now feels very slow. + +## 2. Short Answer + +The issue detail page is not obviously blocked by one pathological endpoint. The main problem is the shape of the page: + +- `IssueDetail` fans out into many independent queries on mount +- some of those queries fetch full company-wide collections for data that is local to one issue +- common mutations invalidate almost every issue-related query, which creates avoidable refetch storms +- the page has only a minimal top-level `Loading...` fallback and very little staged or sectional loading UX + +Measured against the current assigned issue (`PAP-1191`) on local dev, the slowest single request was the full company issues list: + +- `GET /api/issues/:id` about `18ms` +- `GET /api/issues/:id/comments|activity|approvals|attachments` about `6-8ms` +- `GET /api/companies/:companyId/agents|projects` about `9-11ms` +- `GET /api/companies/:companyId/issues` about `76ms` + +That strongly suggests the current pain is aggregate client fan-out plus over-broad invalidation, not one obviously broken endpoint. + +## 3. Similar Issue Inventory + +## 3.1 Issue-detail and issue-action siblings + +- [PAP-1192](/PAP/issues/PAP-1192): issue page feels like it loads forever +- [PAP-1188](/PAP/issues/PAP-1188): assignee changes in the issue properties pane were slow and needed optimistic UI +- [PAP-945](/PAP/issues/PAP-945): optimistic comment rendering +- [PAP-1003](/PAP/issues/PAP-1003): optimistic comments had duplicate draft/pending behavior +- [PAP-947](/PAP/issues/PAP-947): follow-up breakage from optimistic comments +- [PAP-254](/PAP/issues/PAP-254): long issue threads become sluggish when adding comments +- [PAP-189](/PAP/issues/PAP-189): comment semantics while an issue has a live run + +Pattern: the issue page already has a history of needing both optimistic behavior and bounded thread/loading behavior. `PAP-1192` is the same family, not a new category. + +## 3.2 Inbox and list-view siblings + +- [PAP-1119](/PAP/issues/PAP-1119): optimistic archive had fade-out then snap-back +- [PAP-1165](/PAP/issues/PAP-1165): issue search slow +- [PAP-890](/PAP/issues/PAP-890): issue search slow, make it very fast +- [PAP-138](/PAP/issues/PAP-138): inbox loading feels stuck +- [PAP-470](/PAP/issues/PAP-470): create-issue save state felt slow and awkward + +Pattern: Paperclip already has several places where the right fix was "show intent immediately, then reconcile," not "wait for refetch." + +## 3.3 Broader app-loading siblings + +- [PAP-472](/PAP/issues/PAP-472): dashboard charts load very slowly +- [PAP-797](/PAP/issues/PAP-797): reduce loading states through static generation/caching where possible +- [PAP-799](/PAP/issues/PAP-799): embed company data at build time to eliminate loading states +- [PAP-703](/PAP/issues/PAP-703): faster chat and better visual feedback + +Pattern: the product has recurring pressure to reduce blank/loading states across the app, so the issue-detail work should fit that broader direction. + +## 4. Current Issue Detail Findings + +## 4.1 Mount query fan-out is high + +`ui/src/pages/IssueDetail.tsx` mounts all of these data sources up front: + +- issue detail +- comments +- activity +- linked runs +- linked approvals +- attachments +- live runs +- active run +- full company issues list +- agents list +- auth session +- projects list +- feedback votes +- instance general settings +- plugin slots + +This is too much for the initial view of a single issue. + +## 4.2 The page fetches full company issue data just to derive child issues + +`IssueDetail` currently does: + +- `issuesApi.list(selectedCompanyId!)` +- then filters client-side for `parentId === issue.id` + +That is expensive relative to the need. + +Important detail: + +- the server route already supports `parentId` +- `server/src/services/issues.ts` already supports `parentId` +- but `ui/src/api/issues.ts` does not expose `parentId` in the filter type + +So the client is missing an already-supported narrow query path. + +## 4.3 Comments are still fetched as full-thread loads + +`server/src/routes/issues.ts` and `server/src/services/issues.ts` already support: + +- `after` +- `order` +- `limit` + +But `IssueDetail` still calls `issuesApi.listComments(issueId)` with no cursor or limit and then re-invalidates the full thread after common comment actions. + +That means we already have the server-side building blocks for incremental comment loading, but the page is not using them. + +## 4.4 Cache invalidation is broader than necessary + +`invalidateIssue()` in `IssueDetail` invalidates: + +- detail +- activity +- runs +- approvals +- feedback votes +- attachments +- documents +- live runs +- active run +- multiple issue collections +- sidebar badges + +That is acceptable for correctness, but it is expensive for perceived speed and makes optimistic work feel less stable because the page keeps re-painting from fresh network results. + +## 4.5 Live run state is fetched twice + +The page polls both: + +- `issues.liveRuns(issueId)` every 3s +- `issues.activeRun(issueId)` every 3s + +That is duplicate polling for closely related state. + +## 4.6 Properties panel duplicates more list fetching + +`ui/src/components/IssueProperties.tsx` fetches: + +- session +- agents list +- projects list +- labels +- and, when the blocker picker opens, the full company issues list + +The page and panel are each doing their own list work instead of sharing a narrower issue-detail data model. + +## 4.7 The perceived loading UX is too thin + +`IssueDetail` only shows: + +- plain `Loading...` while the main issue query is pending + +After that, many sub-sections can appear empty or incomplete until their own queries resolve. That makes the page feel slower than the raw request times suggest. + +## 5. Recommended Plan + +## 5.1 Phase 1: Fix perceived speed first + +Ship UX changes that make the page feel immediate before deeper backend reshaping: + +- replace the plain `Loading...` state with an issue-detail skeleton +- give comments, activity, attachments, and sub-issues their own skeleton/empty/loading states +- preserve visible stale data during refetch instead of clearing sections +- show explicit pending state for local actions that are already optimistic + +Why first: + +- it improves the user-facing feel immediately +- it reduces the chance that later data changes still feel slow because the page flashes blank + +## 5.2 Phase 2: Stop fetching the full company issues list for child issues + +Add `parentId` to the `issuesApi.list(...)` filter type and switch `IssueDetail` to: + +- fetch child issues only +- stop loading the full company issue collection on page mount + +This is the highest-confidence narrow win because the server path already exists. + +## 5.3 Phase 3: Convert comments to a bounded + incremental model + +Use the existing server support for: + +- latest comment cursor from heartbeat context or issue bootstrap +- incremental fetch with `after` +- bounded initial fetch with `limit` + +Suggested behavior: + +- first load: fetch the latest N comments +- offer `load earlier` for long threads +- after posting or on live updates: append incrementally instead of invalidating the whole thread + +This should address the same performance family as [PAP-254](/PAP/issues/PAP-254). + +## 5.4 Phase 4: Reduce duplicate polling and invalidation + +Tighten the runtime side of the page: + +- collapse `liveRuns` and `activeRun` into one client source if possible +- stop invalidating unrelated issue collections after mutations that only affect the current issue +- merge server responses into cache where we already have enough information + +Examples: + +- posting a comment should not force a broad company issue list refetch unless list-visible metadata changed +- attachment changes should not invalidate approvals or unrelated live-run queries + +## 5.5 Phase 5: Consider an issue-detail bootstrap contract + +If the page is still too chatty after the client fixes, add one tailored bootstrap surface for the issue detail page. + +Potential bootstrap payload: + +- issue core data +- child issue summaries +- latest comment cursor and recent comment page +- live run summary +- attachment summaries +- approval summaries +- any lightweight mention/selector metadata truly needed at first paint + +This should happen after the obvious client overfetch fixes, not before. + +## 6. Concrete Opportunities By Surface + +## 6.1 Issue detail page + +- narrow child issue fetch from full list to `parentId` +- stage loading by section instead of all-or-nothing perception +- bound initial comments payload +- reduce duplicate live-run polling +- replace broad invalidation with targeted cache writes + +## 6.2 Issue properties panel + +- reuse page-level agents/projects data where possible +- fetch blockers lazily and narrowly +- keep local optimistic field updates without broad page invalidation + +## 6.3 Thread/comment UX + +- append optimistic comments directly into the visible thread +- keep queued/pending comment state stable during reconciliation +- fetch only new comments after the last known cursor + +## 6.4 Cross-app optimistic consistency + +The same standards should apply to: + +- issue archive/unarchive +- issue property edits +- create issue/sub-issue flows +- comment posting +- attachment/document actions where the local result is obvious + +## 7. Suggested Execution Order + +1. `PAP-1192`: issue-detail skeletons and staged loading +2. add `parentId` support to `ui/src/api/issues.ts` and switch child-issue fetching to a narrow query +3. move comments to bounded initial load plus incremental updates +4. shrink invalidation and polling scope +5. only then decide whether a new issue-detail bootstrap endpoint is still needed + +## 8. Success Criteria + +This inventory is successful if the follow-up implementation makes the issue page behave like this: + +1. navigating to an issue shows a shaped skeleton immediately, not plain text +2. the page no longer fetches the full company issue list just to render sub-issues +3. long threads do not require full-thread fetches on every load or comment mutation +4. local actions feel immediate and do not snap back because of broad invalidation +5. the issue page feels faster even when absolute backend timings are already reasonable From 4bd62471f76aa4bc8eb34bfdde7579112fc53dd1 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:29:27 -0500 Subject: [PATCH 06/74] kill chrome test servers too --- scripts/kill-dev.sh | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/scripts/kill-dev.sh b/scripts/kill-dev.sh index 9a53498a..b6dec9d8 100755 --- a/scripts/kill-dev.sh +++ b/scripts/kill-dev.sh @@ -24,6 +24,8 @@ node_lines=() pg_pids=() pg_pidfiles=() pg_data_dirs=() +browser_pids=() +browser_lines=() is_pid_running() { local pid="$1" @@ -87,6 +89,14 @@ while IFS= read -r line; do node_lines+=("$line") done < <(ps aux | grep -E '/paperclip(-[^/]+)?/' | grep node | grep -v grep || true) +# --- Agent browser processes (headless Chrome from ~/.agent-browser) --- +while IFS= read -r line; do + [[ -z "$line" ]] && continue + pid=$(echo "$line" | awk '{print $2}') + browser_pids+=("$pid") + browser_lines+=("$line") +done < <(ps aux | grep -E 'agent-browser/browsers/chrome-.*/Google Chrome for Testing' | grep -v grep || true) + candidate_pidfiles=() candidate_pidfiles+=( "$HOME"/.paperclip/instances/*/db/postmaster.pid @@ -107,7 +117,7 @@ for pidfile in "${candidate_pidfiles[@]:-}"; do append_postgres_from_pidfile "$pidfile" done -if [[ ${#node_pids[@]} -eq 0 && ${#pg_pids[@]} -eq 0 ]]; then +if [[ ${#node_pids[@]} -eq 0 && ${#pg_pids[@]} -eq 0 && ${#browser_pids[@]} -eq 0 ]]; then echo "No Paperclip dev processes found." exit 0 fi @@ -144,6 +154,22 @@ if [[ ${#pg_pids[@]} -gt 0 ]]; then echo "" fi +if [[ ${#browser_pids[@]} -gt 0 ]]; then + echo "Found ${#browser_pids[@]} agent browser process(es):" + echo "" + + for i in "${!browser_pids[@]:-}"; do + line="${browser_lines[$i]}" + pid=$(echo "$line" | awk '{print $2}') + start=$(echo "$line" | awk '{print $9}') + cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}') + cmd=$(echo "$cmd" | sed "s|$HOME/||g") + printf " PID %-7s started %-10s %s\n" "$pid" "$start" "$cmd" + done + + echo "" +fi + if [[ "$DRY_RUN" == true ]]; then echo "Dry run — re-run without --dry to kill these processes." exit 0 @@ -158,6 +184,13 @@ if [[ ${#node_pids[@]} -gt 0 ]]; then sleep 2 fi +if [[ ${#browser_pids[@]} -gt 0 ]]; then + echo "Sending SIGTERM to agent browser processes..." + for pid in "${browser_pids[@]}"; do + kill -TERM "$pid" 2>/dev/null && echo " signaled $pid" || echo " $pid already gone" + done +fi + leftover_pg_pids=() leftover_pg_data_dirs=() for i in "${!pg_pids[@]:-}"; do @@ -203,4 +236,13 @@ if [[ ${#pg_pids[@]} -gt 0 ]]; then done fi +if [[ ${#browser_pids[@]} -gt 0 ]]; then + for pid in "${browser_pids[@]:-}"; do + if kill -0 "$pid" 2>/dev/null; then + echo " agent browser $pid still alive, sending SIGKILL..." + kill -KILL "$pid" 2>/dev/null || true + fi + done +fi + echo "Done." From 4e2027930529fe3f095396e3c81a6bb310f7abe9 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:19:00 -0500 Subject: [PATCH 07/74] fix(skill): add scoped-wake fast path to skip full heartbeat on comment wakes When an agent is woken by a comment (Resume Delta or Wake Payload), the skill now explicitly instructs it to skip Steps 1-4 (identity, approvals, inbox, pick work) and go directly to checkout. This prevents agents from wastefully fetching their full assignment list and announcing "checking my paperclip tasks" when they already know exactly which issue to work on. Co-Authored-By: Paperclip --- skills/paperclip/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 82dd0c38..0148248b 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -27,6 +27,8 @@ Manual local CLI mode (outside heartbeat runs): use `paperclipai agent local-cli Follow these steps every time you wake up: +**Scoped-wake fast path.** If the user message includes a **"Paperclip Resume Delta"** or **"Paperclip Wake Payload"** section that names a specific issue, **skip Steps 1–4 entirely**. Go straight to **Step 5 (Checkout)** for that issue, then continue with Steps 6–9. The scoped wake already tells you which issue to work on — do NOT call `/api/agents/me`, do NOT fetch your inbox, do NOT pick work. Just checkout, read the wake context, do the work, and update. + **Step 1 — Identity.** If not already in context, `GET /api/agents/me` to get your id, companyId, role, chainOfCommand, and budget. **Step 2 — Approval follow-up (when triggered).** If `PAPERCLIP_APPROVAL_ID` is set (or wake reason indicates approval resolution), review the approval first: From d00860b12ae70b6f91389c3e5e0081f3b73f400c Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:22:20 -0500 Subject: [PATCH 08/74] Add in-progress issue recovery plan --- .../2026-04-08-in-progress-issue-recovery.md | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 doc/plans/2026-04-08-in-progress-issue-recovery.md diff --git a/doc/plans/2026-04-08-in-progress-issue-recovery.md b/doc/plans/2026-04-08-in-progress-issue-recovery.md new file mode 100644 index 00000000..32cea208 --- /dev/null +++ b/doc/plans/2026-04-08-in-progress-issue-recovery.md @@ -0,0 +1,225 @@ +# 2026-04-08 In-Progress Issue Recovery Plan + +Status: Proposed +Date: 2026-04-08 +Audience: Product and engineering +Related: +- `server/src/services/heartbeat.ts` +- `server/src/services/issues.ts` +- `server/src/services/issue-assignment-wakeup.ts` +- `server/src/routes/issues.ts` +- `server/src/__tests__/heartbeat-process-recovery.test.ts` +- `server/src/__tests__/issues-checkout-wakeup.test.ts` +- [PAP-1227](/PAP/issues/PAP-1227) + +## 1. Purpose + +This note defines how Paperclip should handle an issue that is: + +- still `in_progress` +- still assigned +- but no longer has anyone actively working on it + +The problem is not just stale UI. It is a control-plane gap: the issue still looks owned, but no future wake is guaranteed, so work can stop indefinitely. + +## 2. Current Behavior + +Paperclip already has several partial protections: + +- checkout adoption when a stale `checkoutRunId` points at a terminal or missing run in `server/src/services/issues.ts` +- execution lock cleanup when `executionRunId` points at a non-active run in both `issues.ts` and `heartbeat.ts` +- orphaned local process recovery in `heartbeat.reapOrphanedRuns()` +- deferred wake promotion in `releaseIssueExecutionAndPromote()` +- one follow-up retry when a run ends without posting an issue comment + +What is still missing is a continuity rule for the issue itself. + +When a heartbeat run finishes and the issue remains `in_progress`, Paperclip currently clears `executionRunId` and may promote an already-deferred wake. If there is no deferred wake, the issue is simply left assigned and `in_progress`. + +That means an issue can legitimately end up in this state: + +- `status = in_progress` +- `assigneeAgentId != null` +- `executionRunId = null` +- `checkoutRunId` points at an old finished run, or is otherwise stale +- no queued/running wake exists for the issue + +At that point, nothing automatically resumes the work. + +## 3. Root Cause + +The system enforces comment continuity, but not execution continuity. + +Today the lifecycle is effectively: + +1. wake the assignee +2. run one heartbeat +3. require a comment +4. stop unless some other event happens + +That is fine for tasks that move themselves to `done`, `blocked`, or `in_review` in one heartbeat. It fails for work that legitimately spans multiple heartbeats but does not produce a new external trigger. + +This is why the issue can "just sit there": there is no invariant saying "`in_progress` must imply an active run, a queued continuation, or an explicit waiting state." + +## 4. Desired Invariant + +For an assigned issue, `in_progress` should mean one of these is true: + +1. there is an active execution run for the issue +2. there is a queued/deferred wake that will resume the issue soon +3. the system has exhausted bounded automatic recovery and has surfaced the issue for explicit human/agent intervention + +What must not be allowed as a steady state is: + +- assigned +- `in_progress` +- no active run +- no queued continuation +- no visible escalation + +## 5. Proposed Plan + +## 5.1 Add a first-class orphaned-issue detector + +Introduce a shared helper that identifies an "orphaned in-progress issue": + +- `status === "in_progress"` +- `assigneeAgentId` is present +- no queued/running run currently owns the issue +- no deferred wake already exists for the issue +- `checkoutRunId` is null, missing, or points at a terminal/missing run + +This should live close to the existing issue/run ownership logic so the rules do not diverge. + +## 5.2 Queue one automatic continuation wake + +When a run finishes, after execution-lock release and deferred-wake promotion, check whether the linked issue is now orphaned. + +If it is, queue exactly one automatic continuation wake for the same assignee. + +Important constraints: + +- do not reassign the issue; V1 explicitly avoids automatic reassignment +- do not reset the issue back to `todo`; it is still owned work +- do not create duplicate queued continuation wakes if one already exists +- keep using the existing stale-checkout adoption path so the next run can legally reclaim the old checkout + +Suggested wake reason: + +- `issue_continuation_needed` + +Suggested payload/context fields: + +- `issueId` +- `retryOfRunId` +- `wakeReason = "issue_continuation_needed"` +- `retryReason = "issue_continuation_needed"` + +## 5.3 Bound retries and escalate explicitly + +The continuation wake must be bounded. + +Recommended rule: + +- first orphaning event: queue one automatic continuation wake +- if the continuation wake also ends and the issue is still orphaned: stop retrying automatically and surface the problem + +Escalation behavior: + +- add an issue comment explaining that work is still `in_progress` but no live run remains +- keep the assignee unchanged +- move the issue to `blocked` only if we want strict workflow semantics for "waiting on intervention" + +My recommendation is: + +- keep the first recovery silent except for activity/run events +- on exhaustion, add a comment and set `status = blocked` + +That creates a visible operator queue instead of leaving the issue silently stranded. + +## 5.4 Add a background sweep for legacy stranded issues + +Run finalization fixes future cases, but it does not repair issues already stranded in existing data. + +Add a periodic sweep, alongside other heartbeat housekeeping, that finds issues already matching the orphaned condition and applies the same recovery path. + +This sweep should: + +- skip issues that already have a queued continuation wake +- skip issues whose assignee is paused/terminated/pending approval +- queue a continuation wake when safe +- otherwise add a visible escalation comment and/or mark `blocked` + +This sweep is the backstop for: + +- server restarts +- historical bugs +- manual DB inconsistencies +- cases where a run died outside the normal finalization path + +## 5.5 Expose the state to operators + +Even with auto-recovery, the UI should make the state visible. + +Add a derived flag or state in the issue read model, something like: + +- `workState = active | queued | orphaned | blocked` + +or: + +- `needsRecovery = true` + +Use that to surface: + +- a badge on issue detail and lists when an issue is `in_progress` with no live run +- a dashboard/inbox count for orphaned assigned work + +This is important because the current state is easy to miss: the issue looks "in progress" even when nobody is actually executing it. + +## 6. Suggested Implementation Order + +## 6.1 Phase 1: continuity on run finalization + +Implement the smallest high-confidence fix in `server/src/services/heartbeat.ts`: + +- after a run reaches terminal state and issue execution is released/promoted, detect whether the issue is orphaned +- queue one continuation wake when needed +- add tests for success, failure, timeout, and cancelled paths where the issue remains `in_progress` + +This prevents new stranded issues created by normal run completion. + +## 6.2 Phase 2: background sweep + +Add a scheduled sweep for existing orphaned issues and for edge cases that bypass normal finalization. + +This repairs the current backlog and makes the system robust across restarts. + +## 6.3 Phase 3: operator visibility + +Expose the derived recovery state in issue APIs and show it in the UI. + +This gives humans a direct answer to "what is assigned but not actually being worked right now?" + +## 7. Test Plan For The Implementation + +The implementation should add focused server tests for: + +- a run that ends successfully while the issue remains `in_progress` and assigned queues one continuation wake +- a run that ends with failure/timeout and leaves the issue orphaned also queues one continuation wake +- no continuation wake is queued when a deferred wake already exists +- no duplicate continuation wake is queued when one is already pending +- the second orphaning event after a continuation retry produces escalation instead of another infinite retry +- the background sweep recovers a pre-existing orphaned issue +- paused or terminated assignees are not auto-woken + +## 8. Recommendation + +The right fix is not automatic reassignment and not silently leaving the issue alone. + +The right fix is: + +- preserve ownership +- auto-resume once +- escalate visibly if continuity still fails + +That matches V1's explicit ownership model while closing the current gap where assigned `in_progress` work can stop forever with no signal. From 0937f07c7916e1d795e8d3d5c73b82f8a117e6bb Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:31:01 -0500 Subject: [PATCH 09/74] Remove standalone issue recovery plan doc --- .../2026-04-08-in-progress-issue-recovery.md | 225 ------------------ 1 file changed, 225 deletions(-) delete mode 100644 doc/plans/2026-04-08-in-progress-issue-recovery.md diff --git a/doc/plans/2026-04-08-in-progress-issue-recovery.md b/doc/plans/2026-04-08-in-progress-issue-recovery.md deleted file mode 100644 index 32cea208..00000000 --- a/doc/plans/2026-04-08-in-progress-issue-recovery.md +++ /dev/null @@ -1,225 +0,0 @@ -# 2026-04-08 In-Progress Issue Recovery Plan - -Status: Proposed -Date: 2026-04-08 -Audience: Product and engineering -Related: -- `server/src/services/heartbeat.ts` -- `server/src/services/issues.ts` -- `server/src/services/issue-assignment-wakeup.ts` -- `server/src/routes/issues.ts` -- `server/src/__tests__/heartbeat-process-recovery.test.ts` -- `server/src/__tests__/issues-checkout-wakeup.test.ts` -- [PAP-1227](/PAP/issues/PAP-1227) - -## 1. Purpose - -This note defines how Paperclip should handle an issue that is: - -- still `in_progress` -- still assigned -- but no longer has anyone actively working on it - -The problem is not just stale UI. It is a control-plane gap: the issue still looks owned, but no future wake is guaranteed, so work can stop indefinitely. - -## 2. Current Behavior - -Paperclip already has several partial protections: - -- checkout adoption when a stale `checkoutRunId` points at a terminal or missing run in `server/src/services/issues.ts` -- execution lock cleanup when `executionRunId` points at a non-active run in both `issues.ts` and `heartbeat.ts` -- orphaned local process recovery in `heartbeat.reapOrphanedRuns()` -- deferred wake promotion in `releaseIssueExecutionAndPromote()` -- one follow-up retry when a run ends without posting an issue comment - -What is still missing is a continuity rule for the issue itself. - -When a heartbeat run finishes and the issue remains `in_progress`, Paperclip currently clears `executionRunId` and may promote an already-deferred wake. If there is no deferred wake, the issue is simply left assigned and `in_progress`. - -That means an issue can legitimately end up in this state: - -- `status = in_progress` -- `assigneeAgentId != null` -- `executionRunId = null` -- `checkoutRunId` points at an old finished run, or is otherwise stale -- no queued/running wake exists for the issue - -At that point, nothing automatically resumes the work. - -## 3. Root Cause - -The system enforces comment continuity, but not execution continuity. - -Today the lifecycle is effectively: - -1. wake the assignee -2. run one heartbeat -3. require a comment -4. stop unless some other event happens - -That is fine for tasks that move themselves to `done`, `blocked`, or `in_review` in one heartbeat. It fails for work that legitimately spans multiple heartbeats but does not produce a new external trigger. - -This is why the issue can "just sit there": there is no invariant saying "`in_progress` must imply an active run, a queued continuation, or an explicit waiting state." - -## 4. Desired Invariant - -For an assigned issue, `in_progress` should mean one of these is true: - -1. there is an active execution run for the issue -2. there is a queued/deferred wake that will resume the issue soon -3. the system has exhausted bounded automatic recovery and has surfaced the issue for explicit human/agent intervention - -What must not be allowed as a steady state is: - -- assigned -- `in_progress` -- no active run -- no queued continuation -- no visible escalation - -## 5. Proposed Plan - -## 5.1 Add a first-class orphaned-issue detector - -Introduce a shared helper that identifies an "orphaned in-progress issue": - -- `status === "in_progress"` -- `assigneeAgentId` is present -- no queued/running run currently owns the issue -- no deferred wake already exists for the issue -- `checkoutRunId` is null, missing, or points at a terminal/missing run - -This should live close to the existing issue/run ownership logic so the rules do not diverge. - -## 5.2 Queue one automatic continuation wake - -When a run finishes, after execution-lock release and deferred-wake promotion, check whether the linked issue is now orphaned. - -If it is, queue exactly one automatic continuation wake for the same assignee. - -Important constraints: - -- do not reassign the issue; V1 explicitly avoids automatic reassignment -- do not reset the issue back to `todo`; it is still owned work -- do not create duplicate queued continuation wakes if one already exists -- keep using the existing stale-checkout adoption path so the next run can legally reclaim the old checkout - -Suggested wake reason: - -- `issue_continuation_needed` - -Suggested payload/context fields: - -- `issueId` -- `retryOfRunId` -- `wakeReason = "issue_continuation_needed"` -- `retryReason = "issue_continuation_needed"` - -## 5.3 Bound retries and escalate explicitly - -The continuation wake must be bounded. - -Recommended rule: - -- first orphaning event: queue one automatic continuation wake -- if the continuation wake also ends and the issue is still orphaned: stop retrying automatically and surface the problem - -Escalation behavior: - -- add an issue comment explaining that work is still `in_progress` but no live run remains -- keep the assignee unchanged -- move the issue to `blocked` only if we want strict workflow semantics for "waiting on intervention" - -My recommendation is: - -- keep the first recovery silent except for activity/run events -- on exhaustion, add a comment and set `status = blocked` - -That creates a visible operator queue instead of leaving the issue silently stranded. - -## 5.4 Add a background sweep for legacy stranded issues - -Run finalization fixes future cases, but it does not repair issues already stranded in existing data. - -Add a periodic sweep, alongside other heartbeat housekeeping, that finds issues already matching the orphaned condition and applies the same recovery path. - -This sweep should: - -- skip issues that already have a queued continuation wake -- skip issues whose assignee is paused/terminated/pending approval -- queue a continuation wake when safe -- otherwise add a visible escalation comment and/or mark `blocked` - -This sweep is the backstop for: - -- server restarts -- historical bugs -- manual DB inconsistencies -- cases where a run died outside the normal finalization path - -## 5.5 Expose the state to operators - -Even with auto-recovery, the UI should make the state visible. - -Add a derived flag or state in the issue read model, something like: - -- `workState = active | queued | orphaned | blocked` - -or: - -- `needsRecovery = true` - -Use that to surface: - -- a badge on issue detail and lists when an issue is `in_progress` with no live run -- a dashboard/inbox count for orphaned assigned work - -This is important because the current state is easy to miss: the issue looks "in progress" even when nobody is actually executing it. - -## 6. Suggested Implementation Order - -## 6.1 Phase 1: continuity on run finalization - -Implement the smallest high-confidence fix in `server/src/services/heartbeat.ts`: - -- after a run reaches terminal state and issue execution is released/promoted, detect whether the issue is orphaned -- queue one continuation wake when needed -- add tests for success, failure, timeout, and cancelled paths where the issue remains `in_progress` - -This prevents new stranded issues created by normal run completion. - -## 6.2 Phase 2: background sweep - -Add a scheduled sweep for existing orphaned issues and for edge cases that bypass normal finalization. - -This repairs the current backlog and makes the system robust across restarts. - -## 6.3 Phase 3: operator visibility - -Expose the derived recovery state in issue APIs and show it in the UI. - -This gives humans a direct answer to "what is assigned but not actually being worked right now?" - -## 7. Test Plan For The Implementation - -The implementation should add focused server tests for: - -- a run that ends successfully while the issue remains `in_progress` and assigned queues one continuation wake -- a run that ends with failure/timeout and leaves the issue orphaned also queues one continuation wake -- no continuation wake is queued when a deferred wake already exists -- no duplicate continuation wake is queued when one is already pending -- the second orphaning event after a continuation retry produces escalation instead of another infinite retry -- the background sweep recovers a pre-existing orphaned issue -- paused or terminated assignees are not auto-woken - -## 8. Recommendation - -The right fix is not automatic reassignment and not silently leaving the issue alone. - -The right fix is: - -- preserve ownership -- auto-resume once -- escalate visibly if continuity still fails - -That matches V1's explicit ownership model while closing the current gap where assigned `in_progress` work can stop forever with no signal. From 482dac7097893fb5da78455895f24eb568361b48 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:36:07 -0500 Subject: [PATCH 10/74] docs: add agent-os technical report --- .../2026-04-08-agent-os-technical-report.md | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 doc/plans/2026-04-08-agent-os-technical-report.md diff --git a/doc/plans/2026-04-08-agent-os-technical-report.md b/doc/plans/2026-04-08-agent-os-technical-report.md new file mode 100644 index 00000000..923cf7d2 --- /dev/null +++ b/doc/plans/2026-04-08-agent-os-technical-report.md @@ -0,0 +1,397 @@ +# Agent OS Technical Report for Paperclip + +Date: 2026-04-08 +Analyzed upstream: `rivet-dev/agent-os` at commit `0063cdccd1dcb1c8e211670cd05482d70d26a5c4` (`0063cdc`), dated 2026-04-06 + +## Executive summary + +`agent-os` is not a competitor to Paperclip's core product. It is an execution substrate: an embedded, VM-like runtime for agents, tools, filesystems, and session orchestration. Paperclip is a control plane: company scoping, task hierarchy, approvals, budgets, activity logs, workspaces, and governance. + +The strongest takeaway is not "copy agent-os wholesale." The strongest takeaway is that Paperclip could selectively use its runtime ideas to improve local agent execution safety, reproducibility, and portability while keeping all company/task/governance logic in Paperclip. + +My recommendation is: + +1. Do not merge agent-os concepts into the Paperclip core product model. +2. Do evaluate an optional `agentos_local` execution adapter or internal runtime experiment. +3. Borrow a few design patterns aggressively: + - layered/snapshotted execution filesystems + - explicit capability-based runtime permissions + - a better host-tools bridge for controlled tool execution + - a normalized session capability model for agent adapters +4. Do not import its workflow/cron/queue abstractions into Paperclip core until they are reconciled with Paperclip's issue/comment/governance model. + +## What agent-os actually is + +From the repo layout and implementation, `agent-os` is a mixed TypeScript/Rust system that provides: + +- an `AgentOs` TypeScript API for creating isolated agent VMs +- a Rust kernel/sidecar that virtualizes filesystem, processes, PTYs, pipes, permissions, and networking +- an ACP-based session model for agent runtimes such as Pi, OpenCode, and Claude-style adapters +- a registry of WASM command packages and mount plugins +- optional host toolkits, cron scheduling, and filesystem mounts + +The repo is substantial already: + +- monorepo with `packages/`, `crates/`, and `registry/` +- roughly 1,200 files just across `packages/`, `crates/`, and `registry/` +- mixed implementation model: TypeScript public API plus Rust kernel/sidecar internals + +## Architecture notes + +### 1. Public runtime surface + +The main API lives in `packages/core/src/agent-os.ts` and exports an `AgentOs` class with methods such as: + +- `create()` +- `createSession()` +- `prompt()` +- `exec()` +- `spawn()` +- `snapshotRootFilesystem()` +- cron scheduling helpers + +This is an execution API, not a coordination API. + +### 2. Virtualized kernel model + +The kernel is implemented in Rust under `crates/kernel/src/`. It models: + +- virtual filesystem +- process table +- PTYs and pipes +- resource accounting +- permissioned filesystem access +- network permission checks + +That gives `agent-os` a much stronger isolation story than Paperclip's current "launch a host CLI in a workspace" local adapter approach. + +### 3. Layered filesystem and snapshots + +The filesystem design is one of the most reusable ideas. `agent-os` uses: + +- a bundled base filesystem +- a writable overlay +- optional mounted filesystems +- snapshot export/import for reusing root states + +This is cleaner than treating every execution workspace as a mutable checkout plus ad hoc cleanup. It enables reproducible starting states and cheap isolation. + +### 4. Capability-based permissions + +The kernel-level permission vocabulary is strong and concrete: + +- filesystem operations +- network operations +- child-process execution +- environment access + +The Rust kernel defaults are deny-oriented, but the high-level JS API currently serializes permissive defaults unless the caller provides a policy. That is an important nuance: the primitive is security-minded, but the product surface is still convenience-first. + +### 5. Host-tools bridge + +`agent-os` exposes host-side tools via a toolkit abstraction (`hostTool`, `toolKit`) and a local RPC bridge. This is a strong pattern because it gives the agent explicit, typed tools rather than ambient shell access to everything on the host. + +### 6. ACP session abstraction + +The session model is more uniform than most agent wrappers. It includes: + +- capabilities +- mode/config options +- permission requests +- sequenced session events +- JSON-RPC transport through ACP adapters + +This is directly relevant to Paperclip because our adapter layer still normalizes each CLI agent in a fairly bespoke way. + +## Paperclip anchor points + +The most relevant current Paperclip surfaces for any future `agent-os` integration are: + +- `packages/adapter-utils/src/types.ts` + - shared adapter contract, session metadata, runtime service reporting, environment tests, and optional `detectModel()` +- `server/src/services/heartbeat.ts` + - heartbeat execution, adapter invocation, cost capture, workspace realization, and issue-comment summaries +- `server/src/services/execution-workspaces.ts` + - execution workspace lifecycle and git readiness/cleanup logic +- `server/src/services/plugin-loader.ts` + - dynamic plugin activation, host capability boundaries, and runtime extension loading +- local adapters such as `packages/adapters/codex-local/src/server/execute.ts` and peers + - current host-CLI execution model that an `agent-os` runtime experiment would complement or replace for selected agents + +## What Paperclip can learn from it + +### 1. A safer local execution substrate + +Paperclip's local adapters currently run host CLIs in managed workspaces and rely on adapter-specific behavior plus process-level controls. That is pragmatic, but weakly isolated. + +`agent-os` shows a path toward: + +- running local agent tooling in a constrained runtime +- applying explicit network/filesystem/env policies +- reducing accidental host leakage +- making adapter behavior more portable across machines + +Best use in Paperclip: + +- as an optional runtime beneath local adapters +- or as a new adapter family for agents that can run inside ACP-compatible `agent-os` sessions + +This fits Paperclip because it improves execution safety without changing the control-plane model. + +### 2. Snapshotted execution roots instead of only mutable workspaces + +Paperclip already has strong execution-workspace concepts, but they are repo/worktree-centric. `agent-os` adds a stronger "start from known lower layers, write into a disposable upper layer" model. + +That could improve: + +- reproducible issue starts +- disposable task sandboxes +- faster reset/cleanup +- "resume from snapshot" behavior for recurring routines +- safe preview environments for risky agent operations + +This is especially interesting for tasks that do not need a full git worktree. + +### 3. A capability vocabulary for runtime governance + +Paperclip has governance at the company/task level: + +- approvals +- budgets +- activity logs +- actor permissions +- company scoping + +It has less structure at the runtime capability level. `agent-os` offers a clear vocabulary that Paperclip could adopt even without adopting the runtime itself: + +- `fs.read`, `fs.write`, `fs.mount_sensitive` +- `network.fetch`, `network.http`, `network.listen`, `network.dns` +- child process execution +- env access + +That vocabulary would improve: + +- adapter configuration schemas +- policy UIs +- execution review surfaces +- future approval gates for governed actions + +### 4. Typed host tools instead of shelling out for everything + +Paperclip's plugin system and adapters already have the beginnings of a controlled extension surface. `agent-os` reinforces the value of exposing capabilities as typed tools rather than raw shell access. + +Concrete Paperclip uses: + +- board-approved toolkits for sensitive operations +- company-scoped service tools +- plugin-defined tools with explicit schemas +- safer execution for common actions like git metadata inspection, preview lookups, deployment status checks, or document generation + +This aligns well with Paperclip's governance story. + +### 5. Better adapter normalization around sessions and capabilities + +Paperclip's adapter contract already supports execution results, session params, environment tests, skill syncing, quota windows, and optional `detectModel()`. But much of the per-agent behavior is still adapter-specific. + +`agent-os` suggests a cleaner normalization target: + +- a standard capability map +- a consistent event stream model +- explicit mode/config surfaces +- explicit permission request semantics + +Paperclip does not need ACP everywhere, but it would benefit from a more formal internal session capability model inspired by this. + +### 6. On-demand heavy sandbox escalation + +One of the best architectural choices in `agent-os` is that it does not pretend every workload fits the lightweight runtime. It has a sandbox extension for workloads that need a fuller environment. + +Paperclip can adopt that philosophy directly: + +- lightweight execution by default +- escalate to full worktree / container / remote sandbox only when needed +- keep the escalation explicit in the issue/run model + +That is better than forcing all tasks into the heaviest environment up front. + +## What does not fit Paperclip well + +### 1. Its built-in orchestration primitives overlap the wrong layer + +`agent-os` includes cron/session/workflow style primitives inside the runtime package. Paperclip already has higher-level orchestration concepts: + +- issues/comments +- heartbeat runs +- approvals +- company/org structure +- execution workspaces +- budget enforcement + +If Paperclip copied `agent-os` cron/workflow/queue ideas directly into core, we would likely duplicate orchestration across two layers. That would blur ownership and make debugging harder. + +Paperclip should keep orchestration authoritative at the control-plane layer. + +### 2. It is not company-scoped or governance-native + +`agent-os` is runtime-first, not company-first. It has no native concepts for: + +- company boundaries +- board/operator actor types +- audit logs for business actions +- issue hierarchy +- approval routing +- budget hard-stop behavior + +Those are Paperclip's differentiators. They should not be displaced by runtime abstractions. + +### 3. It introduces meaningful implementation complexity + +Adopting `agent-os` deeply would add: + +- Rust build/runtime complexity +- sidecar lifecycle management +- new failure modes across JS/Rust boundaries +- more packaging and platform compatibility work +- another abstraction layer for debugging already-complex local adapters + +This is justified only if we want stronger local isolation or portability. It is not justified as a general refactor. + +### 4. Its security model is not a drop-in governance solution + +The permission model is good, but it is low-level. Paperclip would still need to answer: + +- who can authorize a capability +- how approval decisions are logged +- how policies are scoped by company/project/issue/agent +- how runtime permissions interact with budgets and task status + +In other words, `agent-os` can supply enforcement primitives, not the control policy system itself. + +### 5. The agent compatibility story is still selective + +The repo is explicit that some runtimes are planned, partial, or still being adapted. In practice this means: + +- good ideas for ACP-native or compatible agents +- less certainty for every CLI agent we support today +- real integration work for Codex/Cursor/Gemini-style Paperclip adapters + +So the main near-term value is not universal replacement. It is selective use where compatibility is strong. + +## Concrete recommendations for Paperclip + +### Recommendation A: prototype an optional `agentos_local` adapter + +This is the highest-value experiment. + +Goal: + +- run one supported agent type inside `agent-os` +- keep Paperclip heartbeat/task/workspace/budget logic unchanged +- evaluate startup time, isolation, transcript quality, and operational complexity + +Good first target: + +- `pi_local` or `opencode_local` + +Why not start with Codex: + +- Paperclip's Codex adapter is already important and carries repo-specific behavior +- `agent-os`'s Codex story is present in the registry/docs, but the safest path is to validate the runtime on a less central adapter first + +Success criteria: + +- heartbeat can invoke the adapter reliably +- session resume works across heartbeats +- Paperclip still records logs, summaries, cost metadata, and issue comments normally +- runtime permissions can be configured without breaking common tasks + +### Recommendation B: adopt capability vocabulary into adapter configs + +Even without using `agent-os`, Paperclip should consider standardizing adapter/runtime permissions around a vocabulary like: + +- filesystem +- network +- subprocess/tool execution +- environment access + +This would improve: + +- schema-driven adapter UIs +- future approvals +- observability +- policy portability across adapters + +### Recommendation C: explore snapshot-backed execution workspaces + +Paperclip should evaluate whether some execution workspaces can be backed by: + +- a reusable lower snapshot +- a disposable upper layer +- optional mounts for project data or artifacts + +This is most valuable for: + +- non-repo tasks +- repeatable routines +- preview/test environments +- isolation-heavy local execution + +It is less urgent for full repo editing flows that already benefit from git worktrees. + +### Recommendation D: strengthen typed tool surfaces + +Paperclip plugins and adapters should continue moving toward explicit typed tools over ad hoc shell access. `agent-os` confirms that this is the right direction. + +This is a good fit for: + +- plugin tools +- workspace runtime services +- governed operations that need approval or auditability + +### Recommendation E: do not import runtime-level workflows into Paperclip core + +Paperclip should not copy `agent-os` cron/workflow/queue concepts into core orchestration yet. + +If we want them later, they must map cleanly onto: + +- issues +- comments +- heartbeats +- approvals +- budgets +- activity logs + +Without that mapping, they would create a second orchestration system inside the product. + +## A practical integration map + +### Best near-term fits + +- optional local adapter runtime +- runtime capability schema +- typed host-tool ideas for plugins/adapters +- snapshot ideas for disposable execution roots + +### Medium-term fits + +- stronger session capability normalization across adapters +- policy-aware runtime permission UI +- selective ACP-inspired event normalization + +### Poor fits right now + +- moving Paperclip orchestration into agent-os workflows +- replacing company/task/governance models with runtime constructs +- making Rust sidecars a mandatory dependency for all local execution + +## Bottom line + +`agent-os` is useful to Paperclip as an execution technology reference, not as a product model. + +Paperclip should treat it the same way it treats sandboxes or agent CLIs: + +- execution substrate underneath the control plane +- optional where the tradeoff is worth it +- never the source of truth for company/task/governance state + +If we do one thing from this report, it should be a narrowly scoped `agentos_local` experiment plus a design pass on capability-based runtime permissions. Those two ideas have the best upside and the lowest architectural risk. From 5758aba91e4c1b9b995400d7b7af0578d34f0237 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:43:58 -0500 Subject: [PATCH 11/74] docs: add agent-os follow-up plan --- .../2026-04-08-agent-os-follow-up-plan.md | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 doc/plans/2026-04-08-agent-os-follow-up-plan.md diff --git a/doc/plans/2026-04-08-agent-os-follow-up-plan.md b/doc/plans/2026-04-08-agent-os-follow-up-plan.md new file mode 100644 index 00000000..52029943 --- /dev/null +++ b/doc/plans/2026-04-08-agent-os-follow-up-plan.md @@ -0,0 +1,261 @@ +# PAP-1229 Agent OS Follow-up Plan + +Date: 2026-04-08 +Related issue: `PAP-1229` +Companion analysis: `doc/plans/2026-04-08-agent-os-technical-report.md` + +## Goal + +Turn the `agent-os` research into a low-risk Paperclip execution plan that preserves Paperclip's control-plane model while testing the few runtime ideas that appear worth adopting. + +## Decision summary + +Paperclip should not absorb `agent-os` as a product model or orchestration layer. + +Paperclip should evaluate `agent-os` in three narrow areas: + +1. optional agent runtime for selected local adapters +2. capability-based runtime permission vocabulary +3. snapshot-backed disposable execution roots + +Everything else should stay out of scope unless those three experiments produce strong evidence. + +## Success condition + +This work is successful when Paperclip has: + +- a clear yes/no answer on whether `agent-os` is worth supporting as an execution substrate +- a concrete adapter/runtime experiment with measurable results +- a proposed runtime capability model that fits current Paperclip adapters +- a clear decision on whether snapshot-backed execution roots are worth integrating + +## Non-goals + +Do not: + +- replace Paperclip heartbeats, issues, comments, approvals, or budgets with `agent-os` primitives +- introduce Rust/sidecar requirements for all local execution paths +- migrate all adapters at once +- add runtime workflow/queue abstractions to Paperclip core + +## Existing Paperclip integration points + +The plan should stay anchored to these existing surfaces: + +- `packages/adapter-utils/src/types.ts` + - adapter contract, runtime service reporting, session metadata, and capability normalization targets +- `server/src/services/heartbeat.ts` + - execution entry point, log capture, issue comment summaries, and cost reporting +- `server/src/services/execution-workspaces.ts` + - current workspace lifecycle and git-oriented cleanup/readiness model +- `server/src/services/plugin-loader.ts` + - typed host capability boundary and extension loading patterns +- local adapter implementations in `packages/adapters/*/src/server/` + - current execution behavior to compare against an `agent-os`-backed path + +## Phase plan + +### Phase 0: constraints and experiment design + +Objective: + +- make the evaluation falsifiable before writing integration code + +Deliverables: + +- short experiment brief added to this document or a child issue +- chosen first runtime target: `pi_local` or `opencode_local` +- baseline metrics definition + +Questions to lock down: + +- what exact developer experience should improve +- what security/isolation property we expect to gain +- what failure modes are unacceptable +- whether the prototype is adapter-only or a deeper internal runtime abstraction spike + +Exit criteria: + +- a single first target chosen +- measurable comparison criteria agreed on + +Recommended metrics: + +- cold start latency +- session resume reliability across heartbeats +- transcript/log quality +- implementation complexity +- operational complexity on local dev machines + +### Phase 1: `agentos_local` spike + +Objective: + +- prove that Paperclip can drive one local agent through an `agent-os` runtime without breaking heartbeat semantics + +Suggested scope: + +- implement a new experimental adapter, `agentos_local`, or a feature-flagged runtime path under one existing adapter +- start with `pi_local` or `opencode_local` +- keep Paperclip's existing heartbeat, issue, workspace, and comment flow authoritative + +Minimum implementation shape: + +- adapter accepts model/runtime config +- `server/src/services/heartbeat.ts` still owns run lifecycle +- execution result still maps into existing `AdapterExecutionResult` +- session state still fits current `sessionParams` / `sessionDisplayId` flow + +What to verify: + +- checkout and heartbeat flow still work end to end +- resume across multiple heartbeats works +- logs/transcripts remain readable in the UI +- failure paths surface cleanly in issue comments and run logs + +Exit criteria: + +- one agent type can run reliably through the new path +- documented comparison against the existing local adapter path +- explicit recommendation: continue, pause, or abandon + +### Phase 2: capability-based runtime permissions + +Objective: + +- introduce a Paperclip-native capability vocabulary without coupling the product to `agent-os` + +Suggested scope: + +- extend adapter config schema vocabulary for runtime permissions +- prototype normalized capabilities such as: + - `fs.read` + - `fs.write` + - `network.fetch` + - `network.listen` + - `process.spawn` + - `env.read` + +Integration targets: + +- `packages/adapter-utils/src/types.ts` +- adapter config-schema support +- server-side runtime config validation +- future board-facing UI for permissions, if needed + +What to avoid: + +- building a full human policy UI before the vocabulary is proven useful +- forcing every adapter to implement capability enforcement immediately + +Exit criteria: + +- documented capability schema +- one adapter path using it meaningfully +- clear compatibility story for non-`agent-os` adapters + +### Phase 3: snapshot-backed execution root experiment + +Objective: + +- determine whether a layered/snapshotted root model improves some Paperclip workloads + +Suggested scope: + +- evaluate it only for disposable or non-repo-heavy tasks first +- keep git worktree-based repo editing as the default for codebase tasks + +Promising use cases: + +- routine-style runs +- ephemeral preview/test environments +- isolated document/artifact generation +- tasks that do not need full git history or branch semantics + +Integration targets: + +- `server/src/services/execution-workspaces.ts` +- workspace realization paths called from `server/src/services/heartbeat.ts` + +Exit criteria: + +- clear statement on which workload classes benefit +- clear statement on which workloads should stay on worktrees +- go/no-go decision for broader implementation + +### Phase 4: typed host tool evaluation + +Objective: + +- identify where Paperclip should prefer explicit typed tools over ambient shell access + +Suggested scope: + +- compare `agent-os` host-toolkit ideas with existing plugin and runtime-service surfaces +- choose 1-2 sensitive operations that should become typed tools + +Good candidates: + +- git metadata/status inspection +- runtime service inspection +- deployment/preview status retrieval +- generated artifact publishing + +Exit criteria: + +- one concrete proposal for typed-tool adoption in Paperclip +- clear statement on whether this belongs in plugins, adapters, or core services + +## Recommended sequencing + +Recommended order: + +1. Phase 0 +2. Phase 1 +3. Phase 2 +4. Phase 3 +5. Phase 4 + +Reasoning: + +- Phase 1 is the fastest way to invalidate or validate the entire `agent-os` direction +- Phase 2 is valuable even if Phase 1 is abandoned +- Phase 3 should wait until there is confidence that the runtime approach is operationally worthwhile +- Phase 4 is useful independently but should be informed by what Phase 1 and Phase 2 expose + +## Risks + +### Technical risk + +- `agent-os` introduces Rust sidecar and packaging complexity that may outweigh runtime benefits + +### Product risk + +- runtime experimentation could blur the boundary between Paperclip as control plane and Paperclip as execution platform + +### Integration risk + +- session semantics, log formatting, and failure behavior may degrade relative to current local adapters + +### Scope risk + +- a small runtime spike could expand into an adapter-system rewrite if not kept tightly bounded + +## Guardrails + +To keep this effort controlled: + +- keep all experiments behind a clearly experimental adapter or feature flag +- do not change issue/comment/approval/budget semantics to suit the runtime +- measure against current local adapters instead of judging in isolation +- stop after Phase 1 if the operational burden is already clearly too high + +## Proposed next action + +The next concrete action should be a small implementation spike issue: + +- title: `Prototype experimental agentos_local runtime for one local adapter` +- target adapter: `opencode_local` unless `pi_local` is materially easier +- expected output: code spike, short verification notes, and a continue/stop recommendation + +If leadership wants planning only and no spike yet, this document is the handoff artifact for that decision. From 0d270655ab7b9b2182ec1263d65411f5d5e475cb Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:45:53 -0500 Subject: [PATCH 12/74] Clarify repo plan docs vs issue plan documents --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dc5e9432..524c573a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,8 +81,8 @@ If you change schema/API behavior, update all impacted layers: 4. Do not replace strategic docs wholesale unless asked. Prefer additive updates. Keep `doc/SPEC.md` and `doc/SPEC-implementation.md` aligned. -5. Keep plan docs dated and centralized. -New plan documents belong in `doc/plans/` and should use `YYYY-MM-DD-slug.md` filenames. +5. Keep repo plan docs dated and centralized. +When you are creating a plan file in the repository itself, new plan documents belong in `doc/plans/` and should use `YYYY-MM-DD-slug.md` filenames. This does not replace Paperclip issue planning: if a Paperclip issue asks for a plan, update the issue `plan` document per the `paperclip` skill instead of creating a repo markdown file. ## 6. Database Change Workflow From b1e9215375f79380ebb1879c80f757f8c6bb2241 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 17:48:44 -0500 Subject: [PATCH 13/74] docs: add browser process cleanup plan --- ...4-08-agent-browser-process-cleanup-plan.md | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 doc/plans/2026-04-08-agent-browser-process-cleanup-plan.md diff --git a/doc/plans/2026-04-08-agent-browser-process-cleanup-plan.md b/doc/plans/2026-04-08-agent-browser-process-cleanup-plan.md new file mode 100644 index 00000000..42024730 --- /dev/null +++ b/doc/plans/2026-04-08-agent-browser-process-cleanup-plan.md @@ -0,0 +1,238 @@ +# PAP-1231 Agent Browser Process Cleanup Plan + +Status: Proposed +Date: 2026-04-08 +Related issue: `PAP-1231` +Audience: Engineering + +## Goal + +Explain why browser processes accumulate during local agent runs and define a cleanup plan that fixes the general process-ownership problem rather than treating `agent-browser` as a one-off. + +## Short answer + +Yes, there is a likely root cause in Paperclip's local execution model. + +Today, heartbeat-run local adapters persist and manage only the top-level spawned PID. Their timeout/cancel path uses direct `child.kill()` semantics. That is weaker than the runtime-service path, which already tracks and terminates whole process groups. + +If Codex, Claude, Cursor, or a skill launched through them starts Chrome or Chromium helpers, Paperclip can lose ownership of those descendants even when it still believes it handled the run correctly. + +## Observed implementation facts + +### 1. Heartbeat-run local adapters track only one PID + +`packages/adapter-utils/src/server-utils.ts` + +- `runChildProcess()` spawns the adapter command and records only `child.pid` +- timeout handling sends `SIGTERM` and then `SIGKILL` to the direct child +- there is no process-group creation or process-group kill path there today + +`packages/db/src/schema/heartbeat_runs.ts` + +- `heartbeat_runs` stores `process_pid` +- there is no persisted `process_group_id` + +`server/src/services/heartbeat.ts` + +- cancellation logic uses the in-memory child handle and calls `child.kill()` +- orphaned-run recovery checks whether the recorded direct PID is alive +- the recovery model is built around one tracked process, not a descendant tree + +### 2. Workspace runtime already uses stronger ownership + +`server/src/services/workspace-runtime.ts` + +- runtime services are spawned with `detached: process.platform !== "win32"` +- the service record stores `processGroupId` +- shutdown calls `terminateLocalService()` with group-aware killing + +`server/src/services/local-service-supervisor.ts` + +- `terminateLocalService()` prefers `process.kill(-processGroupId, signal)` on POSIX +- it escalates from `SIGTERM` to `SIGKILL` + +This is the clearest internal comparison point: Paperclip already has one local-process subsystem that treats process-group ownership as the right abstraction. + +### 3. The current recovery path explains why leaks would be visible but hard to reason about + +If the direct adapter process exits, hangs, or is cancelled after launching a browser subtree: + +- Paperclip may think it cancelled the run because the parent process is gone +- descendant Chrome helpers may still be running +- orphan recovery has no persisted process-group identity to reconcile or reap later + +That makes the failure look like an `agent-browser` problem when the more general bug is "executor descendants are not owned strongly enough." + +## Why `agent-browser` makes the problem obvious + +Inference: + +- Chromium is intentionally multi-process +- browser automation often leaves a browser process plus renderer, GPU, utility, and crashpad/helper children +- skills that open browsers repeatedly amplify the symptom because each run can produce several descendant processes + +So `agent-browser` is probably not the root cause. It is the workload that exposes the weak ownership model fastest. + +## Success condition + +This work is successful when Paperclip can: + +1. start a local adapter run and own the full descendant tree it created +2. cancel, timeout, or recover that run without leaving Chrome descendants behind on POSIX +3. detect and clean up stale local descendants after server restarts +4. expose enough metadata that operators can see which run owns which spawned process tree + +## Non-goals + +Do not: + +- special-case `agent-browser` only +- depend on manual `pkill chrome` cleanup as the primary fix +- require every skill author to add bespoke browser teardown logic before Paperclip can clean up correctly +- change remote/http adapter behavior as part of the first pass + +## Proposed plan + +### Phase 0: reproduce and instrument + +Objective: + +- make the leak measurable from Paperclip's side before changing execution semantics + +Work: + +- add a reproducible local test script or fixture that launches a child process which itself launches descendants and ignores normal parent exit +- capture parent PID, descendant PIDs, and run ID in logs during local adapter execution +- document current behavior separately for: + - normal completion + - timeout + - explicit cancellation + - server restart during run + +Deliverable: + +- one short repro note attached to the implementation issue or child issue + +### Phase 1: give heartbeat-run local adapters process-group ownership + +Objective: + +- align adapter-run execution with the stronger runtime-service model + +Work: + +- update `runChildProcess()` to create a dedicated process group on POSIX +- persist both: + - direct PID + - process-group ID +- update the run cancellation and timeout paths to kill the group first, then escalate +- keep direct-PID fallback behavior for platforms where group kill is not available + +Likely touched surfaces: + +- `packages/adapter-utils/src/server-utils.ts` +- `packages/db/src/schema/heartbeat_runs.ts` +- `packages/shared/src/types/heartbeat.ts` +- `server/src/services/heartbeat.ts` + +Important design choice: + +- use the same ownership model for all local child-process adapters, not just Codex or Claude + +### Phase 2: make restart recovery group-aware + +Objective: + +- prevent stale descendants from surviving server crashes or restarts indefinitely + +Work: + +- teach orphan reconciliation to inspect the persisted process-group ID, not only the direct PID +- if the direct parent is gone but the group still exists, mark the run as detached-orphaned with clearer metadata +- decide whether restart recovery should: + - adopt the still-running group, or + - terminate it as unrecoverable + +Recommendation: + +- for heartbeat runs, prefer terminating unrecoverable orphan groups rather than adopting them unless we can prove the adapter session remains safe and observable + +Reason: + +- runtime services are long-lived and adoptable +- heartbeat runs are task executions with stricter audit and cancellation semantics + +### Phase 3: add operator-visible cleanup tools + +Objective: + +- make the system diagnosable when ownership still fails + +Work: + +- surface the tracked process metadata in run details or debug endpoints +- add a control-plane cleanup action or CLI utility for stale local run processes owned by Paperclip +- scope cleanup by run/agent/company instead of broad browser-name matching + +This should replace ad hoc scripts as the general-purpose escape hatch. + +### Phase 4: cover platform and regression cases + +Objective: + +- keep the fix from regressing and define platform behavior explicitly + +Tests to add: + +- unit tests around process-group-aware cancellation in adapter execution utilities +- heartbeat recovery tests for: + - surviving descendant tree after parent loss + - timeout cleanup + - cancellation cleanup +- platform-conditional behavior notes for Windows, where negative-PID group kill does not apply + +## Recommended first implementation slice + +The first shipping slice should be narrow: + +1. introduce process-group ownership for local heartbeat-run adapters on POSIX +2. persist group metadata on `heartbeat_runs` +3. switch timeout/cancel paths from direct-child kill to group kill +4. add one regression test that proves descendants die with the parent run + +That should address the main Chrome accumulation path without taking on the full restart-recovery design in the same patch. + +## Risks + +### 1. Over-killing unrelated processes + +If process-group boundaries are created incorrectly, cleanup could terminate more than the run owns. + +Mitigation: + +- create a fresh process group only for the spawned adapter command +- persist and target that exact group + +### 2. Cross-platform differences + +Windows does not support the POSIX negative-PID kill pattern used elsewhere in the repo. + +Mitigation: + +- ship POSIX-first +- keep direct-child fallback on Windows +- document Windows as partial until job-object or equivalent handling is designed + +### 3. Session recovery complexity + +Adopting a still-running orphaned group may look attractive but can break observability if stdout/stderr pipes are already gone. + +Mitigation: + +- default to deterministic cleanup for heartbeat runs unless adoption is explicitly proven safe + +## Recommendation + +Treat this as a Paperclip executor ownership bug, not an `agent-browser` bug. + +`agent-browser` should remain a useful repro case, but the implementation should be shared across all local child-process adapters so any descendant process tree spawned by Codex, Claude, Cursor, Gemini, Pi, or OpenCode is owned and cleaned up consistently. From 8e885773713265af2d3f6eb70a6c95a71c52b313 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 9 Apr 2026 06:12:29 -0500 Subject: [PATCH 14/74] chore(dev): preflight workspace links and simplify worktree helpers --- cli/src/__tests__/worktree.test.ts | 5 +- cli/src/commands/worktree.ts | 183 +--------------------- package.json | 9 +- scripts/ensure-workspace-package-links.ts | 9 +- scripts/kill-agent-browsers.sh | 65 ++++++++ 5 files changed, 83 insertions(+), 188 deletions(-) create mode 100755 scripts/kill-agent-browsers.sh diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index 2089c032..3245da05 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -573,6 +573,7 @@ describe("worktree helpers", () => { try { fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true }); + fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true }); fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(sourceRoot, { recursive: true }); @@ -590,6 +591,7 @@ describe("worktree helpers", () => { }); fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8"); fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8"); + fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8"); fs.writeFileSync( currentPaths.envPath, [ @@ -606,7 +608,6 @@ describe("worktree helpers", () => { await worktreeReseedCommand({ fromConfig: sourcePaths.configPath, - seed: false, yes: true, }); @@ -628,7 +629,7 @@ describe("worktree helpers", () => { } fs.rmSync(tempRoot, { recursive: true, force: true }); } - }); + }, 20_000); it("restores the current worktree config and instance data if reseed fails", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-")); diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index 23eafe11..963ae5e8 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -98,22 +98,6 @@ type WorktreeMakeOptions = WorktreeInitOptions & { startPoint?: string; }; -type WorktreeReseedOptions = { - fromConfig?: string; - fromDataDir?: string; - fromInstance?: string; - home?: string; - seedMode?: string; - yes?: boolean; - seed?: boolean; -}; - -type WorktreeReseedBackup = { - tempRoot: string; - repoConfigDirBackup: string | null; - instanceRootBackup: string | null; -}; - type WorktreeEnvOptions = { config?: string; json?: boolean; @@ -964,6 +948,8 @@ async function seedWorktreeDatabase(input: { input.sourceConfig.database.embeddedPostgresDataDir, input.sourceConfig.database.embeddedPostgresPort, ); + const sourceAdminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${sourceHandle.port}/postgres`; + await ensurePostgresDatabase(sourceAdminConnectionString, "paperclip"); } const sourceConnectionString = resolveSourceConnectionString( input.sourceConfig, @@ -1138,160 +1124,6 @@ export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise { - if (!existsSync(sourcePath)) { - return null; - } - await fsPromises.cp(sourcePath, targetPath, { recursive: true }); - return targetPath; -} - -async function snapshotWorktreeReseedState(target: { - repoConfigDir: string; - instanceRoot: string; -}): Promise { - const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-reseed-backup-")); - return { - tempRoot, - repoConfigDirBackup: await snapshotDirectory( - target.repoConfigDir, - path.resolve(tempRoot, "repo-config"), - ), - instanceRootBackup: await snapshotDirectory( - target.instanceRoot, - path.resolve(tempRoot, "instance-root"), - ), - }; -} - -async function restoreDirectoryBackup(backupPath: string | null, targetPath: string): Promise { - rmSync(targetPath, { recursive: true, force: true }); - if (!backupPath) { - return; - } - await fsPromises.cp(backupPath, targetPath, { recursive: true }); -} - -async function restoreWorktreeReseedState( - backup: WorktreeReseedBackup, - target: { repoConfigDir: string; instanceRoot: string }, -): Promise { - await restoreDirectoryBackup(backup.repoConfigDirBackup, target.repoConfigDir); - await restoreDirectoryBackup(backup.instanceRootBackup, target.instanceRoot); -} - -export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise { - printPaperclipCliBanner(); - p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed "))); - - if (!hasExplicitSourceSelection(opts)) { - throw new Error( - "Reseed requires an explicit source. Pass --from-config or --from-instance (optionally with --from-data-dir).", - ); - } - - const target = resolveCurrentWorktreeReseedState({ home: opts.home }); - const sourceConfigPath = resolveSourceConfigPath(opts); - if (path.resolve(sourceConfigPath) === target.currentConfigPath) { - throw new Error( - "Source and target Paperclip configs are the same. Pass a different source instance/config when reseeding.", - ); - } - - const seedMode = opts.seedMode ?? "minimal"; - if (!isWorktreeSeedMode(seedMode)) { - throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`); - } - - const confirmed = opts.yes - ? true - : await p.confirm({ - message: `Reseed the current worktree instance (${target.instanceId}) from ${sourceConfigPath}? This overwrites only the current worktree Paperclip instance data.`, - initialValue: false, - }); - if (p.isCancel(confirmed) || !confirmed) { - p.log.warn("Reseed cancelled."); - return; - } - - const targetPaths = resolveWorktreeLocalPaths({ - cwd: process.cwd(), - homeDir: target.homeDir, - instanceId: target.instanceId, - }); - const backup = await snapshotWorktreeReseedState(targetPaths); - - try { - await runWorktreeInit({ - name: target.worktreeName, - color: target.worktreeColor, - instance: target.instanceId, - home: target.homeDir, - fromConfig: opts.fromConfig, - fromDataDir: opts.fromDataDir, - fromInstance: opts.fromInstance, - sourceConfigPathOverride: sourceConfigPath, - serverPort: target.serverPort, - dbPort: target.dbPort, - seed: opts.seed ?? true, - seedMode, - force: true, - }); - } catch (error) { - await restoreWorktreeReseedState(backup, targetPaths); - throw error; - } finally { - rmSync(backup.tempRoot, { recursive: true, force: true }); - } -} - export async function worktreeMakeCommand(nameArg: string, opts: WorktreeMakeOptions): Promise { printPaperclipCliBanner(); p.intro(pc.bgCyan(pc.black(" paperclipai worktree:make "))); @@ -2968,17 +2800,6 @@ export function registerWorktreeCommands(program: Command): void { .option("--json", "Print JSON instead of shell exports") .action(worktreeEnvCommand); - worktree - .command("reseed") - .description("Replace the current worktree instance with a fresh seed while preserving this worktree's ports and instance id") - .option("--from-config ", "Source config.json to seed from") - .option("--from-data-dir ", "Source PAPERCLIP_HOME used when deriving the source config") - .option("--from-instance ", "Source instance id when deriving the source config") - .option("--home ", `Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`) - .option("--seed-mode ", "Seed profile: minimal or full (default: minimal)", "minimal") - .option("--yes", "Skip the destructive confirmation prompt", false) - .action(worktreeReseedCommand); - program .command("worktree:list") .description("List git worktrees visible from this repo and whether they look like Paperclip worktrees") diff --git a/package.json b/package.json index 58ffa103..7697e1d8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "scripts": { + "preflight:workspace-links": "pnpm exec tsx scripts/ensure-workspace-package-links.ts", "dev": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts watch", "dev:watch": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts watch", "dev:once": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-runner.ts dev", @@ -10,10 +11,10 @@ "dev:stop": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-service.ts stop", "dev:server": "pnpm --filter @paperclipai/server dev", "dev:ui": "pnpm --filter @paperclipai/ui dev", - "build": "pnpm -r build", - "typecheck": "pnpm -r typecheck", - "test": "vitest", - "test:run": "vitest run", + "build": "pnpm run preflight:workspace-links && pnpm -r build", + "typecheck": "pnpm run preflight:workspace-links && pnpm -r typecheck", + "test": "pnpm run preflight:workspace-links && vitest", + "test:run": "pnpm run preflight:workspace-links && vitest run", "db:generate": "pnpm --filter @paperclipai/db generate", "db:migrate": "pnpm --filter @paperclipai/db migrate", "secrets:migrate-inline-env": "tsx scripts/migrate-inline-env-secrets.ts", diff --git a/scripts/ensure-workspace-package-links.ts b/scripts/ensure-workspace-package-links.ts index 8ff86b71..d8be419c 100644 --- a/scripts/ensure-workspace-package-links.ts +++ b/scripts/ensure-workspace-package-links.ts @@ -44,6 +44,13 @@ function discoverWorkspacePackagePaths(rootDir: string): Map { } const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot); +const workspaceDirs = Array.from( + new Set( + Array.from(workspacePackagePaths.values()) + .map((packagePath) => path.relative(repoRoot, packagePath)) + .filter((workspaceDir) => workspaceDir.length > 0), + ), +).sort(); function findWorkspaceLinkMismatches(workspaceDir: string): WorkspaceLinkMismatch[] { const packageJson = readJsonFile(path.join(repoRoot, workspaceDir, "package.json")); @@ -100,6 +107,6 @@ async function ensureWorkspaceLinksCurrent(workspaceDir: string) { ); } -for (const workspaceDir of ["server", "ui"]) { +for (const workspaceDir of workspaceDirs) { await ensureWorkspaceLinksCurrent(workspaceDir); } diff --git a/scripts/kill-agent-browsers.sh b/scripts/kill-agent-browsers.sh new file mode 100755 index 00000000..c89fa96e --- /dev/null +++ b/scripts/kill-agent-browsers.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# Kill all "Google Chrome for Testing" processes (agent headless browsers). +# +# Usage: +# scripts/kill-agent-browsers.sh # kill all +# scripts/kill-agent-browsers.sh --dry # preview what would be killed +# + +set -euo pipefail + +DRY_RUN=false +if [[ "${1:-}" == "--dry" || "${1:-}" == "--dry-run" || "${1:-}" == "-n" ]]; then + DRY_RUN=true +fi + +pids=() +lines=() + +while IFS= read -r line; do + [[ -z "$line" ]] && continue + pid=$(echo "$line" | awk '{print $2}') + pids+=("$pid") + lines+=("$line") +done < <(ps aux | grep 'Google Chrome for Testing' | grep -v grep || true) + +if [[ ${#pids[@]} -eq 0 ]]; then + echo "No Google Chrome for Testing processes found." + exit 0 +fi + +echo "Found ${#pids[@]} Google Chrome for Testing process(es):" +echo "" + +for i in "${!pids[@]}"; do + line="${lines[$i]}" + pid=$(echo "$line" | awk '{print $2}') + start=$(echo "$line" | awk '{print $9}') + cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}') + cmd=$(echo "$cmd" | sed "s|$HOME/||g") + printf " PID %-7s started %-10s %s\n" "$pid" "$start" "$cmd" +done + +echo "" + +if [[ "$DRY_RUN" == true ]]; then + echo "Dry run — re-run without --dry to kill these processes." + exit 0 +fi + +echo "Sending SIGTERM..." +for pid in "${pids[@]}"; do + kill -TERM "$pid" 2>/dev/null && echo " signaled $pid" || echo " $pid already gone" +done + +sleep 2 + +for pid in "${pids[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + echo " $pid still alive, sending SIGKILL..." + kill -KILL "$pid" 2>/dev/null || true + fi +done + +echo "Done." From 372421ef0b43db24340c9b64623324a36d7259f0 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 10:36:31 -0500 Subject: [PATCH 15/74] Add generic issue-linked board approvals Co-Authored-By: Paperclip --- ui/src/pages/IssueDetail.tsx | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 16346bb5..48451738 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -742,6 +742,39 @@ export function IssueDetail() { }, }); + const approvalDecision = useMutation({ + mutationFn: async ({ approvalId, action }: { approvalId: string; action: "approve" | "reject" }) => { + if (action === "approve") { + return approvalsApi.approve(approvalId); + } + return approvalsApi.reject(approvalId); + }, + onMutate: ({ approvalId, action }) => { + setPendingApprovalAction({ approvalId, action }); + }, + onSuccess: (_approval, variables) => { + invalidateIssue(); + queryClient.invalidateQueries({ queryKey: queryKeys.approvals.detail(variables.approvalId) }); + if (resolvedCompanyId) { + queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(resolvedCompanyId) }); + } + pushToast({ + title: variables.action === "approve" ? "Approval approved" : "Approval rejected", + tone: "success", + }); + }, + onError: (err, variables) => { + pushToast({ + title: variables.action === "approve" ? "Approval failed" : "Rejection failed", + body: err instanceof Error ? err.message : "Unable to update approval", + tone: "error", + }); + }, + onSettled: () => { + setPendingApprovalAction(null); + }, + }); + const addComment = useMutation({ mutationFn: ({ body, reopen, interrupt }: { body: string; reopen?: boolean; interrupt?: boolean }) => issuesApi.addComment(issueId!, body, reopen, interrupt), @@ -1892,7 +1925,6 @@ export function IssueDetail() { )} - {/* Mobile properties drawer */} From 1de5fb9316d2ad2b33c23bd5a0022a502e54ae47 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 16:31:14 -0500 Subject: [PATCH 16/74] Support routine variables in titles --- packages/shared/src/routine-variables.test.ts | 11 +++++++++- packages/shared/src/routine-variables.ts | 21 ++++++++++++------- server/src/__tests__/routines-service.test.ts | 6 ++++-- server/src/services/routines.ts | 10 +++++---- ui/src/components/RoutineVariablesEditor.tsx | 8 ++++--- ui/src/pages/RoutineDetail.tsx | 1 + ui/src/pages/Routines.tsx | 1 + 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/routine-variables.test.ts b/packages/shared/src/routine-variables.test.ts index a3832d87..9169dbfa 100644 --- a/packages/shared/src/routine-variables.test.ts +++ b/packages/shared/src/routine-variables.test.ts @@ -12,9 +12,18 @@ describe("routine variable helpers", () => { ).toEqual(["repo", "priority"]); }); + it("deduplicates placeholder names across the routine title and description", () => { + expect( + extractRoutineVariableNames([ + "Triage {{repo}}", + "Review {{repo}} for {{priority}} bugs", + ]), + ).toEqual(["repo", "priority"]); + }); + it("preserves existing metadata when syncing variables from a template", () => { expect( - syncRoutineVariablesWithTemplate("Review {{repo}} and {{priority}}", [ + syncRoutineVariablesWithTemplate(["Triage {{repo}}", "Review {{repo}} and {{priority}}"], [ { name: "repo", label: "Repository", type: "text", defaultValue: "paperclip", required: true, options: [] }, ]), ).toEqual([ diff --git a/packages/shared/src/routine-variables.ts b/packages/shared/src/routine-variables.ts index 73df368d..3c12b51c 100644 --- a/packages/shared/src/routine-variables.ts +++ b/packages/shared/src/routine-variables.ts @@ -1,18 +1,25 @@ import type { RoutineVariable } from "./types/routine.js"; const ROUTINE_VARIABLE_MATCHER = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}\}/g; +type RoutineTemplateInput = string | null | undefined | Array; export function isValidRoutineVariableName(name: string): boolean { return /^[A-Za-z][A-Za-z0-9_]*$/.test(name); } -export function extractRoutineVariableNames(template: string | null | undefined): string[] { - if (!template) return []; +function normalizeRoutineTemplateInput(input: RoutineTemplateInput): string[] { + const templates = Array.isArray(input) ? input : [input]; + return templates.filter((template): template is string => typeof template === "string" && template.length > 0); +} + +export function extractRoutineVariableNames(template: RoutineTemplateInput): string[] { const found = new Set(); - for (const match of template.matchAll(ROUTINE_VARIABLE_MATCHER)) { - const name = match[1]; - if (name && !found.has(name)) { - found.add(name); + for (const source of normalizeRoutineTemplateInput(template)) { + for (const match of source.matchAll(ROUTINE_VARIABLE_MATCHER)) { + const name = match[1]; + if (name && !found.has(name)) { + found.add(name); + } } } return [...found]; @@ -30,7 +37,7 @@ function defaultRoutineVariable(name: string): RoutineVariable { } export function syncRoutineVariablesWithTemplate( - template: string | null | undefined, + template: RoutineTemplateInput, existing: RoutineVariable[] | null | undefined, ): RoutineVariable[] { const names = extractRoutineVariableNames(template); diff --git a/server/src/__tests__/routines-service.test.ts b/server/src/__tests__/routines-service.test.ts index 5363fa83..13ce62e0 100644 --- a/server/src/__tests__/routines-service.test.ts +++ b/server/src/__tests__/routines-service.test.ts @@ -332,7 +332,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { projectId, goalId: null, parentIssueId: null, - title: "repo triage", + title: "repo triage for {{repo}}", description: "Review {{repo}} for {{priority}} bugs", assigneeAgentId: agentId, priority: "medium", @@ -346,6 +346,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { }, {}, ); + expect(variableRoutine.variables.map((variable) => variable.name)).toEqual(["repo", "priority"]); const run = await svc.runRoutine(variableRoutine.id, { source: "manual", @@ -353,7 +354,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { }); const storedIssue = await db - .select({ description: issues.description }) + .select({ title: issues.title, description: issues.description }) .from(issues) .where(eq(issues.id, run.linkedIssueId!)) .then((rows) => rows[0] ?? null); @@ -363,6 +364,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { .where(eq(routineRuns.id, run.id)) .then((rows) => rows[0] ?? null); + expect(storedIssue?.title).toBe("repo triage for paperclip"); expect(storedIssue?.description).toBe("Review paperclip for high bugs"); expect(storedRun?.triggerPayload).toEqual({ variables: { diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index 86cc69cb..8cd5ebb7 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -675,6 +675,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup executionWorkspaceSettings?: Record | null; }) { const resolvedVariables = resolveRoutineVariableValues(input.routine.variables ?? [], input); + const title = interpolateRoutineTemplate(input.routine.title, resolvedVariables) ?? input.routine.title; const description = interpolateRoutineTemplate(input.routine.description, resolvedVariables); const triggerPayload = mergeRoutineRunPayload(input.payload, resolvedVariables); const run = await db.transaction(async (tx) => { @@ -748,7 +749,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup projectId: input.routine.projectId, goalId: input.routine.goalId, parentId: input.routine.parentIssueId, - title: input.routine.title, + title, description, status: "todo", priority: input.routine.priority, @@ -996,7 +997,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup if (input.goalId) await assertGoal(companyId, input.goalId); if (input.parentIssueId) await assertParentIssue(companyId, input.parentIssueId); const variables = syncRoutineVariablesWithTemplate( - input.description, + [input.title, input.description], sanitizeRoutineVariableInputs(input.variables), ); assertRoutineVariableDefinitions(variables); @@ -1029,9 +1030,10 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup if (!existing) return null; const nextProjectId = patch.projectId ?? existing.projectId; const nextAssigneeAgentId = patch.assigneeAgentId ?? existing.assigneeAgentId; + const nextTitle = patch.title ?? existing.title; const nextDescription = patch.description === undefined ? existing.description : patch.description; const nextVariables = syncRoutineVariablesWithTemplate( - nextDescription, + [nextTitle, nextDescription], patch.variables === undefined ? existing.variables : sanitizeRoutineVariableInputs(patch.variables), ); if (patch.projectId) await assertProject(existing.companyId, nextProjectId); @@ -1060,7 +1062,7 @@ export function routineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeup projectId: nextProjectId, goalId: patch.goalId === undefined ? existing.goalId : patch.goalId, parentIssueId: patch.parentIssueId === undefined ? existing.parentIssueId : patch.parentIssueId, - title: patch.title ?? existing.title, + title: nextTitle, description: nextDescription, assigneeAgentId: nextAssigneeAgentId, priority: patch.priority ?? existing.priority, diff --git a/ui/src/components/RoutineVariablesEditor.tsx b/ui/src/components/RoutineVariablesEditor.tsx index 565589b8..f6ecd322 100644 --- a/ui/src/components/RoutineVariablesEditor.tsx +++ b/ui/src/components/RoutineVariablesEditor.tsx @@ -36,18 +36,20 @@ function updateVariableList( } export function RoutineVariablesEditor({ + title, description, value, onChange, }: { + title: string; description: string; value: RoutineVariable[]; onChange: (value: RoutineVariable[]) => void; }) { const [open, setOpen] = useState(true); const syncedVariables = useMemo( - () => syncRoutineVariablesWithTemplate(description, value), - [description, value], + () => syncRoutineVariablesWithTemplate([title, description], value), + [description, title, value], ); const syncedSignature = serializeVariables(syncedVariables); const currentSignature = serializeVariables(value); @@ -68,7 +70,7 @@ export function RoutineVariablesEditor({

Variables

- Detected from `{"{{name}}"}` placeholders in the routine instructions. + Detected from `{"{{name}}"}` placeholders in the routine title and instructions.

{open ? : } diff --git a/ui/src/pages/RoutineDetail.tsx b/ui/src/pages/RoutineDetail.tsx index aeaba763..18e4fa6f 100644 --- a/ui/src/pages/RoutineDetail.tsx +++ b/ui/src/pages/RoutineDetail.tsx @@ -860,6 +860,7 @@ export function RoutineDetail() { /> setEditDraft((current) => ({ ...current, variables }))} diff --git a/ui/src/pages/Routines.tsx b/ui/src/pages/Routines.tsx index 85e32796..3f9606f0 100644 --- a/ui/src/pages/Routines.tsx +++ b/ui/src/pages/Routines.tsx @@ -806,6 +806,7 @@ export function Routines() {
setDraft((current) => ({ ...current, variables }))} From 5640d29ab0b42851b17d54bd77725ef52c1340bc Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 18:26:34 -0500 Subject: [PATCH 17/74] Persist non-issue inbox dismissals --- .../src/migrations/0053_sharp_wild_child.sql | 18 + .../db/src/migrations/meta/0053_snapshot.json | 12979 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 9 +- packages/db/src/schema/inbox_dismissals.ts | 24 + packages/db/src/schema/index.ts | 1 + packages/shared/src/index.ts | 1 + packages/shared/src/types/inbox-dismissal.ts | 9 + packages/shared/src/types/index.ts | 1 + server/src/__tests__/inbox-dismissals.test.ts | 212 + server/src/app.ts | 2 + server/src/routes/inbox-dismissals.ts | 69 + server/src/routes/index.ts | 1 + server/src/routes/sidebar-badges.ts | 37 +- server/src/services/inbox-dismissals.ts | 41 + server/src/services/index.ts | 1 + server/src/services/sidebar-badges.ts | 43 +- ui/src/api/inboxDismissals.ts | 8 + ui/src/api/index.ts | 1 + ui/src/hooks/useInboxBadge.ts | 81 +- ui/src/lib/inbox.test.ts | 39 +- ui/src/lib/inbox.ts | 50 +- ui/src/lib/queryKeys.ts | 1 + ui/src/pages/Inbox.tsx | 49 +- 23 files changed, 13623 insertions(+), 54 deletions(-) create mode 100644 packages/db/src/migrations/0053_sharp_wild_child.sql create mode 100644 packages/db/src/migrations/meta/0053_snapshot.json create mode 100644 packages/db/src/schema/inbox_dismissals.ts create mode 100644 packages/shared/src/types/inbox-dismissal.ts create mode 100644 server/src/__tests__/inbox-dismissals.test.ts create mode 100644 server/src/routes/inbox-dismissals.ts create mode 100644 server/src/services/inbox-dismissals.ts create mode 100644 ui/src/api/inboxDismissals.ts diff --git a/packages/db/src/migrations/0053_sharp_wild_child.sql b/packages/db/src/migrations/0053_sharp_wild_child.sql new file mode 100644 index 00000000..85fdbd9c --- /dev/null +++ b/packages/db/src/migrations/0053_sharp_wild_child.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS "inbox_dismissals" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "user_id" text NOT NULL, + "item_key" text NOT NULL, + "dismissed_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "inbox_dismissals" ADD CONSTRAINT "inbox_dismissals_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "inbox_dismissals_company_user_idx" ON "inbox_dismissals" USING btree ("company_id","user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "inbox_dismissals_company_item_idx" ON "inbox_dismissals" USING btree ("company_id","item_key");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "inbox_dismissals_company_user_item_idx" ON "inbox_dismissals" USING btree ("company_id","user_id","item_key"); diff --git a/packages/db/src/migrations/meta/0053_snapshot.json b/packages/db/src/migrations/meta/0053_snapshot.json new file mode 100644 index 00000000..c72b4c3f --- /dev/null +++ b/packages/db/src/migrations/meta/0053_snapshot.json @@ -0,0 +1,12979 @@ +{ + "id": "eb8aba7f-540a-4ac6-9f58-1ed449707201", + "prevId": "90165bd7-c2f6-45a5-83ea-ba357b060428", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_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": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_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": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index bfe29b36..5fa8cfce 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -372,6 +372,13 @@ "when": 1775571715162, "tag": "0052_mushy_trauma", "breakpoints": true + }, + { + "idx": 53, + "version": "7", + "when": 1775604018515, + "tag": "0053_sharp_wild_child", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/schema/inbox_dismissals.ts b/packages/db/src/schema/inbox_dismissals.ts new file mode 100644 index 00000000..22996a47 --- /dev/null +++ b/packages/db/src/schema/inbox_dismissals.ts @@ -0,0 +1,24 @@ +import { pgTable, uuid, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; + +export const inboxDismissals = pgTable( + "inbox_dismissals", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id), + userId: text("user_id").notNull(), + itemKey: text("item_key").notNull(), + dismissedAt: timestamp("dismissed_at", { withTimezone: true }).notNull().defaultNow(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyUserIdx: index("inbox_dismissals_company_user_idx").on(table.companyId, table.userId), + companyItemIdx: index("inbox_dismissals_company_item_idx").on(table.companyId, table.itemKey), + companyUserItemUnique: uniqueIndex("inbox_dismissals_company_user_item_idx").on( + table.companyId, + table.userId, + table.itemKey, + ), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 505bc2e5..1f86ca67 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -34,6 +34,7 @@ export { issueApprovals } from "./issue_approvals.js"; export { issueComments } from "./issue_comments.js"; export { issueExecutionDecisions } from "./issue_execution_decisions.js"; export { issueInboxArchives } from "./issue_inbox_archives.js"; +export { inboxDismissals } from "./inbox_dismissals.js"; export { feedbackVotes } from "./feedback_votes.js"; export { feedbackExports } from "./feedback_exports.js"; export { issueReadStates } from "./issue_read_states.js"; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a5a01b36..78678509 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -288,6 +288,7 @@ export type { DashboardSummary, ActivityEvent, SidebarBadges, + InboxDismissal, CompanyMembership, PrincipalPermissionGrant, Invite, diff --git a/packages/shared/src/types/inbox-dismissal.ts b/packages/shared/src/types/inbox-dismissal.ts new file mode 100644 index 00000000..0c76ecc8 --- /dev/null +++ b/packages/shared/src/types/inbox-dismissal.ts @@ -0,0 +1,9 @@ +export interface InboxDismissal { + id: string; + companyId: string; + userId: string; + itemKey: string; + dismissedAt: Date; + createdAt: Date; + updatedAt: Date; +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 17570116..888740d3 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -164,6 +164,7 @@ export type { LiveEvent } from "./live.js"; export type { DashboardSummary } from "./dashboard.js"; export type { ActivityEvent } from "./activity.js"; export type { SidebarBadges } from "./sidebar-badges.js"; +export type { InboxDismissal } from "./inbox-dismissal.js"; export type { CompanyMembership, PrincipalPermissionGrant, diff --git a/server/src/__tests__/inbox-dismissals.test.ts b/server/src/__tests__/inbox-dismissals.test.ts new file mode 100644 index 00000000..c6360a21 --- /dev/null +++ b/server/src/__tests__/inbox-dismissals.test.ts @@ -0,0 +1,212 @@ +import { randomUUID } from "node:crypto"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + agents, + approvals, + companies, + createDb, + heartbeatRuns, + inboxDismissals, + invites, + joinRequests, +} from "@paperclipai/db"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { inboxDismissalService } from "../services/inbox-dismissals.ts"; +import { sidebarBadgeService } from "../services/sidebar-badges.ts"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping embedded Postgres inbox dismissal tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +describeEmbeddedPostgres("inbox dismissals", () => { + let db!: ReturnType; + let dismissalsSvc!: ReturnType; + let badgesSvc!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-inbox-dismissals-"); + db = createDb(tempDb.connectionString); + dismissalsSvc = inboxDismissalService(db); + badgesSvc = sidebarBadgeService(db); + }, 20_000); + + afterEach(async () => { + await db.delete(inboxDismissals); + await db.delete(joinRequests); + await db.delete(invites); + await db.delete(heartbeatRuns); + await db.delete(approvals); + await db.delete(agents); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + it("upserts a single dismissal record per user and inbox item key", async () => { + const companyId = randomUUID(); + const userId = "board-user"; + const firstDismissedAt = new Date("2026-03-11T01:00:00.000Z"); + const secondDismissedAt = new Date("2026-03-11T02:00:00.000Z"); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: "PAP", + requireBoardApprovalForNewAgents: false, + }); + + await dismissalsSvc.dismiss(companyId, userId, "approval:approval-1", firstDismissedAt); + await dismissalsSvc.dismiss(companyId, userId, "approval:approval-1", secondDismissedAt); + + const dismissals = await dismissalsSvc.list(companyId, userId); + + expect(dismissals).toHaveLength(1); + expect(dismissals[0]?.itemKey).toBe("approval:approval-1"); + expect(new Date(dismissals[0]?.dismissedAt ?? 0).toISOString()).toBe(secondDismissedAt.toISOString()); + }); + + it("honors dismissal timestamps and resurfaces approvals with newer activity", async () => { + const companyId = randomUUID(); + const userId = "board-user"; + const primaryAgentId = randomUUID(); + const secondaryAgentId = randomUUID(); + const hiddenApprovalId = randomUUID(); + const resurfacedApprovalId = randomUUID(); + const inviteId = randomUUID(); + const hiddenJoinRequestId = randomUUID(); + const hiddenRunId = randomUUID(); + const visibleRunId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: "PAP", + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values([ + { + id: primaryAgentId, + companyId, + name: "Primary", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }, + { + id: secondaryAgentId, + companyId, + name: "Secondary", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }, + ]); + + await db.insert(approvals).values([ + { + id: hiddenApprovalId, + companyId, + type: "hire_agent", + status: "pending", + payload: {}, + updatedAt: new Date("2026-03-11T01:00:00.000Z"), + }, + { + id: resurfacedApprovalId, + companyId, + type: "hire_agent", + status: "revision_requested", + payload: {}, + updatedAt: new Date("2026-03-11T03:00:00.000Z"), + }, + ]); + + await db.insert(invites).values({ + id: inviteId, + companyId, + inviteType: "company_join", + tokenHash: "hash-1", + allowedJoinTypes: "both", + expiresAt: new Date("2026-03-12T00:00:00.000Z"), + }); + + await db.insert(joinRequests).values({ + id: hiddenJoinRequestId, + inviteId, + companyId, + requestType: "human", + status: "pending_approval", + requestIp: "127.0.0.1", + createdAt: new Date("2026-03-11T01:00:00.000Z"), + updatedAt: new Date("2026-03-11T01:00:00.000Z"), + }); + + await db.insert(heartbeatRuns).values([ + { + id: hiddenRunId, + companyId, + agentId: primaryAgentId, + invocationSource: "assignment", + status: "failed", + createdAt: new Date("2026-03-11T01:00:00.000Z"), + updatedAt: new Date("2026-03-11T01:00:00.000Z"), + }, + { + id: visibleRunId, + companyId, + agentId: secondaryAgentId, + invocationSource: "assignment", + status: "timed_out", + createdAt: new Date("2026-03-11T04:00:00.000Z"), + updatedAt: new Date("2026-03-11T04:00:00.000Z"), + }, + ]); + + await dismissalsSvc.dismiss(companyId, userId, `approval:${hiddenApprovalId}`, new Date("2026-03-11T02:00:00.000Z")); + await dismissalsSvc.dismiss(companyId, userId, `approval:${resurfacedApprovalId}`, new Date("2026-03-11T02:00:00.000Z")); + await dismissalsSvc.dismiss(companyId, userId, `join:${hiddenJoinRequestId}`, new Date("2026-03-11T02:00:00.000Z")); + await dismissalsSvc.dismiss(companyId, userId, `run:${hiddenRunId}`, new Date("2026-03-11T02:00:00.000Z")); + + const dismissedAtByKey = new Map( + (await dismissalsSvc.list(companyId, userId)).map((dismissal) => [ + dismissal.itemKey, + new Date(dismissal.dismissedAt).getTime(), + ]), + ); + + const badges = await badgesSvc.get(companyId, { + dismissals: dismissedAtByKey, + joinRequests: [{ + id: hiddenJoinRequestId, + createdAt: new Date("2026-03-11T01:00:00.000Z"), + updatedAt: new Date("2026-03-11T01:00:00.000Z"), + }], + unreadTouchedIssues: 1, + }); + + expect(badges).toEqual({ + inbox: 3, + approvals: 1, + failedRuns: 1, + joinRequests: 0, + }); + }); +}); diff --git a/server/src/app.ts b/server/src/app.ts index eaec70d4..dd89cd7f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -24,6 +24,7 @@ import { costRoutes } from "./routes/costs.js"; import { activityRoutes } from "./routes/activity.js"; import { dashboardRoutes } from "./routes/dashboard.js"; import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js"; +import { inboxDismissalRoutes } from "./routes/inbox-dismissals.js"; import { instanceSettingsRoutes } from "./routes/instance-settings.js"; import { llmRoutes } from "./routes/llms.js"; import { assetRoutes } from "./routes/assets.js"; @@ -166,6 +167,7 @@ export async function createApp( api.use(activityRoutes(db)); api.use(dashboardRoutes(db)); api.use(sidebarBadgeRoutes(db)); + api.use(inboxDismissalRoutes(db)); api.use(instanceSettingsRoutes(db)); const hostServicesDisposers = new Map void>(); const workerManager = createPluginWorkerManager(); diff --git a/server/src/routes/inbox-dismissals.ts b/server/src/routes/inbox-dismissals.ts new file mode 100644 index 00000000..1b51633e --- /dev/null +++ b/server/src/routes/inbox-dismissals.ts @@ -0,0 +1,69 @@ +import { Router } from "express"; +import { z } from "zod"; +import type { Db } from "@paperclipai/db"; +import { validate } from "../middleware/validate.js"; +import { assertCompanyAccess, getActorInfo } from "./authz.js"; +import { inboxDismissalService, logActivity } from "../services/index.js"; + +const inboxDismissalSchema = z.object({ + itemKey: z.string().trim().min(1).regex(/^(approval|join|run):.+$/, "Unsupported inbox item key"), +}); + +export function inboxDismissalRoutes(db: Db) { + const router = Router(); + const svc = inboxDismissalService(db); + + router.get("/companies/:companyId/inbox-dismissals", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Board authentication required" }); + return; + } + if (!req.actor.userId) { + res.status(403).json({ error: "Board user context required" }); + return; + } + const dismissals = await svc.list(companyId, req.actor.userId); + res.json(dismissals); + }); + + router.post( + "/companies/:companyId/inbox-dismissals", + validate(inboxDismissalSchema), + async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Board authentication required" }); + return; + } + if (!req.actor.userId) { + res.status(403).json({ error: "Board user context required" }); + return; + } + + const dismissal = await svc.dismiss(companyId, req.actor.userId, req.body.itemKey, new Date()); + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "inbox.dismissed", + entityType: "company", + entityId: companyId, + details: { + userId: req.actor.userId, + itemKey: dismissal.itemKey, + dismissedAt: dismissal.dismissedAt, + }, + }); + + res.status(201).json(dismissal); + }, + ); + + return router; +} diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index dd9c0b54..bca52940 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -12,6 +12,7 @@ export { costRoutes } from "./costs.js"; export { activityRoutes } from "./activity.js"; export { dashboardRoutes } from "./dashboard.js"; export { sidebarBadgeRoutes } from "./sidebar-badges.js"; +export { inboxDismissalRoutes } from "./inbox-dismissals.js"; export { llmRoutes } from "./llms.js"; export { accessRoutes } from "./access.js"; export { instanceSettingsRoutes } from "./instance-settings.js"; diff --git a/server/src/routes/sidebar-badges.ts b/server/src/routes/sidebar-badges.ts index 03cb4cb0..505b7704 100644 --- a/server/src/routes/sidebar-badges.ts +++ b/server/src/routes/sidebar-badges.ts @@ -1,12 +1,20 @@ import { Router } from "express"; import type { Db } from "@paperclipai/db"; -import { and, eq, sql } from "drizzle-orm"; -import { joinRequests } from "@paperclipai/db"; +import { and, eq } from "drizzle-orm"; +import { inboxDismissals, joinRequests } from "@paperclipai/db"; import { sidebarBadgeService } from "../services/sidebar-badges.js"; import { accessService } from "../services/access.js"; import { dashboardService } from "../services/dashboard.js"; import { assertCompanyAccess } from "./authz.js"; +function buildDismissedAtByKey( + dismissals: Array<{ itemKey: string; dismissedAt: Date | string }>, +): Map { + return new Map( + dismissals.map((dismissal) => [dismissal.itemKey, new Date(dismissal.dismissedAt).getTime()]), + ); +} + export function sidebarBadgeRoutes(db: Db) { const router = Router(); const svc = sidebarBadgeService(db); @@ -26,23 +34,36 @@ export function sidebarBadgeRoutes(db: Db) { canApproveJoins = await access.hasPermission(companyId, "agent", req.actor.agentId, "joins:approve"); } - const joinRequestCount = canApproveJoins + const visibleJoinRequests = canApproveJoins ? await db - .select({ count: sql`count(*)` }) + .select({ + id: joinRequests.id, + updatedAt: joinRequests.updatedAt, + createdAt: joinRequests.createdAt, + }) .from(joinRequests) .where(and(eq(joinRequests.companyId, companyId), eq(joinRequests.status, "pending_approval"))) - .then((rows) => Number(rows[0]?.count ?? 0)) - : 0; + : []; + + const dismissedAtByKey = + req.actor.type === "board" && req.actor.userId + ? await db + .select({ itemKey: inboxDismissals.itemKey, dismissedAt: inboxDismissals.dismissedAt }) + .from(inboxDismissals) + .where(and(eq(inboxDismissals.companyId, companyId), eq(inboxDismissals.userId, req.actor.userId))) + .then(buildDismissedAtByKey) + : new Map(); const badges = await svc.get(companyId, { - joinRequests: joinRequestCount, + dismissals: dismissedAtByKey, + joinRequests: visibleJoinRequests, }); const summary = await dashboard.summary(companyId); const hasFailedRuns = badges.failedRuns > 0; const alertsCount = (summary.agents.error > 0 && !hasFailedRuns ? 1 : 0) + (summary.costs.monthBudgetCents > 0 && summary.costs.monthUtilizationPercent >= 80 ? 1 : 0); - badges.inbox = badges.failedRuns + alertsCount + joinRequestCount + badges.approvals; + badges.inbox = badges.failedRuns + alertsCount + badges.joinRequests + badges.approvals; res.json(badges); }); diff --git a/server/src/services/inbox-dismissals.ts b/server/src/services/inbox-dismissals.ts new file mode 100644 index 00000000..68032c69 --- /dev/null +++ b/server/src/services/inbox-dismissals.ts @@ -0,0 +1,41 @@ +import { and, desc, eq } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { inboxDismissals } from "@paperclipai/db"; + +export function inboxDismissalService(db: Db) { + return { + list: async (companyId: string, userId: string) => + db + .select() + .from(inboxDismissals) + .where(and(eq(inboxDismissals.companyId, companyId), eq(inboxDismissals.userId, userId))) + .orderBy(desc(inboxDismissals.updatedAt)), + + dismiss: async ( + companyId: string, + userId: string, + itemKey: string, + dismissedAt: Date = new Date(), + ) => { + const now = new Date(); + const [row] = await db + .insert(inboxDismissals) + .values({ + companyId, + userId, + itemKey, + dismissedAt, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [inboxDismissals.companyId, inboxDismissals.userId, inboxDismissals.itemKey], + set: { + dismissedAt, + updatedAt: now, + }, + }) + .returning(); + return row; + }, + }; +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 775756e0..909fb05e 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -19,6 +19,7 @@ export { financeService } from "./finance.js"; export { heartbeatService } from "./heartbeat.js"; export { dashboardService } from "./dashboard.js"; export { sidebarBadgeService } from "./sidebar-badges.js"; +export { inboxDismissalService } from "./inbox-dismissals.js"; export { accessService } from "./access.js"; export { boardAuthService } from "./board-auth.js"; export { instanceSettingsService } from "./instance-settings.js"; diff --git a/server/src/services/sidebar-badges.ts b/server/src/services/sidebar-badges.ts index cd39bf57..5849f2ac 100644 --- a/server/src/services/sidebar-badges.ts +++ b/server/src/services/sidebar-badges.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, inArray, not, sql } from "drizzle-orm"; +import { and, desc, eq, inArray, not } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, approvals, heartbeatRuns } from "@paperclipai/db"; import type { SidebarBadges } from "@paperclipai/shared"; @@ -6,14 +6,34 @@ import type { SidebarBadges } from "@paperclipai/shared"; const ACTIONABLE_APPROVAL_STATUSES = ["pending", "revision_requested"]; const FAILED_HEARTBEAT_STATUSES = ["failed", "timed_out"]; +function normalizeTimestamp(value: Date | string | null | undefined): number { + if (!value) return 0; + const timestamp = new Date(value).getTime(); + return Number.isFinite(timestamp) ? timestamp : 0; +} + +function isDismissed( + dismissedAtByKey: ReadonlyMap, + itemKey: string, + activityAt: Date | string | null | undefined, +) { + const dismissedAt = dismissedAtByKey.get(itemKey); + if (dismissedAt == null) return false; + return dismissedAt >= normalizeTimestamp(activityAt); +} + export function sidebarBadgeService(db: Db) { return { get: async ( companyId: string, - extra?: { joinRequests?: number; unreadTouchedIssues?: number }, + extra?: { + dismissals?: ReadonlyMap; + joinRequests?: Array<{ id: string; updatedAt: Date | string | null; createdAt: Date | string }>; + unreadTouchedIssues?: number; + }, ): Promise => { const actionableApprovals = await db - .select({ count: sql`count(*)` }) + .select({ id: approvals.id, updatedAt: approvals.updatedAt }) .from(approvals) .where( and( @@ -21,11 +41,15 @@ export function sidebarBadgeService(db: Db) { inArray(approvals.status, ACTIONABLE_APPROVAL_STATUSES), ), ) - .then((rows) => Number(rows[0]?.count ?? 0)); + .then((rows) => + rows.filter((row) => !isDismissed(extra?.dismissals ?? new Map(), `approval:${row.id}`, row.updatedAt)).length + ); const latestRunByAgent = await db .selectDistinctOn([heartbeatRuns.agentId], { + id: heartbeatRuns.id, runStatus: heartbeatRuns.status, + createdAt: heartbeatRuns.createdAt, }) .from(heartbeatRuns) .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id)) @@ -39,10 +63,17 @@ export function sidebarBadgeService(db: Db) { .orderBy(heartbeatRuns.agentId, desc(heartbeatRuns.createdAt)); const failedRuns = latestRunByAgent.filter((row) => - FAILED_HEARTBEAT_STATUSES.includes(row.runStatus), + FAILED_HEARTBEAT_STATUSES.includes(row.runStatus) + && !isDismissed(extra?.dismissals ?? new Map(), `run:${row.id}`, row.createdAt), ).length; - const joinRequests = extra?.joinRequests ?? 0; + const joinRequests = (extra?.joinRequests ?? []).filter((row) => + !isDismissed( + extra?.dismissals ?? new Map(), + `join:${row.id}`, + row.updatedAt ?? row.createdAt, + ) + ).length; const unreadTouchedIssues = extra?.unreadTouchedIssues ?? 0; return { inbox: actionableApprovals + failedRuns + joinRequests + unreadTouchedIssues, diff --git a/ui/src/api/inboxDismissals.ts b/ui/src/api/inboxDismissals.ts new file mode 100644 index 00000000..f80d3aef --- /dev/null +++ b/ui/src/api/inboxDismissals.ts @@ -0,0 +1,8 @@ +import type { InboxDismissal } from "@paperclipai/shared"; +import { api } from "./client"; + +export const inboxDismissalsApi = { + list: (companyId: string) => api.get(`/companies/${companyId}/inbox-dismissals`), + dismiss: (companyId: string, itemKey: string) => + api.post(`/companies/${companyId}/inbox-dismissals`, { itemKey }), +}; diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 84b58cda..13c72f1f 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -15,4 +15,5 @@ export { dashboardApi } from "./dashboard"; export { heartbeatsApi } from "./heartbeats"; export { instanceSettingsApi } from "./instanceSettings"; export { sidebarBadgesApi } from "./sidebarBadges"; +export { inboxDismissalsApi } from "./inboxDismissals"; export { companySkillsApi } from "./companySkills"; diff --git a/ui/src/hooks/useInboxBadge.ts b/ui/src/hooks/useInboxBadge.ts index 6b7daa2b..627398d6 100644 --- a/ui/src/hooks/useInboxBadge.ts +++ b/ui/src/hooks/useInboxBadge.ts @@ -1,17 +1,19 @@ import { useEffect, useMemo, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { accessApi } from "../api/access"; import { ApiError } from "../api/client"; +import { inboxDismissalsApi } from "../api/inboxDismissals"; import { approvalsApi } from "../api/approvals"; import { dashboardApi } from "../api/dashboard"; import { heartbeatsApi } from "../api/heartbeats"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { + buildInboxDismissedAtByKey, computeInboxBadgeData, getRecentTouchedIssues, - loadDismissedInboxItems, - saveDismissedInboxItems, + loadDismissedInboxAlerts, + saveDismissedInboxAlerts, loadReadInboxItems, saveReadInboxItems, READ_ITEMS_KEY, @@ -19,13 +21,13 @@ import { const INBOX_ISSUE_STATUSES = "backlog,todo,in_progress,in_review,blocked,done"; -export function useDismissedInboxItems() { - const [dismissed, setDismissed] = useState>(loadDismissedInboxItems); +export function useDismissedInboxAlerts() { + const [dismissed, setDismissed] = useState>(loadDismissedInboxAlerts); useEffect(() => { const handleStorage = (event: StorageEvent) => { if (event.key !== "paperclip:inbox:dismissed") return; - setDismissed(loadDismissedInboxItems()); + setDismissed(loadDismissedInboxAlerts()); }; window.addEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage); @@ -35,7 +37,7 @@ export function useDismissedInboxItems() { setDismissed((prev) => { const next = new Set(prev); next.add(id); - saveDismissedInboxItems(next); + saveDismissedInboxAlerts(next); return next; }); }; @@ -43,6 +45,63 @@ export function useDismissedInboxItems() { return { dismissed, dismiss }; } +export function useInboxDismissals(companyId: string | null | undefined) { + const queryClient = useQueryClient(); + const queryKey = companyId + ? queryKeys.inboxDismissals(companyId) + : ["inbox-dismissals", "__disabled__"] as const; + + const { data: dismissals = [] } = useQuery({ + queryKey, + queryFn: () => inboxDismissalsApi.list(companyId!), + enabled: !!companyId, + }); + + const dismissMutation = useMutation({ + mutationFn: ({ itemKey }: { itemKey: string }) => inboxDismissalsApi.dismiss(companyId!, itemKey), + onMutate: async ({ itemKey }) => { + if (!companyId) return { previous: [] as typeof dismissals }; + await queryClient.cancelQueries({ queryKey }); + const previous = queryClient.getQueryData(queryKey) ?? []; + const now = new Date(); + queryClient.setQueryData(queryKey, [ + { + id: `optimistic:${itemKey}`, + companyId, + userId: "me", + itemKey, + dismissedAt: now, + createdAt: now, + updatedAt: now, + }, + ...previous.filter((dismissal) => dismissal.itemKey !== itemKey), + ]); + return { previous }; + }, + onError: (_error, _variables, context) => { + if (!context) return; + queryClient.setQueryData(queryKey, context.previous); + }, + onSettled: () => { + if (!companyId) return; + queryClient.invalidateQueries({ queryKey }); + queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) }); + }, + }); + + const dismissedAtByKey = useMemo( + () => buildInboxDismissedAtByKey(dismissals), + [dismissals], + ); + + return { + dismissals, + dismissedAtByKey, + dismiss: (itemKey: string) => dismissMutation.mutate({ itemKey }), + isPending: dismissMutation.isPending, + }; +} + export function useReadInboxItems() { const [readItems, setReadItems] = useState>(loadReadInboxItems); @@ -77,7 +136,8 @@ export function useReadInboxItems() { } export function useInboxBadge(companyId: string | null | undefined) { - const { dismissed } = useDismissedInboxItems(); + const { dismissed: dismissedAlerts } = useDismissedInboxAlerts(); + const { dismissedAtByKey } = useInboxDismissals(companyId); const { data: approvals = [] } = useQuery({ queryKey: queryKeys.approvals.list(companyId!), @@ -134,8 +194,9 @@ export function useInboxBadge(companyId: string | null | undefined) { dashboard, heartbeatRuns, mineIssues, - dismissed, + dismissedAlerts, + dismissedAtByKey, }), - [approvals, joinRequests, dashboard, heartbeatRuns, mineIssues, dismissed], + [approvals, joinRequests, dashboard, heartbeatRuns, mineIssues, dismissedAlerts, dismissedAtByKey], ); } diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 8bcf0738..5c8e1d57 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -12,6 +12,7 @@ import type { } from "@paperclipai/shared"; import { DEFAULT_INBOX_ISSUE_COLUMNS, + buildInboxDismissedAtByKey, computeInboxBadgeData, getAvailableInboxIssueColumns, getApprovalsForTab, @@ -19,6 +20,7 @@ import { getInboxKeyboardSelectionIndex, getRecentTouchedIssues, getUnreadTouchedIssues, + isInboxEntityDismissed, isMineInboxTab, loadInboxIssueColumns, loadLastInboxTab, @@ -286,7 +288,8 @@ describe("inbox helpers", () => { makeRun("run-other-agent", "failed", "2026-03-11T02:00:00.000Z", "agent-2"), ], mineIssues: [makeIssue("1", true)], - dismissed: new Set(), + dismissedAlerts: new Set(), + dismissedAtByKey: new Map(), }); expect(result).toEqual({ @@ -306,7 +309,8 @@ describe("inbox helpers", () => { dashboard, heartbeatRuns: [makeRun("run-1", "failed", "2026-03-11T00:00:00.000Z")], mineIssues: [], - dismissed: new Set(["run:run-1", "alert:budget", "alert:agent-errors"]), + dismissedAlerts: new Set(["alert:budget", "alert:agent-errors"]), + dismissedAtByKey: new Map([["run:run-1", new Date("2026-03-11T00:00:00.000Z").getTime()]]), }); expect(result).toEqual({ @@ -326,7 +330,7 @@ describe("inbox helpers", () => { dashboard, heartbeatRuns: [], mineIssues: [makeIssue("1", false), makeIssue("2", false), makeIssue("3", true)], - dismissed: new Set(), + dismissedAtByKey: new Map(), }); expect(result.mineIssues).toBe(1); @@ -334,6 +338,35 @@ describe("inbox helpers", () => { expect(result.inbox).toBe(3); }); + it("resurfaces non-issue items when they change after dismissal", () => { + const dismissedAtByKey = buildInboxDismissedAtByKey([ + { + id: "dismissal-1", + companyId: "company-1", + userId: "user-1", + itemKey: "approval:approval-1", + dismissedAt: new Date("2026-03-11T01:00:00.000Z"), + createdAt: new Date("2026-03-11T01:00:00.000Z"), + updatedAt: new Date("2026-03-11T01:00:00.000Z"), + }, + ]); + + expect( + isInboxEntityDismissed( + dismissedAtByKey, + "approval:approval-1", + new Date("2026-03-11T00:30:00.000Z"), + ), + ).toBe(true); + expect( + isInboxEntityDismissed( + dismissedAtByKey, + "approval:approval-1", + new Date("2026-03-11T01:30:00.000Z"), + ), + ).toBe(false); + }); + it("keeps read issues in the touched list but excludes them from unread counts", () => { const issues = [makeIssue("1", true), makeIssue("2", false)]; diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index a69bd6c7..c100bd52 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -1,4 +1,11 @@ -import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; +import type { + Approval, + DashboardSummary, + HeartbeatRun, + InboxDismissal, + Issue, + JoinRequest, +} from "@paperclipai/shared"; export const RECENT_ISSUES_LIMIT = 100; export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]); @@ -43,16 +50,19 @@ export interface InboxBadgeData { alerts: number; } -export function loadDismissedInboxItems(): Set { +export function loadDismissedInboxAlerts(): Set { try { const raw = localStorage.getItem(DISMISSED_KEY); - return raw ? new Set(JSON.parse(raw)) : new Set(); + if (!raw) return new Set(); + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return new Set(); + return new Set(parsed.filter((value): value is string => typeof value === "string" && value.startsWith("alert:"))); } catch { return new Set(); } } -export function saveDismissedInboxItems(ids: Set) { +export function saveDismissedInboxAlerts(ids: Set) { try { localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids])); } catch { @@ -60,6 +70,22 @@ export function saveDismissedInboxItems(ids: Set) { } } +export function buildInboxDismissedAtByKey(dismissals: InboxDismissal[]): Map { + return new Map( + dismissals.map((dismissal) => [dismissal.itemKey, normalizeTimestamp(dismissal.dismissedAt)]), + ); +} + +export function isInboxEntityDismissed( + dismissedAtByKey: ReadonlyMap, + itemKey: string, + activityAt: string | Date | null | undefined, +): boolean { + const dismissedAt = dismissedAtByKey.get(itemKey); + if (dismissedAt == null) return false; + return dismissedAt >= normalizeTimestamp(activityAt); +} + export function loadReadInboxItems(): Set { try { const raw = localStorage.getItem(READ_ITEMS_KEY); @@ -342,25 +368,27 @@ export function computeInboxBadgeData({ dashboard, heartbeatRuns, mineIssues, - dismissed, + dismissedAlerts, + dismissedAtByKey, }: { approvals: Approval[]; joinRequests: JoinRequest[]; dashboard: DashboardSummary | undefined; heartbeatRuns: HeartbeatRun[]; mineIssues: Issue[]; - dismissed: Set; + dismissedAlerts: Set; + dismissedAtByKey: ReadonlyMap; }): InboxBadgeData { const actionableApprovals = approvals.filter( (approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status) && - !dismissed.has(`approval:${approval.id}`), + !isInboxEntityDismissed(dismissedAtByKey, `approval:${approval.id}`, approval.updatedAt), ).length; const failedRuns = getLatestFailedRunsByAgent(heartbeatRuns).filter( - (run) => !dismissed.has(`run:${run.id}`), + (run) => !isInboxEntityDismissed(dismissedAtByKey, `run:${run.id}`, run.createdAt), ).length; const visibleJoinRequests = joinRequests.filter( - (jr) => !dismissed.has(`join:${jr.id}`), + (jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt), ).length; const visibleMineIssues = mineIssues.filter((issue) => issue.isUnreadForMe).length; const agentErrorCount = dashboard?.agents.error ?? 0; @@ -369,11 +397,11 @@ export function computeInboxBadgeData({ const showAggregateAgentError = agentErrorCount > 0 && failedRuns === 0 && - !dismissed.has("alert:agent-errors"); + !dismissedAlerts.has("alert:agent-errors"); const showBudgetAlert = monthBudgetCents > 0 && monthUtilizationPercent >= 80 && - !dismissed.has("alert:budget"); + !dismissedAlerts.has("alert:budget"); const alerts = Number(showAggregateAgentError) + Number(showBudgetAlert); return { diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index c6c8aaed..7a0f07d1 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -105,6 +105,7 @@ export const queryKeys = { }, dashboard: (companyId: string) => ["dashboard", companyId] as const, sidebarBadges: (companyId: string) => ["sidebar-badges", companyId] as const, + inboxDismissals: (companyId: string) => ["inbox-dismissals", companyId] as const, activity: (companyId: string) => ["activity", companyId] as const, costs: (companyId: string, from?: string, to?: string) => ["costs", companyId, from, to] as const, diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 238566f7..6a6e404b 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -86,6 +86,7 @@ import { getInboxKeyboardSelectionIndex, getLatestFailedRunsByAgent, getRecentTouchedIssues, + isInboxEntityDismissed, isMineInboxTab, loadInboxIssueColumns, normalizeInboxIssueColumns, @@ -99,7 +100,7 @@ import { type InboxTab, type InboxWorkItem, } from "../lib/inbox"; -import { useDismissedInboxItems, useReadInboxItems } from "../hooks/useInboxBadge"; +import { useDismissedInboxAlerts, useInboxDismissals, useReadInboxItems } from "../hooks/useInboxBadge"; type InboxCategoryFilter = | "everything" @@ -826,7 +827,8 @@ export function Inbox() { const [allCategoryFilter, setAllCategoryFilter] = useState("everything"); const [allApprovalFilter, setAllApprovalFilter] = useState("all"); const [visibleIssueColumns, setVisibleIssueColumns] = useState(loadInboxIssueColumns); - const { dismissed, dismiss } = useDismissedInboxItems(); + const { dismissed: dismissedAlerts, dismiss: dismissAlert } = useDismissedInboxAlerts(); + const { dismissedAtByKey, dismiss: dismissInboxItem } = useInboxDismissals(selectedCompanyId); const { readItems, markRead: markItemRead, markUnread: markItemUnread } = useReadInboxItems(); const pathSegment = location.pathname.split("/").pop() ?? "mine"; @@ -1033,8 +1035,11 @@ export function Inbox() { const currentUserId = session?.user.id ?? session?.session.userId ?? null; const failedRuns = useMemo( - () => getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter((r) => !dismissed.has(`run:${r.id}`)), - [heartbeatRuns, dismissed], + () => + getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter( + (r) => !isInboxEntityDismissed(dismissedAtByKey, `run:${r.id}`, r.createdAt), + ), + [heartbeatRuns, dismissedAtByKey], ); const liveIssueIds = useMemo(() => { const ids = new Set(); @@ -1049,10 +1054,12 @@ export function Inbox() { const approvalsToRender = useMemo(() => { let filtered = getApprovalsForTab(approvals ?? [], tab, allApprovalFilter); if (tab === "mine") { - filtered = filtered.filter((a) => !dismissed.has(`approval:${a.id}`)); + filtered = filtered.filter( + (a) => !isInboxEntityDismissed(dismissedAtByKey, `approval:${a.id}`, a.updatedAt), + ); } return filtered; - }, [approvals, tab, allApprovalFilter, dismissed]); + }, [approvals, tab, allApprovalFilter, dismissedAtByKey]); const showJoinRequestsCategory = allCategoryFilter === "everything" || allCategoryFilter === "join_requests"; const showTouchedCategory = @@ -1069,9 +1076,13 @@ export function Inbox() { const joinRequestsForTab = useMemo(() => { if (tab === "all" && !showJoinRequestsCategory) return []; - if (tab === "mine") return joinRequests.filter((jr) => !dismissed.has(`join:${jr.id}`)); + if (tab === "mine") { + return joinRequests.filter( + (jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt), + ); + } return joinRequests; - }, [joinRequests, tab, showJoinRequestsCategory, dismissed]); + }, [joinRequests, tab, showJoinRequestsCategory, dismissedAtByKey]); const workItemsToRender = useMemo( () => @@ -1385,14 +1396,18 @@ export function Inbox() { const handleArchiveNonIssue = useCallback((key: string) => { setArchivingNonIssueIds((prev) => new Set(prev).add(key)); setTimeout(() => { - dismiss(key); + if (key.startsWith("alert:")) { + dismissAlert(key); + } else { + dismissInboxItem(key); + } setArchivingNonIssueIds((prev) => { const next = new Set(prev); next.delete(key); return next; }); }, 200); - }, [dismiss]); + }, [dismissAlert, dismissInboxItem]); const nonIssueUnreadState = (key: string): NonIssueUnreadState => { if (!canArchiveFromTab) return null; @@ -1575,12 +1590,16 @@ export function Inbox() { } const hasRunFailures = failedRuns.length > 0; - const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures && !dismissed.has("alert:agent-errors"); + const showAggregateAgentError = + !!dashboard && + dashboard.agents.error > 0 && + !hasRunFailures && + !dismissedAlerts.has("alert:agent-errors"); const showBudgetAlert = !!dashboard && dashboard.costs.monthBudgetCents > 0 && dashboard.costs.monthUtilizationPercent >= 80 && - !dismissed.has("alert:budget"); + !dismissedAlerts.has("alert:budget"); const hasAlerts = showAggregateAgentError || showBudgetAlert; const showWorkItemsSection = filteredWorkItems.length > 0; const showAlertsSection = shouldShowInboxSection({ @@ -1891,7 +1910,7 @@ export function Inbox() { issueById={issueById} agentName={agentName(item.run.agentId)} issueLinkState={issueLinkState} - onDismiss={() => dismiss(runKey)} + onDismiss={() => dismissInboxItem(runKey)} onRetry={() => retryRunMutation.mutate(item.run)} isRetrying={retryingRunIds.has(item.run.id)} unreadState={nonIssueUnreadState(runKey)} @@ -2049,7 +2068,7 @@ export function Inbox() {
{ + it("defaults new agents to no timer heartbeat", () => { + expect(buildNewAgentRuntimeConfig()).toEqual({ + heartbeat: { + enabled: false, + intervalSec: 300, + wakeOnDemand: true, + cooldownSec: 10, + maxConcurrentRuns: 1, + }, + }); + }); + + it("preserves explicit heartbeat settings", () => { + expect( + buildNewAgentRuntimeConfig({ + heartbeatEnabled: true, + intervalSec: 3600, + }), + ).toEqual({ + heartbeat: { + enabled: true, + intervalSec: 3600, + wakeOnDemand: true, + cooldownSec: 10, + maxConcurrentRuns: 1, + }, + }); + }); +}); diff --git a/ui/src/lib/new-agent-runtime-config.ts b/ui/src/lib/new-agent-runtime-config.ts new file mode 100644 index 00000000..2de094b3 --- /dev/null +++ b/ui/src/lib/new-agent-runtime-config.ts @@ -0,0 +1,16 @@ +import { defaultCreateValues } from "../components/agent-config-defaults"; + +export function buildNewAgentRuntimeConfig(input?: { + heartbeatEnabled?: boolean; + intervalSec?: number; +}) { + return { + heartbeat: { + enabled: input?.heartbeatEnabled ?? defaultCreateValues.heartbeatEnabled, + intervalSec: input?.intervalSec ?? defaultCreateValues.intervalSec, + wakeOnDemand: true, + cooldownSec: 10, + maxConcurrentRuns: 1, + }, + }; +} diff --git a/ui/src/pages/NewAgent.tsx b/ui/src/pages/NewAgent.tsx index 9b1dd12c..498f8fad 100644 --- a/ui/src/pages/NewAgent.tsx +++ b/ui/src/pages/NewAgent.tsx @@ -23,6 +23,7 @@ import { getUIAdapter, listUIAdapters } from "../adapters"; import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters"; import { isValidAdapterType } from "../adapters/metadata"; import { ReportsToPicker } from "../components/ReportsToPicker"; +import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, DEFAULT_CODEX_LOCAL_MODEL, @@ -175,15 +176,10 @@ export function NewAgent() { ...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}), adapterType: configValues.adapterType, adapterConfig: buildAdapterConfig(), - runtimeConfig: { - heartbeat: { - enabled: configValues.heartbeatEnabled, - intervalSec: configValues.intervalSec, - wakeOnDemand: true, - cooldownSec: 10, - maxConcurrentRuns: 1, - }, - }, + runtimeConfig: buildNewAgentRuntimeConfig({ + heartbeatEnabled: configValues.heartbeatEnabled, + intervalSec: configValues.intervalSec, + }), budgetMonthlyCents: 0, }); } From 9eaf72ab3177017d22264752a2f4feea117ec9fb Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:02:27 -0500 Subject: [PATCH 19/74] Fix Codex tool-use transcript completion --- .../codex-local/src/ui/parse-stdout.test.ts | 83 +++++++++++++++++++ .../codex-local/src/ui/parse-stdout.ts | 54 ++++++++++-- ui/src/lib/issue-chat-messages.test.ts | 37 +++++++++ 3 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 packages/adapters/codex-local/src/ui/parse-stdout.test.ts diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.test.ts b/packages/adapters/codex-local/src/ui/parse-stdout.test.ts new file mode 100644 index 00000000..976377fa --- /dev/null +++ b/packages/adapters/codex-local/src/ui/parse-stdout.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { parseCodexStdoutLine } from "./parse-stdout.js"; + +describe("parseCodexStdoutLine", () => { + it("marks completed tool_use items as resolved tool results", () => { + const started = parseCodexStdoutLine(JSON.stringify({ + type: "item.started", + item: { + id: "tool-1", + type: "tool_use", + name: "search", + input: { query: "paperclip" }, + }, + }), "2026-04-08T12:00:00.000Z"); + + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-1", + type: "tool_use", + name: "search", + status: "completed", + }, + }), "2026-04-08T12:00:01.000Z"); + + expect(started).toEqual([{ + kind: "tool_call", + ts: "2026-04-08T12:00:00.000Z", + name: "search", + toolUseId: "tool-1", + input: { query: "paperclip" }, + }]); + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:01.000Z", + toolUseId: "tool-1", + content: "search completed", + isError: false, + }]); + }); + + it("keeps explicit tool_result payloads authoritative after tool_use completion", () => { + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-2", + type: "tool_result", + tool_use_id: "tool-1", + content: "final payload", + status: "completed", + }, + }), "2026-04-08T12:00:02.000Z"); + + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:02.000Z", + toolUseId: "tool-1", + content: "final payload", + isError: false, + }]); + }); + + it("marks failed completed tool_use items as error results", () => { + const completed = parseCodexStdoutLine(JSON.stringify({ + type: "item.completed", + item: { + id: "tool-3", + type: "tool_use", + name: "write_file", + status: "error", + error: { message: "permission denied" }, + }, + }), "2026-04-08T12:00:03.000Z"); + + expect(completed).toEqual([{ + kind: "tool_result", + ts: "2026-04-08T12:00:03.000Z", + toolUseId: "tool-3", + content: "permission denied", + isError: true, + }]); + }); +}); diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.ts b/packages/adapters/codex-local/src/ui/parse-stdout.ts index 0f1786b6..cb5661b9 100644 --- a/packages/adapters/codex-local/src/ui/parse-stdout.ts +++ b/packages/adapters/codex-local/src/ui/parse-stdout.ts @@ -118,6 +118,52 @@ function parseFileChangeItem(item: Record, ts: string): Transcr return [{ kind: "system", ts, text: `file changes: ${preview}${more}` }]; } +function parseToolUseItem( + item: Record, + ts: string, + phase: "started" | "completed", +): TranscriptEntry[] { + const name = asString(item.name, "unknown"); + const toolUseId = asString(item.id, name || "tool_use"); + + if (phase === "started") { + return [{ + kind: "tool_call", + ts, + name, + toolUseId, + input: item.input ?? {}, + }]; + } + + const status = asString(item.status); + const isError = + item.is_error === true || + status === "failed" || + status === "errored" || + status === "error" || + status === "cancelled"; + const rawContent = + item.content ?? + item.output ?? + item.result ?? + item.error ?? + item.message; + const content = + asString(rawContent) || + errorText(rawContent) || + stringifyUnknown(rawContent) || + `${name} ${isError ? "failed" : "completed"}`; + + return [{ + kind: "tool_result", + ts, + toolUseId, + content, + isError, + }]; +} + function parseCodexItem( item: Record, ts: string, @@ -146,13 +192,7 @@ function parseCodexItem( } if (itemType === "tool_use") { - return [{ - kind: "tool_call", - ts, - name: asString(item.name, "unknown"), - toolUseId: asString(item.id), - input: item.input ?? {}, - }]; + return parseToolUseItem(item, ts, phase); } if (itemType === "tool_result" && phase === "completed") { diff --git a/ui/src/lib/issue-chat-messages.test.ts b/ui/src/lib/issue-chat-messages.test.ts index f3ae87d7..388291f9 100644 --- a/ui/src/lib/issue-chat-messages.test.ts +++ b/ui/src/lib/issue-chat-messages.test.ts @@ -130,6 +130,43 @@ describe("buildAssistantPartsFromTranscript", () => { ]); }); + it("treats a completed tool-only segment as resolved once a tool_result arrives", () => { + const result = buildAssistantPartsFromTranscript([ + { kind: "thinking", ts: "2026-04-06T12:00:00.000Z", text: "Checking the task." }, + { + kind: "tool_call", + ts: "2026-04-06T12:00:01.000Z", + name: "search", + toolUseId: "tool-1", + input: { query: "paperclip" }, + }, + { + kind: "tool_result", + ts: "2026-04-06T12:00:02.000Z", + toolUseId: "tool-1", + content: "search completed", + isError: false, + }, + { kind: "assistant", ts: "2026-04-06T12:00:03.000Z", text: "Found the relevant code." }, + ]); + + expect(result.parts).toMatchObject([ + { type: "reasoning", text: "Checking the task." }, + { + type: "tool-call", + toolCallId: "tool-1", + toolName: "search", + result: "search completed", + isError: false, + }, + { type: "text", text: "Found the relevant code." }, + ]); + expect(result.segments).toEqual([{ + startMs: new Date("2026-04-06T12:00:00.000Z").getTime(), + endMs: new Date("2026-04-06T12:00:02.000Z").getTime(), + }]); + }); + it("keeps run errors while suppressing init and system transcript noise", () => { const result = buildAssistantPartsFromTranscript([ { From ec75cabcd81d6ab5ddb809e26705218383d775c6 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:05:35 -0500 Subject: [PATCH 20/74] Enforce execution-policy stage handoffs --- packages/adapter-utils/src/server-utils.ts | 106 +++++++- .../src/__tests__/codex-local-execute.test.ts | 246 ++++++++++++++++++ .../heartbeat-workspace-session.test.ts | 12 + .../issue-comment-reopen-routes.test.ts | 161 +++++++++++- .../__tests__/issue-execution-policy.test.ts | 131 ++++++++-- server/src/routes/issues.ts | 191 ++++++++++++-- server/src/services/heartbeat.ts | 52 ++-- server/src/services/issue-execution-policy.ts | 188 ++++++++----- 8 files changed, 949 insertions(+), 138 deletions(-) diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 629924d9..1c6a2795 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -201,6 +201,22 @@ type PaperclipWakeIssue = { priority: string | null; }; +type PaperclipWakeExecutionPrincipal = { + type: "agent" | "user" | null; + agentId: string | null; + userId: string | null; +}; + +type PaperclipWakeExecutionStage = { + wakeRole: "reviewer" | "approver" | "executor" | null; + stageId: string | null; + stageType: string | null; + currentParticipant: PaperclipWakeExecutionPrincipal | null; + returnAssignee: PaperclipWakeExecutionPrincipal | null; + lastDecisionOutcome: string | null; + allowedActions: string[]; +}; + type PaperclipWakeComment = { id: string | null; issueId: string | null; @@ -214,6 +230,7 @@ type PaperclipWakeComment = { type PaperclipWakePayload = { reason: string | null; issue: PaperclipWakeIssue | null; + executionStage: PaperclipWakeExecutionStage | null; commentIds: string[]; latestCommentId: string | null; comments: PaperclipWakeComment[]; @@ -257,6 +274,50 @@ function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | n }; } +function normalizePaperclipWakeExecutionPrincipal(value: unknown): PaperclipWakeExecutionPrincipal | null { + const principal = parseObject(value); + const typeRaw = asString(principal.type, "").trim().toLowerCase(); + if (typeRaw !== "agent" && typeRaw !== "user") return null; + return { + type: typeRaw, + agentId: asString(principal.agentId, "").trim() || null, + userId: asString(principal.userId, "").trim() || null, + }; +} + +function normalizePaperclipWakeExecutionStage(value: unknown): PaperclipWakeExecutionStage | null { + const stage = parseObject(value); + const wakeRoleRaw = asString(stage.wakeRole, "").trim().toLowerCase(); + const wakeRole = + wakeRoleRaw === "reviewer" || wakeRoleRaw === "approver" || wakeRoleRaw === "executor" + ? wakeRoleRaw + : null; + const allowedActions = Array.isArray(stage.allowedActions) + ? stage.allowedActions + .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) + .map((entry) => entry.trim()) + : []; + const currentParticipant = normalizePaperclipWakeExecutionPrincipal(stage.currentParticipant); + const returnAssignee = normalizePaperclipWakeExecutionPrincipal(stage.returnAssignee); + const stageId = asString(stage.stageId, "").trim() || null; + const stageType = asString(stage.stageType, "").trim() || null; + const lastDecisionOutcome = asString(stage.lastDecisionOutcome, "").trim() || null; + + if (!wakeRole && !stageId && !stageType && !currentParticipant && !returnAssignee && !lastDecisionOutcome && allowedActions.length === 0) { + return null; + } + + return { + wakeRole, + stageId, + stageType, + currentParticipant, + returnAssignee, + lastDecisionOutcome, + allowedActions, + }; +} + export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayload | null { const payload = parseObject(value); const comments = Array.isArray(payload.comments) @@ -270,12 +331,16 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) .map((entry) => entry.trim()) : []; + const executionStage = normalizePaperclipWakeExecutionStage(payload.executionStage); - if (comments.length === 0 && commentIds.length === 0) return null; + if (comments.length === 0 && commentIds.length === 0 && !executionStage && !normalizePaperclipWakeIssue(payload.issue)) { + return null; + } return { reason: asString(payload.reason, "").trim() || null, issue: normalizePaperclipWakeIssue(payload.issue), + executionStage, commentIds, latestCommentId: asString(payload.latestCommentId, "").trim() || null, comments, @@ -300,6 +365,12 @@ export function renderPaperclipWakePrompt( const normalized = normalizePaperclipWakePayload(value); if (!normalized) return ""; const resumedSession = options.resumedSession === true; + const executionStage = normalized.executionStage; + const principalLabel = (principal: PaperclipWakeExecutionPrincipal | null) => { + if (!principal || !principal.type) return "unknown"; + if (principal.type === "agent") return principal.agentId ? `agent ${principal.agentId}` : "agent"; + return principal.userId ? `user ${principal.userId}` : "user"; + }; const lines = resumedSession ? [ @@ -342,7 +413,38 @@ export function renderPaperclipWakePrompt( lines.push(`- omitted comments: ${normalized.missingCount}`); } - lines.push("", "New comments in order:"); + if (executionStage) { + lines.push( + `- execution wake role: ${executionStage.wakeRole ?? "unknown"}`, + `- execution stage: ${executionStage.stageType ?? "unknown"}`, + `- execution participant: ${principalLabel(executionStage.currentParticipant)}`, + `- execution return assignee: ${principalLabel(executionStage.returnAssignee)}`, + `- last decision outcome: ${executionStage.lastDecisionOutcome ?? "none"}`, + ); + if (executionStage.allowedActions.length > 0) { + lines.push(`- allowed actions: ${executionStage.allowedActions.join(", ")}`); + } + lines.push(""); + if (executionStage.wakeRole === "reviewer" || executionStage.wakeRole === "approver") { + lines.push( + `You are waking as the active ${executionStage.wakeRole} for this issue.`, + "Do not execute the task itself or continue executor work.", + "Review the issue and choose one of the allowed actions above.", + "If you request changes, the workflow routes back to the stored return assignee.", + "", + ); + } else if (executionStage.wakeRole === "executor") { + lines.push( + "You are waking because changes were requested in the execution workflow.", + "Address the requested changes on this issue and resubmit when the work is ready.", + "", + ); + } + } + + if (normalized.comments.length > 0) { + lines.push("New comments in order:"); + } for (const [index, comment] of normalized.comments.entries()) { const authorLabel = comment.authorId diff --git a/server/src/__tests__/codex-local-execute.test.ts b/server/src/__tests__/codex-local-execute.test.ts index da648367..9514a977 100644 --- a/server/src/__tests__/codex-local-execute.test.ts +++ b/server/src/__tests__/codex-local-execute.test.ts @@ -369,6 +369,252 @@ describe("codex execute", () => { } }); + it("renders execution-stage wake instructions for reviewer and executor roles", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-stage-wake-")); + const workspace = path.join(root, "workspace"); + const commandPath = path.join(root, "codex"); + const capturePath = path.join(root, "capture.json"); + await fs.mkdir(workspace, { recursive: true }); + await writeFakeCodexCommand(commandPath); + + const previousHome = process.env.HOME; + process.env.HOME = root; + + try { + const result = await execute({ + runId: "run-stage-wake", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Codex Coder", + adapterType: "codex_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + env: { + PAPERCLIP_TEST_CAPTURE_PATH: capturePath, + }, + promptTemplate: "Follow the paperclip heartbeat.", + }, + context: { + issueId: "issue-1", + taskId: "issue-1", + wakeReason: "execution_review_requested", + paperclipWake: { + reason: "execution_review_requested", + issue: { + id: "issue-1", + identifier: "PAP-1207", + title: "implement the plan of PAP-1200", + status: "in_review", + priority: "medium", + }, + executionStage: { + wakeRole: "reviewer", + stageId: "stage-1", + stageType: "review", + currentParticipant: { type: "agent", agentId: "qa-agent" }, + returnAssignee: { type: "agent", agentId: "coder-agent" }, + lastDecisionOutcome: null, + allowedActions: ["approve", "request_changes"], + }, + commentIds: [], + latestCommentId: null, + comments: [], + commentWindow: { + requestedCount: 0, + includedCount: 0, + missingCount: 0, + }, + truncated: false, + fallbackFetchNeeded: false, + }, + }, + authToken: "run-jwt-token", + onLog: async () => {}, + }); + + expect(result.exitCode).toBe(0); + const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; + expect(capture.prompt).toContain("execution wake role: reviewer"); + expect(capture.prompt).toContain("You are waking as the active reviewer for this issue."); + expect(capture.prompt).toContain("Do not execute the task itself or continue executor work."); + expect(capture.prompt).toContain("allowed actions: approve, request_changes"); + + const executorCapturePath = path.join(root, "capture-executor.json"); + const executorResult = await execute({ + runId: "run-stage-wake-executor", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Codex Coder", + adapterType: "codex_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + env: { + PAPERCLIP_TEST_CAPTURE_PATH: executorCapturePath, + }, + promptTemplate: "Follow the paperclip heartbeat.", + }, + context: { + issueId: "issue-1", + taskId: "issue-1", + wakeReason: "execution_changes_requested", + paperclipWake: { + reason: "execution_changes_requested", + issue: { + id: "issue-1", + identifier: "PAP-1207", + title: "implement the plan of PAP-1200", + status: "in_progress", + priority: "medium", + }, + executionStage: { + wakeRole: "executor", + stageId: "stage-1", + stageType: "review", + currentParticipant: { type: "agent", agentId: "qa-agent" }, + returnAssignee: { type: "agent", agentId: "coder-agent" }, + lastDecisionOutcome: "changes_requested", + allowedActions: ["address_changes", "resubmit"], + }, + commentIds: [], + latestCommentId: null, + comments: [], + commentWindow: { + requestedCount: 0, + includedCount: 0, + missingCount: 0, + }, + truncated: false, + fallbackFetchNeeded: false, + }, + }, + authToken: "run-jwt-token", + onLog: async () => {}, + }); + + expect(executorResult.exitCode).toBe(0); + const executorCapture = JSON.parse(await fs.readFile(executorCapturePath, "utf8")) as CapturePayload; + expect(executorCapture.prompt).toContain("execution wake role: executor"); + expect(executorCapture.prompt).toContain("You are waking because changes were requested in the execution workflow."); + expect(executorCapture.prompt).toContain("allowed actions: address_changes, resubmit"); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("renders an issue-scoped wake prompt even when the wake has no comments yet", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-issue-wake-")); + const workspace = path.join(root, "workspace"); + const commandPath = path.join(root, "codex"); + const capturePath = path.join(root, "capture.json"); + await fs.mkdir(workspace, { recursive: true }); + await writeFakeCodexCommand(commandPath); + + const previousHome = process.env.HOME; + process.env.HOME = root; + + try { + const result = await execute({ + runId: "run-issue-wake", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Codex Coder", + adapterType: "codex_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: commandPath, + cwd: workspace, + env: { + PAPERCLIP_TEST_CAPTURE_PATH: capturePath, + }, + promptTemplate: "Follow the paperclip heartbeat.", + }, + context: { + issueId: "issue-1", + taskId: "issue-1", + wakeReason: "issue_assigned", + paperclipWake: { + reason: "issue_assigned", + issue: { + id: "issue-1", + identifier: "PAP-1201", + title: "Fix gallery opening for inline images", + status: "todo", + priority: "medium", + }, + commentIds: [], + latestCommentId: null, + comments: [], + commentWindow: { + requestedCount: 0, + includedCount: 0, + missingCount: 0, + }, + truncated: false, + fallbackFetchNeeded: false, + }, + }, + authToken: "run-jwt-token", + onLog: async () => {}, + }); + + expect(result.exitCode).toBe(0); + expect(result.errorMessage).toBeNull(); + + const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; + expect(capture.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON"); + expect(capture.paperclipWakePayloadJson).not.toBeNull(); + expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({ + reason: "issue_assigned", + issue: { + identifier: "PAP-1201", + title: "Fix gallery opening for inline images", + status: "todo", + priority: "medium", + }, + commentIds: [], + }); + expect(capture.prompt).toContain("## Paperclip Wake Payload"); + expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake."); + expect(capture.prompt).toContain("- issue: PAP-1201 Fix gallery opening for inline images"); + expect(capture.prompt).toContain("- pending comments: 0/0"); + expect(capture.prompt).toContain("- issue status: todo"); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("uses a compact wake delta instead of the full heartbeat prompt when resuming a session", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-resume-wake-")); const workspace = path.join(root, "workspace"); diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index 859c8960..d97f63c7 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -272,6 +272,18 @@ describe("shouldResetTaskSessionForWake", () => { expect(shouldResetTaskSessionForWake({ wakeReason: "issue_assigned" })).toBe(true); }); + it("resets session context on execution review wakes", () => { + expect(shouldResetTaskSessionForWake({ wakeReason: "execution_review_requested" })).toBe(true); + }); + + it("resets session context on execution approval wakes", () => { + expect(shouldResetTaskSessionForWake({ wakeReason: "execution_approval_requested" })).toBe(true); + }); + + it("resets session context on execution changes-requested wakes", () => { + expect(shouldResetTaskSessionForWake({ wakeReason: "execution_changes_requested" })).toBe(true); + }); + it("preserves session context on timer heartbeats", () => { expect(shouldResetTaskSessionForWake({ wakeSource: "timer" })).toBe(false); }); diff --git a/server/src/__tests__/issue-comment-reopen-routes.test.ts b/server/src/__tests__/issue-comment-reopen-routes.test.ts index 2594e36e..135d8066 100644 --- a/server/src/__tests__/issue-comment-reopen-routes.test.ts +++ b/server/src/__tests__/issue-comment-reopen-routes.test.ts @@ -7,6 +7,7 @@ import { normalizeIssueExecutionPolicy } from "../services/issue-execution-polic const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), + assertCheckoutOwner: vi.fn(), update: vi.fn(), addComment: vi.fn(), findMentionedAgents: vi.fn(), @@ -75,8 +76,12 @@ vi.mock("../services/index.js", () => ({ function createApp() { const app = express(); app.use(express.json()); + return app; +} + +function installActor(app: express.Express, actor?: Record) { app.use((req, _res, next) => { - (req as any).actor = { + (req as any).actor = actor ?? { type: "board", userId: "local-board", companyIds: ["company-1"], @@ -119,6 +124,10 @@ describe("issue comment reopen routes", () => { mockIssueService.findMentionedAgents.mockResolvedValue([]); mockIssueService.listWakeableBlockedDependents.mockResolvedValue([]); mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); + mockIssueService.assertCheckoutOwner.mockResolvedValue({ adoptedFromRunId: null }); + mockAccessService.canUser.mockResolvedValue(false); + mockAccessService.hasPermission.mockResolvedValue(false); + mockAgentService.getById.mockResolvedValue(null); }); it("treats reopen=true as a no-op when the issue is already open", async () => { @@ -128,7 +137,7 @@ describe("issue comment reopen routes", () => { ...patch, })); - const res = await request(createApp()) + const res = await request(installActor(createApp())) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ comment: "hello", reopen: true, assigneeAgentId: "33333333-3333-4333-8333-333333333333" }); @@ -157,7 +166,7 @@ describe("issue comment reopen routes", () => { ...patch, })); - const res = await request(createApp()) + const res = await request(installActor(createApp())) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ comment: "hello", reopen: true, assigneeAgentId: "33333333-3333-4333-8333-333333333333" }); @@ -207,7 +216,7 @@ describe("issue comment reopen routes", () => { status: "cancelled", }); - const res = await request(createApp()) + const res = await request(installActor(createApp())) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ comment: "hello", interrupt: true, assigneeAgentId: "33333333-3333-4333-8333-333333333333" }); @@ -265,7 +274,7 @@ describe("issue comment reopen routes", () => { _tx: tx, })); - const res = await request(createApp()) + const res = await request(installActor(createApp())) .patch("/api/issues/11111111-1111-4111-8111-111111111111") .send({ status: "done", comment: "Approved for ship" }); @@ -294,4 +303,146 @@ describe("issue comment reopen routes", () => { }), ); }); + + it("coerces executor handoff patches into workflow-controlled review wakes", async () => { + const policy = normalizeIssueExecutionPolicy({ + stages: [ + { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + type: "review", + participants: [{ type: "agent", agentId: "33333333-3333-4333-8333-333333333333" }], + }, + ], + })!; + const issue = { + ...makeIssue("todo"), + status: "in_progress", + assigneeAgentId: "22222222-2222-4222-8222-222222222222", + executionPolicy: policy, + executionState: null, + }; + mockIssueService.getById.mockResolvedValue(issue); + mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...issue, + ...patch, + updatedAt: new Date(), + })); + + const res = await request( + installActor(createApp(), { + type: "agent", + agentId: "22222222-2222-4222-8222-222222222222", + companyId: "company-1", + runId: "run-1", + }), + ) + .patch("/api/issues/11111111-1111-4111-8111-111111111111") + .send({ + status: "in_review", + assigneeAgentId: null, + assigneeUserId: "local-board", + }); + + expect(res.status).toBe(200); + expect(mockIssueService.update).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + expect.objectContaining({ + status: "in_review", + assigneeAgentId: "33333333-3333-4333-8333-333333333333", + assigneeUserId: null, + executionState: expect.objectContaining({ + status: "pending", + currentStageType: "review", + currentParticipant: expect.objectContaining({ + type: "agent", + agentId: "33333333-3333-4333-8333-333333333333", + }), + returnAssignee: expect.objectContaining({ + type: "agent", + agentId: "22222222-2222-4222-8222-222222222222", + }), + }), + }), + ); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + "33333333-3333-4333-8333-333333333333", + expect.objectContaining({ + reason: "execution_review_requested", + payload: expect.objectContaining({ + issueId: "11111111-1111-4111-8111-111111111111", + executionStage: expect.objectContaining({ + wakeRole: "reviewer", + stageType: "review", + allowedActions: ["approve", "request_changes"], + }), + }), + }), + ); + }); + + it("wakes the return assignee with execution_changes_requested", async () => { + const policy = normalizeIssueExecutionPolicy({ + stages: [ + { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + type: "review", + participants: [{ type: "agent", agentId: "33333333-3333-4333-8333-333333333333" }], + }, + ], + })!; + const issue = { + ...makeIssue("todo"), + status: "in_review", + assigneeAgentId: "33333333-3333-4333-8333-333333333333", + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: policy.stages[0].id, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: "33333333-3333-4333-8333-333333333333" }, + returnAssignee: { type: "agent", agentId: "22222222-2222-4222-8222-222222222222" }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }; + mockIssueService.getById.mockResolvedValue(issue); + mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...issue, + ...patch, + updatedAt: new Date(), + })); + + const res = await request( + installActor(createApp(), { + type: "agent", + agentId: "33333333-3333-4333-8333-333333333333", + companyId: "company-1", + runId: "run-2", + }), + ) + .patch("/api/issues/11111111-1111-4111-8111-111111111111") + .send({ + status: "in_progress", + comment: "Needs another pass", + }); + + expect(res.status).toBe(200); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + "22222222-2222-4222-8222-222222222222", + expect.objectContaining({ + reason: "execution_changes_requested", + payload: expect.objectContaining({ + issueId: "11111111-1111-4111-8111-111111111111", + executionStage: expect.objectContaining({ + wakeRole: "executor", + stageType: "review", + lastDecisionOutcome: "changes_requested", + allowedActions: ["address_changes", "resubmit"], + }), + }), + }), + ); + }); }); diff --git a/server/src/__tests__/issue-execution-policy.test.ts b/server/src/__tests__/issue-execution-policy.test.ts index aedc4305..9f5fb80b 100644 --- a/server/src/__tests__/issue-execution-policy.test.ts +++ b/server/src/__tests__/issue-execution-policy.test.ts @@ -413,33 +413,45 @@ describe("issue execution policy transitions", () => { const policy = twoStagePolicy(); const reviewStageId = policy.stages[0].id; - it("non-participant cannot advance stage via status change", () => { - expect(() => - applyIssueExecutionPolicyTransition({ - issue: { - status: "in_review", - assigneeAgentId: qaAgentId, - assigneeUserId: null, - executionPolicy: policy, - executionState: { - status: "pending", - currentStageId: reviewStageId, - currentStageIndex: 0, - currentStageType: "review", - currentParticipant: { type: "agent", agentId: qaAgentId }, - returnAssignee: { type: "agent", agentId: coderAgentId }, - completedStageIds: [], - lastDecisionId: null, - lastDecisionOutcome: null, - }, + it("non-participant stage updates are coerced back to the active stage", () => { + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, }, - policy, - requestedStatus: "done", - requestedAssigneePatch: {}, - actor: { agentId: coderAgentId }, - commentBody: "Trying to bypass review", - }), - ).toThrow("Only the active reviewer or approver can advance"); + }, + policy, + requestedStatus: "done", + requestedAssigneePatch: { assigneeUserId: boardUserId }, + actor: { agentId: coderAgentId }, + commentBody: "Trying to bypass review", + }); + + expect(result.patch).toMatchObject({ + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + }, + }); + expect(result.decision).toBeUndefined(); }); it("non-participant can still post non-advancing updates", () => { @@ -663,6 +675,7 @@ describe("issue execution policy transitions", () => { describe("no-op transitions", () => { const policy = twoStagePolicy(); + const reviewStageId = policy.stages[0].id; it("non-done status change without review context is a no-op", () => { const result = applyIssueExecutionPolicyTransition({ @@ -682,6 +695,72 @@ describe("issue execution policy transitions", () => { expect(result.patch).toEqual({}); }); + it("coerces a malformed executor in_review patch into the first policy stage", () => { + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_progress", + assigneeAgentId: coderAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: null, + }, + policy, + requestedStatus: "in_review", + requestedAssigneePatch: { assigneeUserId: boardUserId }, + actor: { agentId: coderAgentId }, + }); + + expect(result.patch).toMatchObject({ + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionState: { + status: "pending", + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + }, + }); + }); + + it("reasserts the active stage when issue status drifted out of in_review", () => { + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_progress", + assigneeAgentId: coderAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }, + policy, + requestedStatus: "in_progress", + requestedAssigneePatch: { assigneeAgentId: coderAgentId }, + actor: { agentId: coderAgentId }, + }); + + expect(result.patch).toMatchObject({ + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + }, + }); + }); + it("no policy and no state is a no-op", () => { const result = applyIssueExecutionPolicyTransition({ issue: { diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index d133e5fe..f72b19cd 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -56,13 +56,149 @@ import { SVG_CONTENT_TYPE, } from "../attachment-types.js"; import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js"; -import { applyIssueExecutionPolicyTransition, normalizeIssueExecutionPolicy } from "../services/issue-execution-policy.js"; +import { + applyIssueExecutionPolicyTransition, + normalizeIssueExecutionPolicy, + parseIssueExecutionState, +} from "../services/issue-execution-policy.js"; const MAX_ISSUE_COMMENT_LIMIT = 500; const updateIssueRouteSchema = updateIssueSchema.extend({ interrupt: z.boolean().optional(), }); +type ParsedExecutionState = NonNullable>; +type ExecutionStageWakeContext = { + wakeRole: "reviewer" | "approver" | "executor"; + stageId: string | null; + stageType: ParsedExecutionState["currentStageType"]; + currentParticipant: ParsedExecutionState["currentParticipant"]; + returnAssignee: ParsedExecutionState["returnAssignee"]; + lastDecisionOutcome: ParsedExecutionState["lastDecisionOutcome"]; + allowedActions: string[]; +}; + +function executionPrincipalsEqual( + left: ParsedExecutionState["currentParticipant"] | null, + right: ParsedExecutionState["currentParticipant"] | null, +) { + if (!left || !right || left.type !== right.type) return false; + return left.type === "agent" ? left.agentId === right.agentId : left.userId === right.userId; +} + +function buildExecutionStageWakeContext(input: { + state: ParsedExecutionState; + wakeRole: ExecutionStageWakeContext["wakeRole"]; + allowedActions: string[]; +}): ExecutionStageWakeContext { + return { + wakeRole: input.wakeRole, + stageId: input.state.currentStageId, + stageType: input.state.currentStageType, + currentParticipant: input.state.currentParticipant, + returnAssignee: input.state.returnAssignee, + lastDecisionOutcome: input.state.lastDecisionOutcome, + allowedActions: input.allowedActions, + }; +} + +function buildExecutionStageWakeup(input: { + issueId: string; + previousState: ParsedExecutionState | null; + nextState: ParsedExecutionState | null; + interruptedRunId: string | null; + requestedByActorType: "user" | "agent"; + requestedByActorId: string; +}) { + const { issueId, previousState, nextState, interruptedRunId } = input; + if (!nextState) return null; + + if (nextState.status === "pending") { + const agentId = + nextState.currentParticipant?.type === "agent" ? (nextState.currentParticipant.agentId ?? null) : null; + const stageChanged = + previousState?.status !== "pending" || + previousState?.currentStageId !== nextState.currentStageId || + !executionPrincipalsEqual(previousState?.currentParticipant ?? null, nextState.currentParticipant ?? null); + if (!agentId || !stageChanged) return null; + + const reason = + nextState.currentStageType === "approval" ? "execution_approval_requested" : "execution_review_requested"; + const executionStage = buildExecutionStageWakeContext({ + state: nextState, + wakeRole: nextState.currentStageType === "approval" ? "approver" : "reviewer", + allowedActions: ["approve", "request_changes"], + }); + + return { + agentId, + wakeup: { + source: "assignment" as const, + triggerDetail: "system" as const, + reason, + payload: { + issueId, + mutation: "update", + executionStage, + ...(interruptedRunId ? { interruptedRunId } : {}), + }, + requestedByActorType: input.requestedByActorType, + requestedByActorId: input.requestedByActorId, + contextSnapshot: { + issueId, + taskId: issueId, + wakeReason: reason, + source: "issue.execution_stage", + executionStage, + ...(interruptedRunId ? { interruptedRunId } : {}), + }, + }, + }; + } + + if (nextState.status === "changes_requested") { + const agentId = nextState.returnAssignee?.type === "agent" ? (nextState.returnAssignee.agentId ?? null) : null; + const becameChangesRequested = + previousState?.status !== "changes_requested" || + previousState?.lastDecisionId !== nextState.lastDecisionId || + !executionPrincipalsEqual(previousState?.returnAssignee ?? null, nextState.returnAssignee ?? null); + if (!agentId || !becameChangesRequested) return null; + + const executionStage = buildExecutionStageWakeContext({ + state: nextState, + wakeRole: "executor", + allowedActions: ["address_changes", "resubmit"], + }); + + return { + agentId, + wakeup: { + source: "assignment" as const, + triggerDetail: "system" as const, + reason: "execution_changes_requested", + payload: { + issueId, + mutation: "update", + executionStage, + ...(interruptedRunId ? { interruptedRunId } : {}), + }, + requestedByActorType: input.requestedByActorType, + requestedByActorId: input.requestedByActorId, + contextSnapshot: { + issueId, + taskId: issueId, + wakeReason: "execution_changes_requested", + source: "issue.execution_stage", + executionStage, + ...(interruptedRunId ? { interruptedRunId } : {}), + }, + }, + }; + } + + return null; +} + export function issueRoutes( db: Db, storage: StorageService, @@ -1110,24 +1246,6 @@ export function issueRoutes( return; } assertCompanyAccess(req, existing.companyId); - const assigneeWillChange = - (req.body.assigneeAgentId !== undefined && req.body.assigneeAgentId !== existing.assigneeAgentId) || - (req.body.assigneeUserId !== undefined && req.body.assigneeUserId !== existing.assigneeUserId); - - const isAgentReturningIssueToCreator = - req.actor.type === "agent" && - !!req.actor.agentId && - existing.assigneeAgentId === req.actor.agentId && - req.body.assigneeAgentId === null && - typeof req.body.assigneeUserId === "string" && - !!existing.createdByUserId && - req.body.assigneeUserId === existing.createdByUserId; - - if (assigneeWillChange) { - if (!isAgentReturningIssueToCreator) { - await assertCanAssignTasks(req, existing.companyId); - } - } if (!(await assertAgentRunCheckoutOwnership(req, res, existing))) return; const actor = getActorInfo(req); @@ -1224,6 +1342,27 @@ export function issueRoutes( } Object.assign(updateFields, transition.patch); + const nextAssigneeAgentId = + updateFields.assigneeAgentId === undefined ? existing.assigneeAgentId : (updateFields.assigneeAgentId as string | null); + const nextAssigneeUserId = + updateFields.assigneeUserId === undefined ? existing.assigneeUserId : (updateFields.assigneeUserId as string | null); + const assigneeWillChange = + nextAssigneeAgentId !== existing.assigneeAgentId || nextAssigneeUserId !== existing.assigneeUserId; + const isAgentReturningIssueToCreator = + req.actor.type === "agent" && + !!req.actor.agentId && + existing.assigneeAgentId === req.actor.agentId && + nextAssigneeAgentId === null && + typeof nextAssigneeUserId === "string" && + !!existing.createdByUserId && + nextAssigneeUserId === existing.createdByUserId; + + if (assigneeWillChange && !transition.workflowControlledAssignment) { + if (!isAgentReturningIssueToCreator) { + await assertCanAssignTasks(req, existing.companyId); + } + } + let issue; try { if (transition.decision && decisionId) { @@ -1414,6 +1553,16 @@ export function issueRoutes( existing.status === "backlog" && issue.status !== "backlog" && req.body.status !== undefined; + const previousExecutionState = parseIssueExecutionState(existing.executionState); + const nextExecutionState = parseIssueExecutionState(issue.executionState); + const executionStageWakeup = buildExecutionStageWakeup({ + issueId: issue.id, + previousState: previousExecutionState, + nextState: nextExecutionState, + interruptedRunId, + requestedByActorType: actor.actorType, + requestedByActorId: actor.actorId, + }); // Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs. void (async () => { @@ -1427,7 +1576,9 @@ export function issueRoutes( wakeups.set(`${agentId}:${wakeIssueId}`, { agentId, wakeup }); }; - if (assigneeChanged && issue.assigneeAgentId && issue.status !== "backlog") { + if (executionStageWakeup) { + addWakeup(executionStageWakeup.agentId, executionStageWakeup.wakeup); + } else if (assigneeChanged && issue.assigneeAgentId && issue.status !== "backlog") { addWakeup(issue.assigneeAgentId, { source: "assignment", triggerDetail: "system", diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 32dc4913..d94922a0 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -696,7 +696,14 @@ export function shouldResetTaskSessionForWake( if (contextSnapshot?.forceFreshSession === true) return true; const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); - if (wakeReason === "issue_assigned") return true; + if ( + wakeReason === "issue_assigned" || + wakeReason === "execution_review_requested" || + wakeReason === "execution_approval_requested" || + wakeReason === "execution_changes_requested" + ) { + return true; + } return false; } @@ -714,6 +721,9 @@ function describeSessionResetReason( const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason); if (wakeReason === "issue_assigned") return "wake reason is issue_assigned"; + if (wakeReason === "execution_review_requested") return "wake reason is execution_review_requested"; + if (wakeReason === "execution_approval_requested") return "wake reason is execution_approval_requested"; + if (wakeReason === "execution_changes_requested") return "wake reason is execution_changes_requested"; return null; } @@ -867,9 +877,8 @@ async function buildPaperclipWakePayload(input: { } | null; }) { + const executionStage = parseObject(input.contextSnapshot.executionStage); const commentIds = extractWakeCommentIds(input.contextSnapshot); - if (commentIds.length === 0) return null; - const issueId = readNonEmptyString(input.contextSnapshot.issueId); const issueSummary = input.issueSummary ?? @@ -886,23 +895,27 @@ async function buildPaperclipWakePayload(input: { .where(and(eq(issues.id, issueId), eq(issues.companyId, input.companyId))) .then((rows) => rows[0] ?? null) : null); + if (commentIds.length === 0 && Object.keys(executionStage).length === 0 && !issueSummary) return null; - const commentRows = await input.db - .select({ - id: issueComments.id, - issueId: issueComments.issueId, - body: issueComments.body, - authorAgentId: issueComments.authorAgentId, - authorUserId: issueComments.authorUserId, - createdAt: issueComments.createdAt, - }) - .from(issueComments) - .where( - and( - eq(issueComments.companyId, input.companyId), - inArray(issueComments.id, commentIds), - ), - ); + const commentRows = + commentIds.length === 0 + ? [] + : await input.db + .select({ + id: issueComments.id, + issueId: issueComments.issueId, + body: issueComments.body, + authorAgentId: issueComments.authorAgentId, + authorUserId: issueComments.authorUserId, + createdAt: issueComments.createdAt, + }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, input.companyId), + inArray(issueComments.id, commentIds), + ), + ); const commentsById = new Map(commentRows.map((comment) => [comment.id, comment])); const comments: Array> = []; @@ -959,6 +972,7 @@ async function buildPaperclipWakePayload(input: { priority: issueSummary.priority, } : null, + executionStage: Object.keys(executionStage).length > 0 ? executionStage : null, commentIds, latestCommentId: commentIds[commentIds.length - 1] ?? null, comments, diff --git a/server/src/services/issue-execution-policy.ts b/server/src/services/issue-execution-policy.ts index 86de20e4..3bc21c03 100644 --- a/server/src/services/issue-execution-policy.ts +++ b/server/src/services/issue-execution-policy.ts @@ -36,6 +36,7 @@ type TransitionInput = { type TransitionResult = { patch: Record; decision?: Pick; + workflowControlledAssignment?: boolean; }; const COMPLETED_STATUS: IssueExecutionState["status"] = "completed"; @@ -198,14 +199,36 @@ function buildChangesRequestedState(previous: IssueExecutionState, currentStage: }; } +function buildPendingStagePatch(input: { + patch: Record; + previous: IssueExecutionState | null; + policy: IssueExecutionPolicy; + stage: IssueExecutionStage; + participant: IssueExecutionStagePrincipal; + returnAssignee: IssueExecutionStagePrincipal | null; +}) { + input.patch.status = "in_review"; + Object.assign(input.patch, patchForPrincipal(input.participant)); + input.patch.executionState = buildPendingState({ + previous: input.previous, + stage: input.stage, + stageIndex: input.policy.stages.findIndex((candidate) => candidate.id === input.stage.id), + participant: input.participant, + returnAssignee: input.returnAssignee, + }); +} + export function applyIssueExecutionPolicyTransition(input: TransitionInput): TransitionResult { const patch: Record = {}; const existingState = parseIssueExecutionState(input.issue.executionState); const currentAssignee = assigneePrincipal(input.issue); const actor = actorPrincipal(input.actor); + const requestedAssigneePatchProvided = + input.requestedAssigneePatch.assigneeAgentId !== undefined || input.requestedAssigneePatch.assigneeUserId !== undefined; const explicitAssignee = assigneePrincipal(input.requestedAssigneePatch); const currentStage = input.policy ? findStageById(input.policy, existingState?.currentStageId) : null; const requestedStatus = input.requestedStatus; + const activeStage = currentStage && existingState?.status === PENDING_STATUS ? currentStage : null; if (!input.policy) { if (existingState) { @@ -228,90 +251,121 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra return { patch }; } - if (currentStage && input.issue.status === "in_review") { - if (!principalsEqual(existingState?.currentParticipant ?? null, actor)) { - if (requestedStatus && requestedStatus !== "in_review") { - throw unprocessable("Only the active reviewer or approver can advance the current execution stage"); - } - return { patch }; + if (activeStage) { + const currentParticipant = + existingState?.currentParticipant ?? + selectStageParticipant(activeStage, { + exclude: existingState?.returnAssignee ?? null, + }); + if (!currentParticipant) { + throw unprocessable(`No eligible ${activeStage.type} participant is configured for this issue`); } - if (requestedStatus === "done") { - if (!input.commentBody?.trim()) { - throw unprocessable("Approving a review or approval stage requires a comment"); - } - const approvedState = buildCompletedState(existingState, currentStage); - const nextStage = nextPendingStage( - input.policy, - { ...approvedState, completedStageIds: approvedState.completedStageIds }, - ); + if (principalsEqual(currentParticipant, actor)) { + if (requestedStatus === "done") { + if (!input.commentBody?.trim()) { + throw unprocessable("Approving a review or approval stage requires a comment"); + } + const approvedState = buildCompletedState(existingState, activeStage); + const nextStage = nextPendingStage( + input.policy, + { ...approvedState, completedStageIds: approvedState.completedStageIds }, + ); - if (!nextStage) { - patch.executionState = approvedState; + if (!nextStage) { + patch.executionState = approvedState; + return { + patch, + decision: { + stageId: activeStage.id, + stageType: activeStage.type, + outcome: "approved", + body: input.commentBody.trim(), + }, + }; + } + + const participant = selectStageParticipant(nextStage, { + preferred: explicitAssignee, + exclude: existingState?.returnAssignee ?? null, + }); + if (!participant) { + throw unprocessable(`No eligible ${nextStage.type} participant is configured for this issue`); + } + + buildPendingStagePatch({ + patch, + previous: approvedState, + policy: input.policy, + stage: nextStage, + participant, + returnAssignee: existingState?.returnAssignee ?? currentAssignee ?? actor, + }); return { patch, decision: { - stageId: currentStage.id, - stageType: currentStage.type, + stageId: activeStage.id, + stageType: activeStage.type, outcome: "approved", body: input.commentBody.trim(), }, + workflowControlledAssignment: true, }; } - const participant = selectStageParticipant(nextStage, { - preferred: explicitAssignee, - exclude: existingState?.returnAssignee ?? null, - }); - if (!participant) { - throw unprocessable(`No eligible ${nextStage.type} participant is configured for this issue`); + if (requestedStatus && requestedStatus !== "in_review") { + if (!input.commentBody?.trim()) { + throw unprocessable("Requesting changes requires a comment"); + } + if (!existingState?.returnAssignee) { + throw unprocessable("This execution stage has no return assignee"); + } + patch.status = "in_progress"; + Object.assign(patch, patchForPrincipal(existingState.returnAssignee)); + patch.executionState = buildChangesRequestedState(existingState, activeStage); + return { + patch, + decision: { + stageId: activeStage.id, + stageType: activeStage.type, + outcome: "changes_requested", + body: input.commentBody.trim(), + }, + workflowControlledAssignment: true, + }; } - - patch.status = "in_review"; - Object.assign(patch, patchForPrincipal(participant)); - patch.executionState = buildPendingState({ - previous: approvedState, - stage: nextStage, - stageIndex: input.policy.stages.findIndex((stage) => stage.id === nextStage.id), - participant, - returnAssignee: existingState?.returnAssignee ?? currentAssignee, - }); - return { - patch, - decision: { - stageId: currentStage.id, - stageType: currentStage.type, - outcome: "approved", - body: input.commentBody.trim(), - }, - }; } - if (requestedStatus && requestedStatus !== "in_review") { - if (!input.commentBody?.trim()) { - throw unprocessable("Requesting changes requires a comment"); - } - if (!existingState?.returnAssignee) { - throw unprocessable("This execution stage has no return assignee"); - } - patch.status = "in_progress"; - Object.assign(patch, patchForPrincipal(existingState.returnAssignee)); - patch.executionState = buildChangesRequestedState(existingState, currentStage); + if ( + input.issue.status !== "in_review" || + !principalsEqual(currentAssignee, currentParticipant) || + !principalsEqual(existingState?.currentParticipant ?? null, currentParticipant) || + (requestedStatus !== undefined && requestedStatus !== "in_review") || + (requestedAssigneePatchProvided && !principalsEqual(explicitAssignee, currentParticipant)) + ) { + buildPendingStagePatch({ + patch, + previous: existingState, + policy: input.policy, + stage: activeStage, + participant: currentParticipant, + returnAssignee: existingState?.returnAssignee ?? currentAssignee ?? actor, + }); return { patch, - decision: { - stageId: currentStage.id, - stageType: currentStage.type, - outcome: "changes_requested", - body: input.commentBody.trim(), - }, + workflowControlledAssignment: true, }; } return { patch }; } - if (requestedStatus !== "done") { + const shouldStartWorkflow = + requestedStatus === "done" || + requestedStatus === "in_review" || + (input.issue.status === "in_review" && existingState == null); + + if (!shouldStartWorkflow) { return { patch }; } @@ -333,14 +387,16 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra throw unprocessable(`No eligible ${pendingStage.type} participant is configured for this issue`); } - patch.status = "in_review"; - Object.assign(patch, patchForPrincipal(participant)); - patch.executionState = buildPendingState({ + buildPendingStagePatch({ + patch, previous: existingState, + policy: input.policy, stage: pendingStage, - stageIndex: input.policy.stages.findIndex((stage) => stage.id === pendingStage.id), participant, returnAssignee, }); - return { patch }; + return { + patch, + workflowControlledAssignment: true, + }; } From 8894520ed0fa47f103a5d5cc3dbd3b541795cf4a Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:29:39 -0500 Subject: [PATCH 21/74] comment wake batching test --- .../heartbeat-comment-wake-batching.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts index 5c5d8805..f1b3fbd3 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -488,6 +488,23 @@ describe("heartbeat comment wake batching", () => { expect(firstRun).not.toBeNull(); await waitFor(() => gateway.getAgentPayloads().length === 1); + const firstPayload = gateway.getAgentPayloads()[0] ?? {}; + expect(firstPayload.paperclip).toMatchObject({ + wake: { + reason: "issue_assigned", + issue: { + id: issueId, + identifier: `${issuePrefix}-1`, + title: "Require a comment", + status: "todo", + priority: "medium", + }, + commentIds: [], + }, + }); + expect(String(firstPayload.message ?? "")).toContain("## Paperclip Wake Payload"); + expect(String(firstPayload.message ?? "")).toContain("Do not switch to another issue until you have handled this wake."); + expect(String(firstPayload.message ?? "")).toContain(`${issuePrefix}-1 Require a comment`); gateway.releaseFirstWait(); await waitFor(async () => { const runs = await db From 3baebee2dfd3335631342a8a1a20a4a843eb1ca5 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 09:13:32 -0500 Subject: [PATCH 22/74] Track blocker and review activity events --- .../issue-activity-events-routes.test.ts | 244 +++++++++++++++ server/src/routes/issues.ts | 128 +++++++- ui/src/components/ActivityRow.tsx | 67 +--- ui/src/lib/activity-format.test.ts | 60 ++++ ui/src/lib/activity-format.ts | 289 ++++++++++++++++++ ui/src/pages/IssueDetail.tsx | 82 +---- 6 files changed, 719 insertions(+), 151 deletions(-) create mode 100644 server/src/__tests__/issue-activity-events-routes.test.ts create mode 100644 ui/src/lib/activity-format.test.ts create mode 100644 ui/src/lib/activity-format.ts diff --git a/server/src/__tests__/issue-activity-events-routes.test.ts b/server/src/__tests__/issue-activity-events-routes.test.ts new file mode 100644 index 00000000..d4e99a8e --- /dev/null +++ b/server/src/__tests__/issue-activity-events-routes.test.ts @@ -0,0 +1,244 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { errorHandler } from "../middleware/index.js"; +import { issueRoutes } from "../routes/issues.js"; +import { normalizeIssueExecutionPolicy } from "../services/issue-execution-policy.ts"; + +const mockIssueService = vi.hoisted(() => ({ + getById: vi.fn(), + assertCheckoutOwner: vi.fn(), + update: vi.fn(), + addComment: vi.fn(), + findMentionedAgents: vi.fn(), + getRelationSummaries: vi.fn(), + listWakeableBlockedDependents: vi.fn(), + getWakeableParentAfterChildCompletion: vi.fn(), +})); + +const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined)); + +vi.mock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(async () => false), + hasPermission: vi.fn(async () => false), + }), + agentService: () => ({ + getById: vi.fn(async () => null), + }), + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), + goalService: () => ({}), + heartbeatService: () => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + getRun: vi.fn(async () => null), + getActiveRunForAgent: vi.fn(async () => null), + cancelRun: vi.fn(async () => null), + }), + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), +})); + +function createApp() { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as any).actor = { + type: "board", + userId: "local-board", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: false, + }; + next(); + }); + app.use("/api", issueRoutes({} as any, {} as any)); + app.use(errorHandler); + return app; +} + +function makeIssue() { + return { + id: "11111111-1111-4111-8111-111111111111", + companyId: "company-1", + status: "todo", + assigneeAgentId: "22222222-2222-4222-8222-222222222222", + assigneeUserId: null, + createdByUserId: "local-board", + identifier: "PAP-580", + title: "Activity event issue", + executionPolicy: null, + executionState: null, + }; +} + +describe("issue activity event routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockIssueService.assertCheckoutOwner.mockResolvedValue({ adoptedFromRunId: null }); + mockIssueService.findMentionedAgents.mockResolvedValue([]); + mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] }); + mockIssueService.listWakeableBlockedDependents.mockResolvedValue([]); + mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); + }); + + it("logs blocker activity with added and removed issue summaries", async () => { + const issue = makeIssue(); + mockIssueService.getById.mockResolvedValue(issue); + mockIssueService.getRelationSummaries + .mockResolvedValueOnce({ + blockedBy: [ + { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + identifier: "PAP-10", + title: "Old blocker", + status: "todo", + priority: "medium", + assigneeAgentId: null, + assigneeUserId: null, + }, + ], + blocks: [], + }) + .mockResolvedValueOnce({ + blockedBy: [ + { + id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + identifier: "PAP-11", + title: "New blocker", + status: "todo", + priority: "medium", + assigneeAgentId: null, + assigneeUserId: null, + }, + ], + blocks: [], + }); + mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...issue, + ...patch, + updatedAt: new Date(), + })); + + const res = await request(createApp()) + .patch("/api/issues/11111111-1111-4111-8111-111111111111") + .send({ blockedByIssueIds: ["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"] }); + + expect(res.status).toBe(200); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.blockers_updated", + details: expect.objectContaining({ + addedBlockedByIssueIds: ["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"], + removedBlockedByIssueIds: ["aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"], + addedBlockedByIssues: [ + { + id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + identifier: "PAP-11", + title: "New blocker", + }, + ], + removedBlockedByIssues: [ + { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + identifier: "PAP-10", + title: "Old blocker", + }, + ], + }), + }), + ); + }); + + it("logs explicit reviewer and approver activity when execution policy participants change", async () => { + const existingPolicy = normalizeIssueExecutionPolicy({ + stages: [ + { + id: "11111111-1111-4111-8111-111111111111", + type: "review", + participants: [{ type: "agent", agentId: "11111111-2222-4333-8444-555555555555" }], + }, + { + id: "22222222-2222-4222-8222-222222222222", + type: "approval", + participants: [{ type: "agent", agentId: "66666666-7777-4888-8999-aaaaaaaaaaaa" }], + }, + ], + })!; + const nextPolicy = normalizeIssueExecutionPolicy({ + stages: [ + { + id: "11111111-1111-4111-8111-111111111111", + type: "review", + participants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff" }], + }, + { + id: "22222222-2222-4222-8222-222222222222", + type: "approval", + participants: [{ type: "user", userId: "local-board" }], + }, + ], + })!; + const issue = { + ...makeIssue(), + executionPolicy: existingPolicy, + }; + mockIssueService.getById.mockResolvedValue(issue); + mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...issue, + ...patch, + executionPolicy: patch.executionPolicy, + updatedAt: new Date(), + })); + + const res = await request(createApp()) + .patch("/api/issues/11111111-1111-4111-8111-111111111111") + .send({ executionPolicy: nextPolicy }); + + expect(res.status).toBe(200); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.reviewers_updated", + details: expect.objectContaining({ + participants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], + addedParticipants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], + removedParticipants: [{ type: "agent", agentId: "11111111-2222-4333-8444-555555555555", userId: null }], + }), + }), + ); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.approvers_updated", + details: expect.objectContaining({ + participants: [{ type: "user", agentId: null, userId: "local-board" }], + addedParticipants: [{ type: "user", agentId: null, userId: "local-board" }], + removedParticipants: [{ type: "agent", agentId: "66666666-7777-4888-8999-aaaaaaaaaaaa", userId: null }], + }), + }), + ); + }); +}); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index f72b19cd..561c1331 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -68,6 +68,16 @@ const updateIssueRouteSchema = updateIssueSchema.extend({ }); type ParsedExecutionState = NonNullable>; +type NormalizedExecutionPolicy = NonNullable>; +type ActivityIssueRelationSummary = { + id: string; + identifier: string | null; + title: string; +}; +type ActivityExecutionParticipant = Pick< + NormalizedExecutionPolicy["stages"][number]["participants"][number], + "type" | "agentId" | "userId" +>; type ExecutionStageWakeContext = { wakeRole: "reviewer" | "approver" | "executor"; stageId: string | null; @@ -102,6 +112,59 @@ function buildExecutionStageWakeContext(input: { }; } +function summarizeIssueRelationForActivity(relation: { + id: string; + identifier: string | null; + title: string; +}): ActivityIssueRelationSummary { + return { + id: relation.id, + identifier: relation.identifier, + title: relation.title, + }; +} + +function activityExecutionParticipantKey(participant: ActivityExecutionParticipant): string { + return participant.type === "agent" ? `agent:${participant.agentId}` : `user:${participant.userId}`; +} + +function summarizeExecutionParticipants( + policy: NormalizedExecutionPolicy | null, + stageType: NormalizedExecutionPolicy["stages"][number]["type"], +): ActivityExecutionParticipant[] { + const stage = policy?.stages.find((candidate) => candidate.type === stageType); + return ( + stage?.participants.map((participant) => ({ + type: participant.type, + agentId: participant.agentId ?? null, + userId: participant.userId ?? null, + })) ?? [] + ); +} + +function diffExecutionParticipants( + previousPolicy: NormalizedExecutionPolicy | null, + nextPolicy: NormalizedExecutionPolicy | null, + stageType: NormalizedExecutionPolicy["stages"][number]["type"], +) { + const previousParticipants = summarizeExecutionParticipants(previousPolicy, stageType); + const nextParticipants = summarizeExecutionParticipants(nextPolicy, stageType); + const previousByKey = new Map(previousParticipants.map((participant) => [ + activityExecutionParticipantKey(participant), + participant, + ])); + const nextByKey = new Map(nextParticipants.map((participant) => [ + activityExecutionParticipantKey(participant), + participant, + ])); + + return { + participants: nextParticipants, + addedParticipants: nextParticipants.filter((participant) => !previousByKey.has(activityExecutionParticipantKey(participant))), + removedParticipants: previousParticipants.filter((participant) => !nextByKey.has(activityExecutionParticipantKey(participant))), + }; +} + function buildExecutionStageWakeup(input: { issueId: string; previousState: ParsedExecutionState | null; @@ -1202,9 +1265,10 @@ export function issueRoutes( } const actor = getActorInfo(req); + const executionPolicy = normalizeIssueExecutionPolicy(req.body.executionPolicy); const issue = await svc.create(companyId, { ...req.body, - executionPolicy: normalizeIssueExecutionPolicy(req.body.executionPolicy), + executionPolicy, createdByAgentId: actor.agentId, createdByUserId: actor.actorType === "user" ? actor.actorId : null, }); @@ -1309,13 +1373,15 @@ export function issueRoutes( if (req.body.executionPolicy !== undefined) { updateFields.executionPolicy = normalizeIssueExecutionPolicy(req.body.executionPolicy); } + const previousExecutionPolicy = normalizeIssueExecutionPolicy(existing.executionPolicy ?? null); + const nextExecutionPolicy = + updateFields.executionPolicy !== undefined + ? (updateFields.executionPolicy as NormalizedExecutionPolicy | null) + : previousExecutionPolicy; const transition = applyIssueExecutionPolicyTransition({ issue: existing, - policy: - updateFields.executionPolicy !== undefined - ? (updateFields.executionPolicy as NonNullable | null) - : normalizeIssueExecutionPolicy(existing.executionPolicy ?? null), + policy: nextExecutionPolicy, requestedStatus: typeof updateFields.status === "string" ? updateFields.status : undefined, requestedAssigneePatch: { assigneeAgentId: @@ -1430,8 +1496,9 @@ export function issueRoutes( return; } let issueResponse: typeof issue & { blockedBy?: unknown; blocks?: unknown } = issue; + let updatedRelations: Awaited> | null = null; if (issue && Array.isArray(req.body.blockedByIssueIds)) { - const updatedRelations = await svc.getRelationSummaries(issue.id); + updatedRelations = await svc.getRelationSummaries(issue.id); issueResponse = { ...issue, blockedBy: updatedRelations.blockedBy, @@ -1488,6 +1555,8 @@ export function issueRoutes( const nextBlockedByIds = new Set(req.body.blockedByIssueIds as string[]); const addedBlockedByIssueIds = [...nextBlockedByIds].filter((candidate) => !previousBlockedByIds.has(candidate)); const removedBlockedByIssueIds = [...previousBlockedByIds].filter((candidate) => !nextBlockedByIds.has(candidate)); + const nextBlockedByRelations = updatedRelations?.blockedBy ?? []; + const previousBlockedByRelations = existingRelations?.blockedBy ?? []; if (addedBlockedByIssueIds.length > 0 || removedBlockedByIssueIds.length > 0) { await logActivity(db, { companyId: issue.companyId, @@ -1503,11 +1572,58 @@ export function issueRoutes( blockedByIssueIds: req.body.blockedByIssueIds, addedBlockedByIssueIds, removedBlockedByIssueIds, + blockedByIssues: nextBlockedByRelations.map(summarizeIssueRelationForActivity), + addedBlockedByIssues: nextBlockedByRelations + .filter((relation) => addedBlockedByIssueIds.includes(relation.id)) + .map(summarizeIssueRelationForActivity), + removedBlockedByIssues: previousBlockedByRelations + .filter((relation) => removedBlockedByIssueIds.includes(relation.id)) + .map(summarizeIssueRelationForActivity), }, }); } } + const reviewerChanges = diffExecutionParticipants(previousExecutionPolicy, nextExecutionPolicy, "review"); + if (reviewerChanges.addedParticipants.length > 0 || reviewerChanges.removedParticipants.length > 0) { + await logActivity(db, { + companyId: issue.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.reviewers_updated", + entityType: "issue", + entityId: issue.id, + details: { + identifier: issue.identifier, + participants: reviewerChanges.participants, + addedParticipants: reviewerChanges.addedParticipants, + removedParticipants: reviewerChanges.removedParticipants, + }, + }); + } + + const approverChanges = diffExecutionParticipants(previousExecutionPolicy, nextExecutionPolicy, "approval"); + if (approverChanges.addedParticipants.length > 0 || approverChanges.removedParticipants.length > 0) { + await logActivity(db, { + companyId: issue.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.approvers_updated", + entityType: "issue", + entityId: issue.id, + details: { + identifier: issue.identifier, + participants: approverChanges.participants, + addedParticipants: approverChanges.addedParticipants, + removedParticipants: approverChanges.removedParticipants, + }, + }); + } + if (issue.status === "done" && existing.status !== "done") { const tc = getTelemetryClient(); if (tc && actor.agentId) { diff --git a/ui/src/components/ActivityRow.tsx b/ui/src/components/ActivityRow.tsx index ebfe23c5..dbe88785 100644 --- a/ui/src/components/ActivityRow.tsx +++ b/ui/src/components/ActivityRow.tsx @@ -2,72 +2,9 @@ import { Link } from "@/lib/router"; import { Identity } from "./Identity"; import { timeAgo } from "../lib/timeAgo"; import { cn } from "../lib/utils"; +import { formatActivityVerb } from "../lib/activity-format"; import { deriveProjectUrlKey, type ActivityEvent, type Agent } from "@paperclipai/shared"; -const ACTION_VERBS: Record = { - "issue.created": "created", - "issue.updated": "updated", - "issue.checked_out": "checked out", - "issue.released": "released", - "issue.comment_added": "commented on", - "issue.attachment_added": "attached file to", - "issue.attachment_removed": "removed attachment from", - "issue.document_created": "created document for", - "issue.document_updated": "updated document on", - "issue.document_deleted": "deleted document from", - "issue.commented": "commented on", - "issue.deleted": "deleted", - "agent.created": "created", - "agent.updated": "updated", - "agent.paused": "paused", - "agent.resumed": "resumed", - "agent.terminated": "terminated", - "agent.key_created": "created API key for", - "agent.budget_updated": "updated budget for", - "agent.runtime_session_reset": "reset session for", - "heartbeat.invoked": "invoked heartbeat for", - "heartbeat.cancelled": "cancelled heartbeat for", - "approval.created": "requested approval", - "approval.approved": "approved", - "approval.rejected": "rejected", - "project.created": "created", - "project.updated": "updated", - "project.deleted": "deleted", - "goal.created": "created", - "goal.updated": "updated", - "goal.deleted": "deleted", - "cost.reported": "reported cost for", - "cost.recorded": "recorded cost for", - "company.created": "created company", - "company.updated": "updated company", - "company.archived": "archived", - "company.budget_updated": "updated budget for", -}; - -function humanizeValue(value: unknown): string { - if (typeof value !== "string") return String(value ?? "none"); - return value.replace(/_/g, " "); -} - -function formatVerb(action: string, details?: Record | null): string { - if (action === "issue.updated" && details) { - const previous = (details._previous ?? {}) as Record; - if (details.status !== undefined) { - const from = previous.status; - return from - ? `changed status from ${humanizeValue(from)} to ${humanizeValue(details.status)} on` - : `changed status to ${humanizeValue(details.status)} on`; - } - if (details.priority !== undefined) { - const from = previous.priority; - return from - ? `changed priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)} on` - : `changed priority to ${humanizeValue(details.priority)} on`; - } - } - return ACTION_VERBS[action] ?? action.replace(/[._]/g, " "); -} - function entityLink(entityType: string, entityId: string, name?: string | null): string | null { switch (entityType) { case "issue": return `/issues/${name ?? entityId}`; @@ -88,7 +25,7 @@ interface ActivityRowProps { } export function ActivityRow({ event, agentMap, entityNameMap, entityTitleMap, className }: ActivityRowProps) { - const verb = formatVerb(event.action, event.details); + const verb = formatActivityVerb(event.action, event.details, { agentMap }); const isHeartbeatEvent = event.entityType === "heartbeat_run"; const heartbeatAgentId = isHeartbeatEvent diff --git a/ui/src/lib/activity-format.test.ts b/ui/src/lib/activity-format.test.ts new file mode 100644 index 00000000..25f2d37a --- /dev/null +++ b/ui/src/lib/activity-format.test.ts @@ -0,0 +1,60 @@ +import type { Agent } from "@paperclipai/shared"; +import { describe, expect, it } from "vitest"; +import { formatActivityVerb, formatIssueActivityAction } from "./activity-format"; + +describe("activity formatting", () => { + const agentMap = new Map([ + ["agent-reviewer", { id: "agent-reviewer", name: "Reviewer Bot" } as Agent], + ["agent-approver", { id: "agent-approver", name: "Approver Bot" } as Agent], + ]); + + it("formats blocker activity using linked issue identifiers", () => { + const details = { + addedBlockedByIssues: [ + { id: "issue-2", identifier: "PAP-22", title: "Blocked task" }, + ], + removedBlockedByIssues: [], + }; + + expect(formatActivityVerb("issue.blockers_updated", details)).toBe("added blocker PAP-22 to"); + expect(formatIssueActivityAction("issue.blockers_updated", details)).toBe("added blocker PAP-22"); + }); + + it("formats reviewer activity using agent names", () => { + const details = { + addedParticipants: [ + { type: "agent", agentId: "agent-reviewer", userId: null }, + ], + removedParticipants: [], + }; + + expect(formatActivityVerb("issue.reviewers_updated", details, { agentMap })).toBe("added reviewer Reviewer Bot to"); + expect(formatIssueActivityAction("issue.reviewers_updated", details, { agentMap })).toBe("added reviewer Reviewer Bot"); + }); + + it("formats approver removals using user-aware labels", () => { + const details = { + addedParticipants: [], + removedParticipants: [ + { type: "user", agentId: null, userId: "local-board" }, + ], + }; + + expect(formatActivityVerb("issue.approvers_updated", details)).toBe("removed approver Board from"); + expect(formatIssueActivityAction("issue.approvers_updated", details)).toBe("removed approver Board"); + }); + + it("falls back to updated wording when reviewers are both added and removed", () => { + const details = { + addedParticipants: [ + { type: "agent", agentId: "agent-reviewer", userId: null }, + ], + removedParticipants: [ + { type: "agent", agentId: "agent-approver", userId: null }, + ], + }; + + expect(formatActivityVerb("issue.reviewers_updated", details, { agentMap })).toBe("updated reviewers on"); + expect(formatIssueActivityAction("issue.reviewers_updated", details, { agentMap })).toBe("updated reviewers"); + }); +}); diff --git a/ui/src/lib/activity-format.ts b/ui/src/lib/activity-format.ts new file mode 100644 index 00000000..bfebb113 --- /dev/null +++ b/ui/src/lib/activity-format.ts @@ -0,0 +1,289 @@ +import type { Agent } from "@paperclipai/shared"; + +type ActivityDetails = Record | null | undefined; + +type ActivityParticipant = { + type: "agent" | "user"; + agentId?: string | null; + userId?: string | null; +}; + +type ActivityIssueReference = { + id?: string | null; + identifier?: string | null; + title?: string | null; +}; + +interface ActivityFormatOptions { + agentMap?: Map; + currentUserId?: string | null; +} + +const ACTIVITY_ROW_VERBS: Record = { + "issue.created": "created", + "issue.updated": "updated", + "issue.checked_out": "checked out", + "issue.released": "released", + "issue.comment_added": "commented on", + "issue.attachment_added": "attached file to", + "issue.attachment_removed": "removed attachment from", + "issue.document_created": "created document for", + "issue.document_updated": "updated document on", + "issue.document_deleted": "deleted document from", + "issue.commented": "commented on", + "issue.deleted": "deleted", + "agent.created": "created", + "agent.updated": "updated", + "agent.paused": "paused", + "agent.resumed": "resumed", + "agent.terminated": "terminated", + "agent.key_created": "created API key for", + "agent.budget_updated": "updated budget for", + "agent.runtime_session_reset": "reset session for", + "heartbeat.invoked": "invoked heartbeat for", + "heartbeat.cancelled": "cancelled heartbeat for", + "approval.created": "requested approval", + "approval.approved": "approved", + "approval.rejected": "rejected", + "project.created": "created", + "project.updated": "updated", + "project.deleted": "deleted", + "goal.created": "created", + "goal.updated": "updated", + "goal.deleted": "deleted", + "cost.reported": "reported cost for", + "cost.recorded": "recorded cost for", + "company.created": "created company", + "company.updated": "updated company", + "company.archived": "archived", + "company.budget_updated": "updated budget for", +}; + +const ISSUE_ACTIVITY_LABELS: Record = { + "issue.created": "created the issue", + "issue.updated": "updated the issue", + "issue.checked_out": "checked out the issue", + "issue.released": "released the issue", + "issue.comment_added": "added a comment", + "issue.feedback_vote_saved": "saved feedback on an AI output", + "issue.attachment_added": "added an attachment", + "issue.attachment_removed": "removed an attachment", + "issue.document_created": "created a document", + "issue.document_updated": "updated a document", + "issue.document_deleted": "deleted a document", + "issue.deleted": "deleted the issue", + "agent.created": "created an agent", + "agent.updated": "updated the agent", + "agent.paused": "paused the agent", + "agent.resumed": "resumed the agent", + "agent.terminated": "terminated the agent", + "heartbeat.invoked": "invoked a heartbeat", + "heartbeat.cancelled": "cancelled a heartbeat", + "approval.created": "requested approval", + "approval.approved": "approved", + "approval.rejected": "rejected", +}; + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function humanizeValue(value: unknown): string { + if (typeof value !== "string") return String(value ?? "none"); + return value.replace(/_/g, " "); +} + +function isActivityParticipant(value: unknown): value is ActivityParticipant { + const record = asRecord(value); + if (!record) return false; + return record.type === "agent" || record.type === "user"; +} + +function isActivityIssueReference(value: unknown): value is ActivityIssueReference { + return asRecord(value) !== null; +} + +function readParticipants(details: ActivityDetails, key: string): ActivityParticipant[] { + const value = details?.[key]; + if (!Array.isArray(value)) return []; + return value.filter(isActivityParticipant); +} + +function readIssueReferences(details: ActivityDetails, key: string): ActivityIssueReference[] { + const value = details?.[key]; + if (!Array.isArray(value)) return []; + return value.filter(isActivityIssueReference); +} + +function formatUserLabel(userId: string | null | undefined, currentUserId?: string | null): string { + if (!userId || userId === "local-board") return "Board"; + if (currentUserId && userId === currentUserId) return "You"; + return `user ${userId.slice(0, 5)}`; +} + +function formatParticipantLabel(participant: ActivityParticipant, options: ActivityFormatOptions): string { + if (participant.type === "agent") { + const agentId = participant.agentId ?? ""; + return options.agentMap?.get(agentId)?.name ?? "agent"; + } + return formatUserLabel(participant.userId, options.currentUserId); +} + +function formatIssueReferenceLabel(reference: ActivityIssueReference): string { + if (reference.identifier) return reference.identifier; + if (reference.title) return reference.title; + if (reference.id) return reference.id.slice(0, 8); + return "issue"; +} + +function formatChangedEntityLabel( + singular: string, + plural: string, + labels: string[], +): string { + if (labels.length <= 0) return plural; + if (labels.length === 1) return `${singular} ${labels[0]}`; + return `${labels.length} ${plural}`; +} + +function formatIssueUpdatedVerb(details: ActivityDetails): string | null { + if (!details) return null; + const previous = asRecord(details._previous) ?? {}; + if (details.status !== undefined) { + const from = previous.status; + return from + ? `changed status from ${humanizeValue(from)} to ${humanizeValue(details.status)} on` + : `changed status to ${humanizeValue(details.status)} on`; + } + if (details.priority !== undefined) { + const from = previous.priority; + return from + ? `changed priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)} on` + : `changed priority to ${humanizeValue(details.priority)} on`; + } + return null; +} + +function formatIssueUpdatedAction(details: ActivityDetails): string | null { + if (!details) return null; + const previous = asRecord(details._previous) ?? {}; + const parts: string[] = []; + + if (details.status !== undefined) { + const from = previous.status; + parts.push( + from + ? `changed the status from ${humanizeValue(from)} to ${humanizeValue(details.status)}` + : `changed the status to ${humanizeValue(details.status)}`, + ); + } + if (details.priority !== undefined) { + const from = previous.priority; + parts.push( + from + ? `changed the priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)}` + : `changed the priority to ${humanizeValue(details.priority)}`, + ); + } + if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) { + parts.push(details.assigneeAgentId || details.assigneeUserId ? "assigned the issue" : "unassigned the issue"); + } + if (details.title !== undefined) parts.push("updated the title"); + if (details.description !== undefined) parts.push("updated the description"); + + return parts.length > 0 ? parts.join(", ") : null; +} + +function formatStructuredIssueChange(input: { + action: string; + details: ActivityDetails; + options: ActivityFormatOptions; + forIssueDetail: boolean; +}): string | null { + const details = input.details; + if (!details) return null; + + if (input.action === "issue.blockers_updated") { + const added = readIssueReferences(details, "addedBlockedByIssues").map(formatIssueReferenceLabel); + const removed = readIssueReferences(details, "removedBlockedByIssues").map(formatIssueReferenceLabel); + if (added.length > 0 && removed.length === 0) { + const changed = formatChangedEntityLabel("blocker", "blockers", added); + return input.forIssueDetail ? `added ${changed}` : `added ${changed} to`; + } + if (removed.length > 0 && added.length === 0) { + const changed = formatChangedEntityLabel("blocker", "blockers", removed); + return input.forIssueDetail ? `removed ${changed}` : `removed ${changed} from`; + } + return input.forIssueDetail ? "updated blockers" : "updated blockers on"; + } + + if (input.action === "issue.reviewers_updated" || input.action === "issue.approvers_updated") { + const added = readParticipants(details, "addedParticipants").map((participant) => formatParticipantLabel(participant, input.options)); + const removed = readParticipants(details, "removedParticipants").map((participant) => formatParticipantLabel(participant, input.options)); + const singular = input.action === "issue.reviewers_updated" ? "reviewer" : "approver"; + const plural = input.action === "issue.reviewers_updated" ? "reviewers" : "approvers"; + if (added.length > 0 && removed.length === 0) { + const changed = formatChangedEntityLabel(singular, plural, added); + return input.forIssueDetail ? `added ${changed}` : `added ${changed} to`; + } + if (removed.length > 0 && added.length === 0) { + const changed = formatChangedEntityLabel(singular, plural, removed); + return input.forIssueDetail ? `removed ${changed}` : `removed ${changed} from`; + } + return input.forIssueDetail ? `updated ${plural}` : `updated ${plural} on`; + } + + return null; +} + +export function formatActivityVerb( + action: string, + details?: Record | null, + options: ActivityFormatOptions = {}, +): string { + if (action === "issue.updated") { + const issueUpdatedVerb = formatIssueUpdatedVerb(details); + if (issueUpdatedVerb) return issueUpdatedVerb; + } + + const structuredChange = formatStructuredIssueChange({ + action, + details, + options, + forIssueDetail: false, + }); + if (structuredChange) return structuredChange; + + return ACTIVITY_ROW_VERBS[action] ?? action.replace(/[._]/g, " "); +} + +export function formatIssueActivityAction( + action: string, + details?: Record | null, + options: ActivityFormatOptions = {}, +): string { + if (action === "issue.updated") { + const issueUpdatedAction = formatIssueUpdatedAction(details); + if (issueUpdatedAction) return issueUpdatedAction; + } + + const structuredChange = formatStructuredIssueChange({ + action, + details, + options, + forIssueDetail: true, + }); + if (structuredChange) return structuredChange; + + if ( + (action === "issue.document_created" || action === "issue.document_updated" || action === "issue.document_deleted") && + details + ) { + const key = typeof details.key === "string" ? details.key : "document"; + const title = typeof details.title === "string" && details.title ? ` (${details.title})` : ""; + return `${ISSUE_ACTIVITY_LABELS[action] ?? action} ${key}${title}`; + } + + return ISSUE_ACTIVITY_LABELS[action] ?? action.replace(/[._]/g, " "); +} diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 48451738..fcaa09cf 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -58,6 +58,7 @@ import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { formatIssueActivityAction } from "@/lib/activity-format"; import { Activity as ActivityIcon, Check, @@ -94,43 +95,8 @@ type IssueDetailComment = (IssueComment | OptimisticIssueComment) & { queueTargetRunId?: string | null; }; -const ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS = 3000; -const IDLE_ISSUE_RUN_POLL_INTERVAL_MS = 30000; -const ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 5000; -const IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 30000; - -const ACTION_LABELS: Record = { - "issue.created": "created the issue", - "issue.updated": "updated the issue", - "issue.checked_out": "checked out the issue", - "issue.released": "released the issue", - "issue.comment_added": "added a comment", - "issue.feedback_vote_saved": "saved feedback on an AI output", - "issue.attachment_added": "added an attachment", - "issue.attachment_removed": "removed an attachment", - "issue.document_created": "created a document", - "issue.document_updated": "updated a document", - "issue.document_deleted": "deleted a document", - "issue.deleted": "deleted the issue", - "agent.created": "created an agent", - "agent.updated": "updated the agent", - "agent.paused": "paused the agent", - "agent.resumed": "resumed the agent", - "agent.terminated": "terminated the agent", - "heartbeat.invoked": "invoked a heartbeat", - "heartbeat.cancelled": "cancelled a heartbeat", - "approval.created": "requested approval", - "approval.approved": "approved", - "approval.rejected": "rejected", -}; - const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos"; -function humanizeValue(value: unknown): string { - if (typeof value !== "string") return String(value ?? "none"); - return value.replace(/_/g, " "); -} - function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; @@ -180,50 +146,6 @@ function titleizeFilename(input: string) { .join(" "); } -function formatAction(action: string, details?: Record | null): string { - if (action === "issue.updated" && details) { - const previous = (details._previous ?? {}) as Record; - const parts: string[] = []; - - if (details.status !== undefined) { - const from = previous.status; - parts.push( - from - ? `changed the status from ${humanizeValue(from)} to ${humanizeValue(details.status)}` - : `changed the status to ${humanizeValue(details.status)}` - ); - } - if (details.priority !== undefined) { - const from = previous.priority; - parts.push( - from - ? `changed the priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)}` - : `changed the priority to ${humanizeValue(details.priority)}` - ); - } - if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) { - parts.push( - details.assigneeAgentId || details.assigneeUserId - ? "assigned the issue" - : "unassigned the issue", - ); - } - if (details.title !== undefined) parts.push("updated the title"); - if (details.description !== undefined) parts.push("updated the description"); - - if (parts.length > 0) return parts.join(", "); - } - if ( - (action === "issue.document_created" || action === "issue.document_updated" || action === "issue.document_deleted") && - details - ) { - const key = typeof details.key === "string" ? details.key : "document"; - const title = typeof details.title === "string" && details.title ? ` (${details.title})` : ""; - return `${ACTION_LABELS[action] ?? action} ${key}${title}`; - } - return ACTION_LABELS[action] ?? action.replace(/[._]/g, " "); -} - function mergeOptimisticFeedbackVote( previousVotes: FeedbackVote[] | undefined, nextVote: { @@ -1901,7 +1823,7 @@ export function IssueDetail() { {activity.slice(0, 20).map((evt) => (
- {formatAction(evt.action, evt.details)} + {formatIssueActivityAction(evt.action, evt.details, { agentMap, currentUserId })} {relativeTime(evt.createdAt)}
))} From 26d4cabb2e8a041f15fb1080f421c35777128c21 Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 09:47:02 -0500 Subject: [PATCH 23/74] Persist heartbeat child pid before stdin handoff --- .../adapter-utils/src/server-utils.test.ts | 38 +++++++++++++++++++ packages/adapter-utils/src/server-utils.ts | 25 +++++++----- 2 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 packages/adapter-utils/src/server-utils.test.ts diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts new file mode 100644 index 00000000..62e395b0 --- /dev/null +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -0,0 +1,38 @@ +import { randomUUID } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { runChildProcess } from "./server-utils.js"; + +describe("runChildProcess", () => { + it("waits for onSpawn before sending stdin to the child", async () => { + const spawnDelayMs = 150; + const startedAt = Date.now(); + let onSpawnCompletedAt = 0; + + const result = await runChildProcess( + randomUUID(), + process.execPath, + [ + "-e", + "let data='';process.stdin.setEncoding('utf8');process.stdin.on('data',chunk=>data+=chunk);process.stdin.on('end',()=>process.stdout.write(data));", + ], + { + cwd: process.cwd(), + env: {}, + stdin: "hello from stdin", + timeoutSec: 5, + graceSec: 1, + onLog: async () => {}, + onSpawn: async () => { + await new Promise((resolve) => setTimeout(resolve, spawnDelayMs)); + onSpawnCompletedAt = Date.now(); + }, + }, + ); + const finishedAt = Date.now(); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe("hello from stdin"); + expect(onSpawnCompletedAt).toBeGreaterThanOrEqual(startedAt + spawnDelayMs); + expect(finishedAt - startedAt).toBeGreaterThanOrEqual(spawnDelayMs); + }); +}); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 1c6a2795..83dbe06f 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -1069,16 +1069,12 @@ export async function runChildProcess( }) as ChildProcessWithEvents; const startedAt = new Date().toISOString(); - if (opts.stdin != null && child.stdin) { - child.stdin.write(opts.stdin); - child.stdin.end(); - } - - if (typeof child.pid === "number" && child.pid > 0 && opts.onSpawn) { - void opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => { - onLogError(err, runId, "failed to record child process metadata"); - }); - } + const spawnPersistPromise = + typeof child.pid === "number" && child.pid > 0 && opts.onSpawn + ? opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => { + onLogError(err, runId, "failed to record child process metadata"); + }) + : Promise.resolve(); runningProcesses.set(runId, { child, graceSec: opts.graceSec }); @@ -1116,6 +1112,15 @@ export async function runChildProcess( .catch((err) => onLogError(err, runId, "failed to append stderr log chunk")); }); + const stdin = child.stdin; + if (opts.stdin != null && stdin) { + void spawnPersistPromise.finally(() => { + if (child.killed || stdin.destroyed) return; + stdin.write(opts.stdin as string); + stdin.end(); + }); + } + child.on("error", (err: Error) => { if (timeout) clearTimeout(timeout); runningProcesses.delete(runId); From 27ec1e0c8b13cd881b6a4c794f17b37b56f1ebda Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 16:54:43 -0500 Subject: [PATCH 24/74] Fix execution policy edits on in-review issues --- .../issue-execution-policy-routes.test.ts | 140 ++++++++++++++++++ .../__tests__/issue-execution-policy.test.ts | 115 ++++++++++++++ server/src/services/issue-execution-policy.ts | 60 +++++++- 3 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 server/src/__tests__/issue-execution-policy-routes.test.ts diff --git a/server/src/__tests__/issue-execution-policy-routes.test.ts b/server/src/__tests__/issue-execution-policy-routes.test.ts new file mode 100644 index 00000000..4f8d9fc6 --- /dev/null +++ b/server/src/__tests__/issue-execution-policy-routes.test.ts @@ -0,0 +1,140 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { errorHandler } from "../middleware/index.js"; +import { issueRoutes } from "../routes/issues.js"; +import { normalizeIssueExecutionPolicy } from "../services/issue-execution-policy.ts"; + +const mockIssueService = vi.hoisted(() => ({ + getById: vi.fn(), + assertCheckoutOwner: vi.fn(), + update: vi.fn(), + addComment: vi.fn(), + findMentionedAgents: vi.fn(), + getRelationSummaries: vi.fn(), + listWakeableBlockedDependents: vi.fn(), + getWakeableParentAfterChildCompletion: vi.fn(), +})); + +const mockHeartbeatService = vi.hoisted(() => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + getRun: vi.fn(async () => null), + getActiveRunForAgent: vi.fn(async () => null), + cancelRun: vi.fn(async () => null), +})); + +vi.mock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(async () => false), + hasPermission: vi.fn(async () => false), + }), + agentService: () => ({ + getById: vi.fn(async () => null), + }), + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), + goalService: () => ({}), + heartbeatService: () => mockHeartbeatService, + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: vi.fn(async () => undefined), + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), +})); + +function createApp() { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as any).actor = { + type: "board", + userId: "local-board", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: false, + }; + next(); + }); + app.use("/api", issueRoutes({} as any, {} as any)); + app.use(errorHandler); + return app; +} + +describe("issue execution policy routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockIssueService.assertCheckoutOwner.mockResolvedValue({ adoptedFromRunId: null }); + mockIssueService.findMentionedAgents.mockResolvedValue([]); + mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] }); + mockIssueService.listWakeableBlockedDependents.mockResolvedValue([]); + mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); + }); + + it("does not auto-start execution review when reviewers are added to an already in_review issue", async () => { + const policy = normalizeIssueExecutionPolicy({ + stages: [ + { + id: "11111111-1111-4111-8111-111111111111", + type: "review", + participants: [{ type: "agent", agentId: "33333333-3333-4333-8333-333333333333" }], + }, + ], + })!; + const issue = { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + companyId: "company-1", + status: "in_review", + assigneeAgentId: null, + assigneeUserId: "local-board", + createdByUserId: "local-board", + identifier: "PAP-999", + title: "Execution policy edit", + executionPolicy: null, + executionState: null, + }; + mockIssueService.getById.mockResolvedValue(issue); + mockIssueService.update.mockImplementation(async (_id: string, patch: Record) => ({ + ...issue, + ...patch, + updatedAt: new Date(), + })); + + const res = await request(createApp()) + .patch("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa") + .send({ executionPolicy: policy }); + + expect(res.status).toBe(200); + expect(mockIssueService.update).toHaveBeenCalledWith( + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + expect.objectContaining({ + executionPolicy: policy, + actorAgentId: null, + actorUserId: "local-board", + }), + ); + const updatePatch = mockIssueService.update.mock.calls[0]?.[1] as Record; + expect(updatePatch.status).toBeUndefined(); + expect(updatePatch.assigneeAgentId).toBeUndefined(); + expect(updatePatch.assigneeUserId).toBeUndefined(); + expect(updatePatch.executionState).toBeUndefined(); + expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/__tests__/issue-execution-policy.test.ts b/server/src/__tests__/issue-execution-policy.test.ts index 9f5fb80b..7271b499 100644 --- a/server/src/__tests__/issue-execution-policy.test.ts +++ b/server/src/__tests__/issue-execution-policy.test.ts @@ -778,6 +778,25 @@ describe("issue execution policy transitions", () => { expect(result.patch).toEqual({}); }); + + it("does not auto-start workflow when policy is added to an already in_review issue", () => { + const reviewOnly = reviewOnlyPolicy(); + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_review", + assigneeAgentId: null, + assigneeUserId: boardUserId, + executionPolicy: null, + executionState: null, + }, + policy: reviewOnly, + requestedStatus: undefined, + requestedAssigneePatch: {}, + actor: { userId: boardUserId }, + }); + + expect(result.patch).toEqual({}); + }); }); describe("multi-participant stages", () => { @@ -974,4 +993,100 @@ describe("issue execution policy transitions", () => { expect(result.patch.assigneeUserId).toBe(boardUserId); }); }); + + describe("policy edits while a stage is active", () => { + it("clears the active execution state when its stage is removed from the policy", () => { + const reviewAndApproval = twoStagePolicy(); + const approvalOnly = approvalOnlyPolicy(); + + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionPolicy: reviewAndApproval, + executionState: { + status: "pending", + currentStageId: reviewAndApproval.stages[0].id, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }, + policy: approvalOnly, + requestedStatus: undefined, + requestedAssigneePatch: {}, + actor: { userId: boardUserId }, + }); + + expect(result.patch).toMatchObject({ + status: "in_progress", + assigneeAgentId: coderAgentId, + assigneeUserId: null, + executionState: null, + }); + }); + + it("reassigns the active stage when the current participant is removed", () => { + const policy = makePolicy([ + { + type: "review", + participants: [ + { type: "agent", agentId: qaAgentId }, + { type: "agent", agentId: ctoAgentId }, + ], + }, + ]); + const updatedPolicy = makePolicy([ + { + type: "review", + participants: [{ type: "agent", agentId: ctoAgentId }], + }, + ]); + + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: policy.stages[0].id, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }, + policy: { + ...updatedPolicy, + stages: [{ ...updatedPolicy.stages[0], id: policy.stages[0].id }], + }, + requestedStatus: undefined, + requestedAssigneePatch: {}, + actor: { userId: boardUserId }, + }); + + expect(result.patch).toMatchObject({ + status: "in_review", + assigneeAgentId: ctoAgentId, + assigneeUserId: null, + executionState: { + status: "pending", + currentStageId: policy.stages[0].id, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: ctoAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + }, + }); + }); + }); }); diff --git a/server/src/services/issue-execution-policy.ts b/server/src/services/issue-execution-policy.ts index 3bc21c03..6f4ba7b5 100644 --- a/server/src/services/issue-execution-policy.ts +++ b/server/src/services/issue-execution-policy.ts @@ -145,6 +145,11 @@ function selectStageParticipant( return first ? { type: first.type, agentId: first.agentId ?? null, userId: first.userId ?? null } : null; } +function stageHasParticipant(stage: IssueExecutionStage, participant: IssueExecutionStagePrincipal | null): boolean { + if (!participant) return false; + return stage.participants.some((candidate) => principalsEqual(candidate, participant)); +} + function patchForPrincipal(principal: IssueExecutionStagePrincipal | null) { if (!principal) { return { assigneeAgentId: null, assigneeUserId: null }; @@ -218,6 +223,19 @@ function buildPendingStagePatch(input: { }); } +function clearExecutionStatePatch(input: { + patch: Record; + issueStatus: string; + requestedStatus?: string; + returnAssignee: IssueExecutionStagePrincipal | null; +}) { + input.patch.executionState = null; + if (input.requestedStatus === undefined && input.issueStatus === "in_review" && input.returnAssignee) { + input.patch.status = "in_progress"; + Object.assign(input.patch, patchForPrincipal(input.returnAssignee)); + } +} + export function applyIssueExecutionPolicyTransition(input: TransitionInput): TransitionResult { const patch: Record = {}; const existingState = parseIssueExecutionState(input.issue.executionState); @@ -251,6 +269,16 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra return { patch }; } + if (existingState?.currentStageId && !currentStage) { + clearExecutionStatePatch({ + patch, + issueStatus: input.issue.status, + requestedStatus, + returnAssignee: existingState.returnAssignee, + }); + return { patch }; + } + if (activeStage) { const currentParticipant = existingState?.currentParticipant ?? @@ -261,6 +289,35 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra throw unprocessable(`No eligible ${activeStage.type} participant is configured for this issue`); } + if (!stageHasParticipant(activeStage, currentParticipant)) { + const participant = selectStageParticipant(activeStage, { + preferred: explicitAssignee ?? existingState?.currentParticipant ?? null, + exclude: existingState?.returnAssignee ?? null, + }); + if (!participant) { + clearExecutionStatePatch({ + patch, + issueStatus: input.issue.status, + requestedStatus, + returnAssignee: existingState?.returnAssignee ?? null, + }); + return { patch }; + } + + buildPendingStagePatch({ + patch, + previous: existingState, + policy: input.policy, + stage: activeStage, + participant, + returnAssignee: existingState?.returnAssignee ?? currentAssignee ?? actor, + }); + return { + patch, + workflowControlledAssignment: true, + }; + } + if (principalsEqual(currentParticipant, actor)) { if (requestedStatus === "done") { if (!input.commentBody?.trim()) { @@ -362,8 +419,7 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra const shouldStartWorkflow = requestedStatus === "done" || - requestedStatus === "in_review" || - (input.issue.status === "in_review" && existingState == null); + requestedStatus === "in_review"; if (!shouldStartWorkflow) { return { patch }; From c6779b570f44079a80a360f3c6d6255a92fb55e0 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 6 Apr 2026 21:58:29 -0500 Subject: [PATCH 25/74] feat(ui): add workspace and parent issue grouping to issues list Adds two new groupBy options on the issues page: "Workspace" groups issues by their projectWorkspaceId, and "Parent Issue" groups by parentId. Groups with items sort first; sentinel groups (No Workspace / No Parent) appear last. Creating a new issue from a parent group pre-fills parentId. Co-Authored-By: Paperclip --- ui/src/components/IssuesList.tsx | 58 ++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 4760d360..06718d6e 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -45,7 +45,7 @@ export type IssueViewState = { projects: string[]; sortField: "status" | "priority" | "title" | "created" | "updated"; sortDir: "asc" | "desc"; - groupBy: "status" | "priority" | "assignee" | "none"; + groupBy: "status" | "priority" | "assignee" | "workspace" | "parent" | "none"; viewMode: "list" | "board"; collapsedGroups: string[]; collapsedParents: string[]; @@ -155,6 +155,7 @@ interface Agent { interface ProjectOption { id: string; name: string; + workspaces?: { id: string; name: string }[]; } interface IssuesListProps { @@ -265,6 +266,24 @@ export function IssuesList({ return agents.find((a) => a.id === id)?.name ?? null; }, [agents]); + const workspaceNameMap = useMemo(() => { + const map = new Map(); + for (const project of projects ?? []) { + for (const ws of project.workspaces ?? []) { + map.set(ws.id, ws.name || project.name); + } + } + return map; + }, [projects]); + + const issueTitleMap = useMemo(() => { + const map = new Map(); + for (const issue of issues) { + map.set(issue.id, issue.identifier ? `${issue.identifier}: ${issue.title}` : issue.title); + } + return map; + }, [issues]); + const filtered = useMemo(() => { const sourceIssues = normalizedIssueSearch.length > 0 ? searchedIssues : issues; const filteredByControls = applyFilters(sourceIssues, viewState, currentUserId); @@ -295,6 +314,36 @@ export function IssuesList({ .filter((p) => groups[p]?.length) .map((p) => ({ key: p, label: statusLabel(p), items: groups[p]! })); } + if (viewState.groupBy === "workspace") { + const groups = groupBy(filtered, (i) => i.projectWorkspaceId ?? "__no_workspace"); + return Object.keys(groups) + .sort((a, b) => { + // Groups with items first, "no workspace" last + if (a === "__no_workspace") return 1; + if (b === "__no_workspace") return -1; + return (groups[b]?.length ?? 0) - (groups[a]?.length ?? 0); + }) + .map((key) => ({ + key, + label: key === "__no_workspace" ? "No Workspace" : (workspaceNameMap.get(key) ?? key.slice(0, 8)), + items: groups[key]!, + })); + } + if (viewState.groupBy === "parent") { + const groups = groupBy(filtered, (i) => i.parentId ?? "__no_parent"); + return Object.keys(groups) + .sort((a, b) => { + // Groups with items first, "no parent" last + if (a === "__no_parent") return 1; + if (b === "__no_parent") return -1; + return (groups[b]?.length ?? 0) - (groups[a]?.length ?? 0); + }) + .map((key) => ({ + key, + label: key === "__no_parent" ? "No Parent" : (issueTitleMap.get(key) ?? key.slice(0, 8)), + items: groups[key]!, + })); + } // assignee const groups = groupBy( filtered, @@ -310,7 +359,7 @@ export function IssuesList({ : (agentName(key) ?? key.slice(0, 8)), items: groups[key]!, })); - }, [filtered, viewState.groupBy, agents, agentName, currentUserId]); + }, [filtered, viewState.groupBy, agents, agentName, currentUserId, workspaceNameMap, issueTitleMap]); const newIssueDefaults = useCallback((groupKey?: string) => { const defaults: Record = {}; @@ -322,6 +371,9 @@ export function IssuesList({ if (groupKey.startsWith("__user:")) defaults.assigneeUserId = groupKey.slice("__user:".length); else defaults.assigneeAgentId = groupKey; } + else if (viewState.groupBy === "parent" && groupKey !== "__no_parent") { + defaults.parentId = groupKey; + } } return defaults; }, [projectId, viewState.groupBy]); @@ -605,6 +657,8 @@ export function IssuesList({ ["status", "Status"], ["priority", "Priority"], ["assignee", "Assignee"], + ["workspace", "Workspace"], + ["parent", "Parent Issue"], ["none", "None"], ] as const).map(([value, label]) => ( -
- - { - setIssueSearch(e.target.value); - onSearchChange?.(e.target.value); - }} - placeholder="Search issues..." - className="pl-7 text-xs sm:text-sm" - aria-label="Search issues" - /> -
+ { + setIssueSearch(nextSearch); + onSearchChange?.(nextSearch); + }} + />
From 1cbb0a5e34e801aa8576cc5ab5e110d43c1d7a0d Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 7 Apr 2026 16:33:37 -0500 Subject: [PATCH 27/74] Add execution workspace issues tab --- ui/src/App.tsx | 4 + ui/src/lib/company-routes.test.ts | 7 + ui/src/pages/ExecutionWorkspaceDetail.tsx | 952 ++++++++++++---------- 3 files changed, 532 insertions(+), 431 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 804e8d48..7aac9dfa 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -161,6 +161,8 @@ function boardRoutes() { } /> } /> } /> + } /> + } /> } /> } /> } /> @@ -349,6 +351,8 @@ export function App() { } /> } /> } /> + } /> + } /> } /> } /> }> diff --git a/ui/src/lib/company-routes.test.ts b/ui/src/lib/company-routes.test.ts index ced25744..866b4a62 100644 --- a/ui/src/lib/company-routes.test.ts +++ b/ui/src/lib/company-routes.test.ts @@ -9,16 +9,23 @@ import { describe("company routes", () => { it("treats execution workspace paths as board routes that need a company prefix", () => { expect(isBoardPathWithoutPrefix("/execution-workspaces/workspace-123")).toBe(true); + expect(isBoardPathWithoutPrefix("/execution-workspaces/workspace-123/issues")).toBe(true); expect(extractCompanyPrefixFromPath("/execution-workspaces/workspace-123")).toBeNull(); expect(applyCompanyPrefix("/execution-workspaces/workspace-123", "PAP")).toBe( "/PAP/execution-workspaces/workspace-123", ); + expect(applyCompanyPrefix("/execution-workspaces/workspace-123/issues", "PAP")).toBe( + "/PAP/execution-workspaces/workspace-123/issues", + ); }); it("normalizes prefixed execution workspace paths back to company-relative paths", () => { expect(toCompanyRelativePath("/PAP/execution-workspaces/workspace-123")).toBe( "/execution-workspaces/workspace-123", ); + expect(toCompanyRelativePath("/PAP/execution-workspaces/workspace-123/configuration")).toBe( + "/execution-workspaces/workspace-123/configuration", + ); }); /** diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index e5445890..20168b66 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -1,15 +1,20 @@ import { useEffect, useMemo, useState } from "react"; -import { Link, useParams } from "@/lib/router"; +import { Link, Navigate, useLocation, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import type { ExecutionWorkspace, Project, ProjectWorkspace } from "@paperclipai/shared"; -import { ArrowLeft, Check, Copy, ExternalLink, Loader2 } from "lucide-react"; +import type { ExecutionWorkspace, Issue, Project, ProjectWorkspace } from "@paperclipai/shared"; +import { ArrowLeft, Copy, ExternalLink, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; +import { Tabs } from "@/components/ui/tabs"; import { CopyText } from "../components/CopyText"; import { ExecutionWorkspaceCloseDialog } from "../components/ExecutionWorkspaceCloseDialog"; +import { agentsApi } from "../api/agents"; import { executionWorkspacesApi } from "../api/execution-workspaces"; +import { heartbeatsApi } from "../api/heartbeats"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; +import { IssuesList } from "../components/IssuesList"; +import { PageTabBar } from "../components/PageTabBar"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; @@ -29,6 +34,18 @@ type WorkspaceFormState = { workspaceRuntime: string; }; +type ExecutionWorkspaceTab = "configuration" | "issues"; + +function resolveExecutionWorkspaceTab(pathname: string, workspaceId: string): ExecutionWorkspaceTab | null { + const segments = pathname.split("/").filter(Boolean); + const executionWorkspacesIndex = segments.indexOf("execution-workspaces"); + if (executionWorkspacesIndex === -1 || segments[executionWorkspacesIndex + 1] !== workspaceId) return null; + const tab = segments[executionWorkspacesIndex + 2]; + if (tab === "issues") return "issues"; + if (tab === "configuration") return "configuration"; + return null; +} + function isSafeExternalUrl(value: string | null | undefined) { if (!value) return false; try { @@ -214,8 +231,79 @@ function WorkspaceLink({ return {workspace.name}; } +function ExecutionWorkspaceIssuesList({ + companyId, + workspaceId, + issues, + isLoading, + error, + project, +}: { + companyId: string; + workspaceId: string; + issues: Issue[]; + isLoading: boolean; + error: Error | null; + project: Project | null; +}) { + const queryClient = useQueryClient(); + + const { data: agents } = useQuery({ + queryKey: queryKeys.agents.list(companyId), + queryFn: () => agentsApi.list(companyId), + enabled: !!companyId, + }); + + const { data: liveRuns } = useQuery({ + queryKey: queryKeys.liveRuns(companyId), + queryFn: () => heartbeatsApi.liveRunsForCompany(companyId), + enabled: !!companyId, + refetchInterval: 5000, + }); + + const liveIssueIds = useMemo(() => { + const ids = new Set(); + for (const run of liveRuns ?? []) { + if (run.issueId) ids.add(run.issueId); + } + return ids; + }, [liveRuns]); + + const updateIssue = useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => issuesApi.update(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listByExecutionWorkspace(companyId, workspaceId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) }); + if (project?.id) { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listByProject(companyId, project.id) }); + } + }, + }); + + const projectOptions = useMemo( + () => (project ? [{ id: project.id, name: project.name, workspaces: project.workspaces ?? [] }] : undefined), + [project], + ); + + return ( + updateIssue.mutate({ id, data })} + /> + ); +} + export function ExecutionWorkspaceDetail() { const { workspaceId } = useParams<{ workspaceId: string }>(); + const location = useLocation(); + const navigate = useNavigate(); const queryClient = useQueryClient(); const { setBreadcrumbs } = useBreadcrumbs(); const { selectedCompanyId, setSelectedCompanyId } = useCompany(); @@ -223,6 +311,7 @@ export function ExecutionWorkspaceDetail() { const [closeDialogOpen, setCloseDialogOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [runtimeActionMessage, setRuntimeActionMessage] = useState(null); + const activeTab = workspaceId ? resolveExecutionWorkspaceTab(location.pathname, workspaceId) : null; const workspaceQuery = useQuery({ queryKey: queryKeys.executionWorkspaces.detail(workspaceId!), @@ -357,6 +446,24 @@ export function ExecutionWorkspaceDetail() { } if (!workspace || !form || !initialState) return null; + if (workspaceId && activeTab === null) { + let cachedTab: ExecutionWorkspaceTab = "configuration"; + try { + const storedTab = localStorage.getItem(`paperclip:execution-workspace-tab:${workspaceId}`); + if (storedTab === "issues" || storedTab === "configuration") { + cachedTab = storedTab; + } + } catch {} + return ; + } + + const handleTabChange = (tab: ExecutionWorkspaceTab) => { + try { + localStorage.setItem(`paperclip:execution-workspace-tab:${workspace.id}`, tab); + } catch {} + navigate(`/execution-workspaces/${workspace.id}/${tab}`); + }; + const saveChanges = () => { const validationError = validateForm(form); if (validationError) { @@ -393,468 +500,451 @@ export function ExecutionWorkspaceDetail() {
-
-
-
-
-
+
+
+
+
+ Execution workspace +
+

{workspace.name}

+

+ Configure the concrete runtime workspace that Paperclip reuses for this issue flow. + These settings stay attached to the execution workspace so future runs can keep local paths, repo refs, provisioning, teardown, and runtime-service behavior in sync with the actual workspace being reused. +

+
+
+ +
+
+
+ + handleTabChange(value as ExecutionWorkspaceTab)}> + handleTabChange(value as ExecutionWorkspaceTab)} + /> + + + {activeTab === "configuration" ? ( +
+
+
+
- Execution workspace + Configuration
-

{workspace.name}

-

- Configure the concrete runtime workspace that Paperclip reuses for this issue flow. - These settings stay - attached to the execution workspace so future runs can keep local paths, repo refs, provisioning, teardown, - and runtime-service behavior in sync with the actual workspace being reused. +

Workspace settings

+

+ Edit the concrete path, repo, branch, provisioning, teardown, and runtime overrides attached to this execution workspace.

-
+ + + +
+ + setForm((current) => current ? { ...current, name: event.target.value } : current)} + placeholder="Execution workspace name" + /> + + + setForm((current) => current ? { ...current, branchName: event.target.value } : current)} + placeholder="PAP-946-workspace" + /> + +
+ +
+ + setForm((current) => current ? { ...current, cwd: event.target.value } : current)} + placeholder="/absolute/path/to/workspace" + /> + + + setForm((current) => current ? { ...current, providerRef: event.target.value } : current)} + placeholder="/path/to/worktree or provider ref" + /> + +
+ +
+ + setForm((current) => current ? { ...current, repoUrl: event.target.value } : current)} + placeholder="https://github.com/org/repo" + /> + + + setForm((current) => current ? { ...current, baseRef: event.target.value } : current)} + placeholder="origin/main" + /> + +
+ +
+ +