@cat-factory/node-server 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +297 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/container.d.ts +88 -0
  7. package/dist/container.d.ts.map +1 -0
  8. package/dist/container.js +937 -0
  9. package/dist/container.js.map +1 -0
  10. package/dist/db/client.d.ts +13 -0
  11. package/dist/db/client.d.ts.map +1 -0
  12. package/dist/db/client.js +21 -0
  13. package/dist/db/client.js.map +1 -0
  14. package/dist/db/migrate.d.ts +12 -0
  15. package/dist/db/migrate.d.ts.map +1 -0
  16. package/dist/db/migrate.js +40 -0
  17. package/dist/db/migrate.js.map +1 -0
  18. package/dist/db/schema.d.ts +7858 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +928 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/environments.d.ts +11 -0
  23. package/dist/environments.d.ts.map +1 -0
  24. package/dist/environments.js +31 -0
  25. package/dist/environments.js.map +1 -0
  26. package/dist/execution/bootstrapRunner.d.ts +27 -0
  27. package/dist/execution/bootstrapRunner.d.ts.map +1 -0
  28. package/dist/execution/bootstrapRunner.js +79 -0
  29. package/dist/execution/bootstrapRunner.js.map +1 -0
  30. package/dist/execution/config.d.ts +37 -0
  31. package/dist/execution/config.d.ts.map +1 -0
  32. package/dist/execution/config.js +86 -0
  33. package/dist/execution/config.js.map +1 -0
  34. package/dist/execution/drive.d.ts +6 -0
  35. package/dist/execution/drive.d.ts.map +1 -0
  36. package/dist/execution/drive.js +13 -0
  37. package/dist/execution/drive.js.map +1 -0
  38. package/dist/execution/pgBossRunner.d.ts +82 -0
  39. package/dist/execution/pgBossRunner.d.ts.map +1 -0
  40. package/dist/execution/pgBossRunner.js +163 -0
  41. package/dist/execution/pgBossRunner.js.map +1 -0
  42. package/dist/gateways.d.ts +4 -0
  43. package/dist/gateways.d.ts.map +1 -0
  44. package/dist/gateways.js +91 -0
  45. package/dist/gateways.js.map +1 -0
  46. package/dist/index.d.ts +13 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +22 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/main.d.ts +2 -0
  51. package/dist/main.d.ts.map +1 -0
  52. package/dist/main.js +9 -0
  53. package/dist/main.js.map +1 -0
  54. package/dist/modelProvider.d.ts +6 -0
  55. package/dist/modelProvider.d.ts.map +1 -0
  56. package/dist/modelProvider.js +72 -0
  57. package/dist/modelProvider.js.map +1 -0
  58. package/dist/realtime.d.ts +62 -0
  59. package/dist/realtime.d.ts.map +1 -0
  60. package/dist/realtime.js +171 -0
  61. package/dist/realtime.js.map +1 -0
  62. package/dist/recurring.d.ts +11 -0
  63. package/dist/recurring.d.ts.map +1 -0
  64. package/dist/recurring.js +33 -0
  65. package/dist/recurring.js.map +1 -0
  66. package/dist/repositories/bootstrap.d.ts +25 -0
  67. package/dist/repositories/bootstrap.d.ts.map +1 -0
  68. package/dist/repositories/bootstrap.js +280 -0
  69. package/dist/repositories/bootstrap.js.map +1 -0
  70. package/dist/repositories/containerExecution.d.ts +33 -0
  71. package/dist/repositories/containerExecution.d.ts.map +1 -0
  72. package/dist/repositories/containerExecution.js +199 -0
  73. package/dist/repositories/containerExecution.js.map +1 -0
  74. package/dist/repositories/documents.d.ts +31 -0
  75. package/dist/repositories/documents.d.ts.map +1 -0
  76. package/dist/repositories/documents.js +176 -0
  77. package/dist/repositories/documents.js.map +1 -0
  78. package/dist/repositories/drizzle.d.ts +105 -0
  79. package/dist/repositories/drizzle.d.ts.map +1 -0
  80. package/dist/repositories/drizzle.js +1872 -0
  81. package/dist/repositories/drizzle.js.map +1 -0
  82. package/dist/repositories/environments.d.ts +23 -0
  83. package/dist/repositories/environments.d.ts.map +1 -0
  84. package/dist/repositories/environments.js +162 -0
  85. package/dist/repositories/environments.js.map +1 -0
  86. package/dist/repositories/fragments.d.ts +23 -0
  87. package/dist/repositories/fragments.d.ts.map +1 -0
  88. package/dist/repositories/fragments.js +190 -0
  89. package/dist/repositories/fragments.js.map +1 -0
  90. package/dist/repositories/github.d.ts +53 -0
  91. package/dist/repositories/github.d.ts.map +1 -0
  92. package/dist/repositories/github.js +441 -0
  93. package/dist/repositories/github.js.map +1 -0
  94. package/dist/repositories/localModelEndpoint.d.ts +12 -0
  95. package/dist/repositories/localModelEndpoint.d.ts.map +1 -0
  96. package/dist/repositories/localModelEndpoint.js +75 -0
  97. package/dist/repositories/localModelEndpoint.js.map +1 -0
  98. package/dist/repositories/notifications.d.ts +11 -0
  99. package/dist/repositories/notifications.d.ts.map +1 -0
  100. package/dist/repositories/notifications.js +88 -0
  101. package/dist/repositories/notifications.js.map +1 -0
  102. package/dist/repositories/personalSubscription.d.ts +22 -0
  103. package/dist/repositories/personalSubscription.d.ts.map +1 -0
  104. package/dist/repositories/personalSubscription.js +159 -0
  105. package/dist/repositories/personalSubscription.js.map +1 -0
  106. package/dist/repositories/providerApiKey.d.ts +18 -0
  107. package/dist/repositories/providerApiKey.d.ts.map +1 -0
  108. package/dist/repositories/providerApiKey.js +111 -0
  109. package/dist/repositories/providerApiKey.js.map +1 -0
  110. package/dist/repositories/providerSubscription.d.ts +16 -0
  111. package/dist/repositories/providerSubscription.d.ts.map +1 -0
  112. package/dist/repositories/providerSubscription.js +88 -0
  113. package/dist/repositories/providerSubscription.js.map +1 -0
  114. package/dist/repositories/slack.d.ts +23 -0
  115. package/dist/repositories/slack.d.ts.map +1 -0
  116. package/dist/repositories/slack.js +150 -0
  117. package/dist/repositories/slack.js.map +1 -0
  118. package/dist/repositories/tasks.d.ts +24 -0
  119. package/dist/repositories/tasks.d.ts.map +1 -0
  120. package/dist/repositories/tasks.js +194 -0
  121. package/dist/repositories/tasks.js.map +1 -0
  122. package/dist/retention.d.ts +38 -0
  123. package/dist/retention.d.ts.map +1 -0
  124. package/dist/retention.js +53 -0
  125. package/dist/retention.js.map +1 -0
  126. package/dist/runtime.d.ts +10 -0
  127. package/dist/runtime.d.ts.map +1 -0
  128. package/dist/runtime.js +13 -0
  129. package/dist/runtime.js.map +1 -0
  130. package/dist/server.d.ts +41 -0
  131. package/dist/server.d.ts.map +1 -0
  132. package/dist/server.js +138 -0
  133. package/dist/server.js.map +1 -0
  134. package/dist/tasks/JiraProvider.d.ts +27 -0
  135. package/dist/tasks/JiraProvider.d.ts.map +1 -0
  136. package/dist/tasks/JiraProvider.js +79 -0
  137. package/dist/tasks/JiraProvider.js.map +1 -0
  138. package/drizzle/20260622175812_flashy_maginty/migration.sql +689 -0
  139. package/drizzle/20260622175812_flashy_maginty/snapshot.json +8318 -0
  140. package/drizzle/20260623172634_loud_wallop/migration.sql +11 -0
  141. package/drizzle/20260623172634_loud_wallop/snapshot.json +8439 -0
  142. package/drizzle/20260623174706_acoustic_zemo/migration.sql +16 -0
  143. package/drizzle/20260623174706_acoustic_zemo/snapshot.json +8506 -0
  144. package/drizzle/20260623184400_silent_cardiac/migration.sql +24 -0
  145. package/drizzle/20260623184400_silent_cardiac/snapshot.json +8639 -0
  146. package/drizzle/20260623205323_quick_arclight/migration.sql +1 -0
  147. package/drizzle/20260623205323_quick_arclight/snapshot.json +8963 -0
  148. package/drizzle/20260623221910_black_zombie/migration.sql +22 -0
  149. package/drizzle/20260623221910_black_zombie/snapshot.json +9189 -0
  150. package/drizzle/20260624131343_far_lily_hollister/migration.sql +3 -0
  151. package/drizzle/20260624131343_far_lily_hollister/snapshot.json +9228 -0
  152. package/drizzle/20260624135452_tiny_norman_osborn/migration.sql +11 -0
  153. package/drizzle/20260624135452_tiny_norman_osborn/snapshot.json +9126 -0
  154. package/drizzle/20260624140138_wandering_avengers/migration.sql +1 -0
  155. package/drizzle/20260624140138_wandering_avengers/snapshot.json +9045 -0
  156. package/package.json +62 -0
@@ -0,0 +1,928 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { bigint, doublePrecision, index, integer, pgTable, primaryKey, serial, text, uniqueIndex, } from 'drizzle-orm/pg-core';
3
+ // Postgres schema mirroring the Cloudflare D1 tables column-for-column (snake_case
4
+ // field names = column names) so the shared row<->domain mappers in
5
+ // @cat-factory/server work unchanged against either store. JSON-shaped columns are
6
+ // `text` (the mappers (de)serialise them), and epoch-ms / GitHub-id columns are
7
+ // `bigint({ mode: 'number' })` so they read back as JS numbers. The indexes mirror
8
+ // the D1 migrations 1:1 so query plans (and the unique personal-account constraint)
9
+ // match across stores.
10
+ export const workspaces = pgTable('workspaces', {
11
+ id: text('id').primaryKey(),
12
+ name: text('name').notNull(),
13
+ description: text('description'),
14
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
15
+ account_id: text('account_id'),
16
+ owner_user_id: text('owner_user_id'),
17
+ },
18
+ // listVisible filters by owner_user_id (legacy) and account_id (membership scope).
19
+ (t) => [
20
+ index('idx_workspaces_owner').on(t.owner_user_id),
21
+ index('idx_workspaces_account').on(t.account_id),
22
+ ]);
23
+ // Canonical user identity (decoupled from GitHub). Everything else keys off users.id.
24
+ export const users = pgTable('users', {
25
+ id: text('id').primaryKey(),
26
+ name: text('name'),
27
+ email: text('email'),
28
+ avatar_url: text('avatar_url'),
29
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
30
+ }, (t) => [
31
+ uniqueIndex('idx_users_email')
32
+ .on(t.email)
33
+ .where(sql `email IS NOT NULL`),
34
+ ]);
35
+ // A linked login identity for a user. (provider, subject) is unique.
36
+ export const userIdentities = pgTable('user_identities', {
37
+ user_id: text('user_id').notNull(),
38
+ provider: text('provider').notNull(),
39
+ subject: text('subject').notNull(),
40
+ secret: text('secret'),
41
+ metadata: text('metadata'),
42
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
43
+ }, (t) => [
44
+ primaryKey({ columns: [t.provider, t.subject] }),
45
+ index('idx_user_identities_user').on(t.user_id),
46
+ ]);
47
+ export const accounts = pgTable('accounts', {
48
+ id: text('id').primaryKey(),
49
+ type: text('type').notNull(),
50
+ name: text('name').notNull(),
51
+ github_account_login: text('github_account_login'),
52
+ // The user who owns a personal account (its account-of-one). Null for orgs.
53
+ owner_user_id: text('owner_user_id'),
54
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
55
+ // The default cloud provider new services in this account inherit.
56
+ default_cloud_provider: text('default_cloud_provider'),
57
+ },
58
+ // Enforce one personal account per user (a correctness constraint, not just a
59
+ // lookup index) — the partial unique index `findPersonalByUser` relies on.
60
+ (t) => [
61
+ uniqueIndex('idx_accounts_personal')
62
+ .on(t.owner_user_id)
63
+ .where(sql `type = 'personal'`),
64
+ ]);
65
+ export const memberships = pgTable('memberships', {
66
+ account_id: text('account_id').notNull(),
67
+ user_id: text('user_id').notNull(),
68
+ // Combinable roles (admin / developer / product) as a CSV; defaults to developer.
69
+ roles: text('roles').notNull().default('developer'),
70
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
71
+ }, (t) => [
72
+ primaryKey({ columns: [t.account_id, t.user_id] }),
73
+ index('idx_memberships_user').on(t.user_id),
74
+ ]);
75
+ // Per-account transactional-email sender (UI-onboarded). The provider API key is
76
+ // sealed at rest (SecretCipher), never plaintext.
77
+ export const emailConnections = pgTable('email_connections', {
78
+ account_id: text('account_id').primaryKey(),
79
+ provider: text('provider').notNull(),
80
+ from_address: text('from_address').notNull(),
81
+ api_key_cipher: text('api_key_cipher').notNull(),
82
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
83
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
84
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
85
+ });
86
+ // Email invitations into an org account. Only the token's hash is stored.
87
+ export const accountInvitations = pgTable('account_invitations', {
88
+ id: text('id').primaryKey(),
89
+ account_id: text('account_id').notNull(),
90
+ email: text('email').notNull(),
91
+ roles: text('roles').notNull().default('developer'),
92
+ token_hash: text('token_hash').notNull(),
93
+ invited_by: text('invited_by').notNull(),
94
+ status: text('status').notNull().default('pending'),
95
+ expires_at: bigint('expires_at', { mode: 'number' }).notNull(),
96
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
97
+ }, (t) => [
98
+ index('idx_account_invitations_account').on(t.account_id),
99
+ uniqueIndex('idx_account_invitations_token').on(t.token_hash),
100
+ ]);
101
+ export const blocks = pgTable('blocks', {
102
+ workspace_id: text('workspace_id').notNull(),
103
+ id: text('id').notNull(),
104
+ title: text('title').notNull(),
105
+ type: text('type').notNull(),
106
+ description: text('description').notNull().default(''),
107
+ pos_x: doublePrecision('pos_x').notNull().default(0),
108
+ pos_y: doublePrecision('pos_y').notNull().default(0),
109
+ // Explicit, user-dragged frame size; null => the board auto-sizes from content.
110
+ width: doublePrecision('width'),
111
+ height: doublePrecision('height'),
112
+ status: text('status').notNull(),
113
+ progress: doublePrecision('progress').notNull().default(0),
114
+ depends_on: text('depends_on').notNull().default('[]'),
115
+ execution_id: text('execution_id'),
116
+ level: text('level').notNull().default('frame'),
117
+ parent_id: text('parent_id'),
118
+ confidence: doublePrecision('confidence'),
119
+ module_name: text('module_name'),
120
+ fragment_ids: text('fragment_ids'),
121
+ // Service-level (frame): the service's selected best-practice fragment ids (JSON array).
122
+ service_fragment_ids: text('service_fragment_ids'),
123
+ model_id: text('model_id'),
124
+ pull_request: text('pull_request'),
125
+ merge_preset_id: text('merge_preset_id'),
126
+ pipeline_id: text('pipeline_id'),
127
+ // Task-level agent config-contribution values (JSON id->value map).
128
+ agent_config: text('agent_config'),
129
+ // Service-level (frame): Tester local-infra docker-compose path, the "no infra
130
+ // dependencies" flag, the cloud provider and the abstract instance size.
131
+ test_compose_path: text('test_compose_path'),
132
+ no_infra_dependencies: integer('no_infra_dependencies'),
133
+ cloud_provider: text('cloud_provider'),
134
+ instance_size: text('instance_size'),
135
+ // The account-owned service this block belongs to (migration 0031); will become the
136
+ // physical scope key once the repositories switch off workspace_id.
137
+ service_id: text('service_id'),
138
+ // GitHub user id of the block's creator (migration 0038); drives "notify the task
139
+ // creator" routing. Nullable — legacy blocks / auth-disabled dev have no creator.
140
+ created_by: text('created_by'),
141
+ // The responsible product person (usr_*): notified when requirement review flags it.
142
+ responsible_product_user_id: text('responsible_product_user_id'),
143
+ // Task-level: the task-estimator's triage (complexity/risk/impact + rationale) as
144
+ // JSON; persisted on the block for gating consensus steps + UI ratings.
145
+ estimate: text('estimate'),
146
+ }, (t) => [
147
+ primaryKey({ columns: [t.workspace_id, t.id] }),
148
+ index('idx_blocks_parent').on(t.workspace_id, t.parent_id),
149
+ index('idx_blocks_service').on(t.service_id),
150
+ // findById looks a block up by id alone (no workspace_id), so it can't use the
151
+ // (workspace_id, id) PK — index id directly to avoid scanning the largest table.
152
+ // Block ids are only unique within a workspace, so this is a plain lookup index.
153
+ index('idx_blocks_id').on(t.id),
154
+ ]);
155
+ // In-org shared services: account-owned service + per-workspace mount (migration 0030).
156
+ export const services = pgTable('services', {
157
+ id: text('id').primaryKey(),
158
+ account_id: text('account_id'),
159
+ frame_block_id: text('frame_block_id').notNull(),
160
+ installation_id: bigint('installation_id', { mode: 'number' }),
161
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }),
162
+ // Subdirectory within the linked monorepo this service lives in (NULL = whole repo).
163
+ directory: text('directory'),
164
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
165
+ }, (t) => [
166
+ index('idx_services_account').on(t.account_id),
167
+ // One service per frame block *within an account* (the frame↔service mapping is 1:1).
168
+ // Scoped by account_id, not global: block ids are only unique within a workspace, so a
169
+ // reused/seeded frame id recurs across workspaces; NULL account ids are SQL-distinct, so
170
+ // the auth-disabled/local path stays unconstrained while real accounts stay 1:1.
171
+ uniqueIndex('idx_services_frame').on(t.account_id, t.frame_block_id),
172
+ // getByFrameBlock resolves a service by frame_block_id alone (no account_id), so it
173
+ // can't use the composite idx_services_frame above. This lookup runs in a loop walking
174
+ // a block's ancestry on every agent run's repo resolution + on board reads — index it.
175
+ index('idx_services_frame_block').on(t.frame_block_id),
176
+ index('idx_services_repo').on(t.installation_id, t.repo_github_id),
177
+ ]);
178
+ export const workspaceServices = pgTable('workspace_services', {
179
+ workspace_id: text('workspace_id').notNull(),
180
+ service_id: text('service_id').notNull(),
181
+ pos_x: doublePrecision('pos_x').notNull().default(0),
182
+ pos_y: doublePrecision('pos_y').notNull().default(0),
183
+ width: doublePrecision('width'),
184
+ height: doublePrecision('height'),
185
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
186
+ }, (t) => [
187
+ primaryKey({ columns: [t.workspace_id, t.service_id] }),
188
+ index('idx_workspace_services_service').on(t.service_id),
189
+ ]);
190
+ export const pipelines = pgTable('pipelines', {
191
+ workspace_id: text('workspace_id').notNull(),
192
+ id: text('id').notNull(),
193
+ name: text('name').notNull(),
194
+ agent_kinds: text('agent_kinds').notNull().default('[]'),
195
+ gates: text('gates'),
196
+ thresholds: text('thresholds'),
197
+ // Nullable JSON array of per-step enable flags; truthy `builtin` marks the curated
198
+ // read-only catalog templates (mirror of D1 migration 0002).
199
+ enabled: text('enabled'),
200
+ builtin: integer('builtin'),
201
+ // Nullable JSON array of per-step consensus configs, parallel to agent_kinds (set in
202
+ // the pipeline builder for steps whose kind carries a consensus capability trait).
203
+ consensus: text('consensus'),
204
+ // Monotonic insert sequence (Postgres has no SQLite rowid): a workspace's pipelines
205
+ // are read back in the order they were seeded — the curated `seedPipelines()` order
206
+ // — so the catalog order (and the UI's default `pipelines[0]`) is deterministic and
207
+ // matches the Cloudflare facade (which orders by `rowid`). Auto-assigned on insert.
208
+ seq: serial('seq').notNull(),
209
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.id] })]);
210
+ export const agentRuns = pgTable('agent_runs', {
211
+ workspace_id: text('workspace_id').notNull(),
212
+ id: text('id').notNull(),
213
+ kind: text('kind').notNull(),
214
+ block_id: text('block_id'),
215
+ status: text('status').notNull(),
216
+ detail: text('detail').notNull().default('{}'),
217
+ subtasks: text('subtasks'),
218
+ error: text('error'),
219
+ failure: text('failure'),
220
+ workflow_instance_id: text('workflow_instance_id'),
221
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
222
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
223
+ // The service this run targets (migration 0031), derived from its block.
224
+ service_id: text('service_id'),
225
+ }, (t) => [
226
+ primaryKey({ columns: [t.workspace_id, t.id] }),
227
+ // listByWorkspace filters by workspace_id and orders by created_at.
228
+ index('idx_agent_runs_workspace').on(t.workspace_id, t.created_at),
229
+ index('idx_agent_runs_status_lease').on(t.status, t.updated_at),
230
+ index('idx_agent_runs_block').on(t.workspace_id, t.block_id),
231
+ index('idx_agent_runs_service').on(t.service_id),
232
+ ]);
233
+ export const tokenUsage = pgTable('token_usage', {
234
+ id: text('id').primaryKey(),
235
+ workspace_id: text('workspace_id').notNull(),
236
+ execution_id: text('execution_id'),
237
+ agent_kind: text('agent_kind').notNull(),
238
+ provider: text('provider').notNull(),
239
+ model: text('model').notNull(),
240
+ input_tokens: integer('input_tokens').notNull().default(0),
241
+ output_tokens: integer('output_tokens').notNull().default(0),
242
+ cost_estimate: doublePrecision('cost_estimate').notNull().default(0),
243
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
244
+ }, (t) => [index('idx_token_usage_created').on(t.created_at)]);
245
+ // Per-workspace, per-agent-kind default model selection (mirror of D1 migration
246
+ // 0028). One row per (workspace, agent kind); the model each kind defaults to,
247
+ // overriding the env routing for that workspace. A kind absent for a workspace
248
+ // falls back to the env routing.
249
+ export const workspaceModelDefaults = pgTable('workspace_model_defaults', {
250
+ workspace_id: text('workspace_id').notNull(),
251
+ agent_kind: text('agent_kind').notNull(),
252
+ model_id: text('model_id').notNull(),
253
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
254
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.agent_kind] })]);
255
+ // Per-workspace default service-fragment selection (mirror of D1 migration 0040). One
256
+ // row per workspace; the best-practice fragment ids new services inherit, JSON array.
257
+ export const workspaceFragmentDefaults = pgTable('workspace_fragment_defaults', {
258
+ workspace_id: text('workspace_id').primaryKey(),
259
+ fragment_ids: text('fragment_ids').notNull(),
260
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
261
+ });
262
+ // Prompt-fragment library (ADR 0006; mirror of D1 migration 0020). The managed,
263
+ // tenant-scoped catalog of best-practice fragments, scoped by an (owner_kind,
264
+ // owner_id) pair so one table backs both the account and workspace tiers. JSON-shaped
265
+ // columns (`applies_to`, `tags`) are `text`; a tombstone (`deleted_at`) suppresses an
266
+ // inherited or removed-upstream fragment.
267
+ export const promptFragments = pgTable('prompt_fragments', {
268
+ fragment_id: text('fragment_id').notNull(),
269
+ owner_kind: text('owner_kind').notNull(),
270
+ owner_id: text('owner_id').notNull(),
271
+ version: text('version').notNull(),
272
+ title: text('title').notNull(),
273
+ category: text('category'),
274
+ summary: text('summary').notNull(),
275
+ body: text('body').notNull(),
276
+ applies_to: text('applies_to'),
277
+ tags: text('tags'),
278
+ source_id: text('source_id'),
279
+ source_path: text('source_path'),
280
+ source_sha: text('source_sha'),
281
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
282
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
283
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
284
+ }, (t) => [
285
+ primaryKey({ columns: [t.owner_kind, t.owner_id, t.fragment_id] }),
286
+ index('idx_prompt_fragments_owner')
287
+ .on(t.owner_kind, t.owner_id)
288
+ .where(sql `${t.deleted_at} IS NULL`),
289
+ index('idx_prompt_fragments_source')
290
+ .on(t.source_id)
291
+ .where(sql `${t.deleted_at} IS NULL`),
292
+ ]);
293
+ // A repo directory linked as a source of Markdown guideline files (ADR 0006 §3;
294
+ // mirror of D1 migration 0020). At most one live source per (owner, repo, ref, dir) —
295
+ // the unique index is the upsert key; a partial owner index powers the list.
296
+ export const fragmentSources = pgTable('fragment_sources', {
297
+ id: text('id').primaryKey(),
298
+ owner_kind: text('owner_kind').notNull(),
299
+ owner_id: text('owner_id').notNull(),
300
+ repo_owner: text('repo_owner').notNull(),
301
+ repo_name: text('repo_name').notNull(),
302
+ git_ref: text('git_ref').notNull().default('HEAD'),
303
+ dir_path: text('dir_path').notNull().default(''),
304
+ last_synced_sha: text('last_synced_sha'),
305
+ last_synced_at: bigint('last_synced_at', { mode: 'number' }),
306
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
307
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
308
+ }, (t) => [
309
+ uniqueIndex('idx_fragment_sources_unique').on(t.owner_kind, t.owner_id, t.repo_owner, t.repo_name, t.git_ref, t.dir_path),
310
+ index('idx_fragment_sources_owner')
311
+ .on(t.owner_kind, t.owner_id)
312
+ .where(sql `${t.deleted_at} IS NULL`),
313
+ ]);
314
+ // LLM observability sink (mirror of D1 migration 0026). One row per proxied
315
+ // container-agent model call: full prompt/response, output-limit headroom and the
316
+ // transport-vs-execution latency split. Pruned aggressively by retention (the full
317
+ // bodies make it heavy); booleans are integer 0/1 to match the SQLite store.
318
+ export const llmCallMetrics = pgTable('llm_call_metrics', {
319
+ id: text('id').primaryKey(),
320
+ workspace_id: text('workspace_id').notNull(),
321
+ execution_id: text('execution_id'),
322
+ agent_kind: text('agent_kind').notNull(),
323
+ provider: text('provider').notNull(),
324
+ model: text('model').notNull(),
325
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
326
+ streaming: integer('streaming').notNull().default(0),
327
+ message_count: integer('message_count').notNull().default(0),
328
+ tool_count: integer('tool_count').notNull().default(0),
329
+ request_max_tokens: integer('request_max_tokens'),
330
+ prompt_tokens: integer('prompt_tokens').notNull().default(0),
331
+ cached_prompt_tokens: integer('cached_prompt_tokens').notNull().default(0),
332
+ completion_tokens: integer('completion_tokens').notNull().default(0),
333
+ total_tokens: integer('total_tokens').notNull().default(0),
334
+ finish_reason: text('finish_reason'),
335
+ upstream_ms: integer('upstream_ms').notNull().default(0),
336
+ overhead_ms: integer('overhead_ms').notNull().default(0),
337
+ total_ms: integer('total_ms').notNull().default(0),
338
+ ok: integer('ok').notNull().default(1),
339
+ http_status: integer('http_status'),
340
+ error_message: text('error_message'),
341
+ // prompt_text is stored as a DELTA (only the messages this call appended beyond
342
+ // prompt_prefix_count); the full prompt is rebuilt on export. See D1 migration 0027.
343
+ prompt_text: text('prompt_text').notNull().default(''),
344
+ prompt_prefix_count: integer('prompt_prefix_count').notNull().default(0),
345
+ prompt_hash: text('prompt_hash').notNull().default(''),
346
+ response_text: text('response_text').notNull().default(''),
347
+ // The model's reasoning/"thinking" trace on a separate channel, when emitted (a
348
+ // reasoning model can spend its whole output budget here and return empty
349
+ // response_text). Mirrors D1 migration 0002_llm_reasoning_text.
350
+ reasoning_text: text('reasoning_text').notNull().default(''),
351
+ }, (t) => [
352
+ index('idx_llm_call_metrics_execution').on(t.workspace_id, t.execution_id, t.created_at),
353
+ index('idx_llm_call_metrics_created').on(t.created_at),
354
+ ]);
355
+ // Recurring pipelines (mirror of D1 migration 0029). A schedule attaches a pipeline
356
+ // to a service frame and owns one reused on-board block; the sweeper fires every
357
+ // enabled schedule whose `next_run_at <= now`. `weekdays` is a JSON array (text),
358
+ // epoch-ms columns are bigint. Each fire is recorded in `pipeline_schedule_runs`.
359
+ export const pipelineSchedules = pgTable('pipeline_schedules', {
360
+ workspace_id: text('workspace_id').notNull(),
361
+ id: text('id').notNull(),
362
+ service_id: text('service_id'),
363
+ block_id: text('block_id').notNull(),
364
+ frame_id: text('frame_id').notNull(),
365
+ pipeline_id: text('pipeline_id').notNull(),
366
+ template: text('template').notNull(),
367
+ name: text('name').notNull(),
368
+ interval_hours: integer('interval_hours').notNull(),
369
+ weekdays: text('weekdays').notNull().default('[]'),
370
+ window_start_hour: integer('window_start_hour'),
371
+ window_end_hour: integer('window_end_hour'),
372
+ timezone: text('timezone').notNull().default('UTC'),
373
+ enabled: integer('enabled').notNull().default(1),
374
+ last_run_at: bigint('last_run_at', { mode: 'number' }),
375
+ next_run_at: bigint('next_run_at', { mode: 'number' }).notNull(),
376
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
377
+ }, (t) => [
378
+ primaryKey({ columns: [t.workspace_id, t.id] }),
379
+ index('idx_pipeline_schedules_due').on(t.enabled, t.next_run_at),
380
+ index('idx_pipeline_schedules_block').on(t.workspace_id, t.block_id),
381
+ index('idx_pipeline_schedules_service').on(t.service_id),
382
+ ]);
383
+ export const pipelineScheduleRuns = pgTable('pipeline_schedule_runs', {
384
+ workspace_id: text('workspace_id').notNull(),
385
+ id: text('id').notNull(),
386
+ schedule_id: text('schedule_id').notNull(),
387
+ execution_id: text('execution_id'),
388
+ status: text('status').notNull(),
389
+ started_at: bigint('started_at', { mode: 'number' }).notNull(),
390
+ finished_at: bigint('finished_at', { mode: 'number' }),
391
+ outcome: text('outcome'),
392
+ }, (t) => [
393
+ primaryKey({ columns: [t.workspace_id, t.id] }),
394
+ index('idx_schedule_runs_schedule').on(t.workspace_id, t.schedule_id, t.started_at),
395
+ index('idx_schedule_runs_started').on(t.started_at),
396
+ ]);
397
+ // Requirements reviews (mirror of D1 migration 0021). One row per review; the
398
+ // reviewed `items` live as a JSON array (text). At most one live review per block —
399
+ // the service deletes a block's prior review before inserting a fresh one, so
400
+ // `getByBlock` returns the current one. `incorporated_requirements` holds the
401
+ // reworked, standard-format requirements document the rework step produced.
402
+ export const requirementReviews = pgTable('requirement_reviews', {
403
+ workspace_id: text('workspace_id').notNull(),
404
+ id: text('id').notNull(),
405
+ block_id: text('block_id').notNull(),
406
+ status: text('status').notNull(),
407
+ items: text('items').notNull().default('[]'),
408
+ model: text('model'),
409
+ incorporated_requirements: text('incorporated_requirements'),
410
+ // Reviewer-pass counter + its budget for the iterative review loop (the initial
411
+ // review is iteration 1; an "extra round" choice bumps max_iterations).
412
+ iteration: integer('iteration').notNull().default(1),
413
+ max_iterations: integer('max_iterations').notNull().default(1),
414
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
415
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
416
+ }, (t) => [
417
+ primaryKey({ columns: [t.workspace_id, t.id] }),
418
+ // getByBlock looks up a block's reviews (newest wins), mirroring D1 migration 0021.
419
+ index('idx_requirement_reviews_block').on(t.workspace_id, t.block_id),
420
+ ]);
421
+ // Consensus session transcripts (mirror of D1 migration 0002): one row per
422
+ // (execution, step) recording the multi-model process — participants, round-by-round
423
+ // contributions/votes, and the synthesized result. The observability surface the
424
+ // dedicated Consensus Session window renders; written by `@cat-factory/consensus`.
425
+ export const consensusSessions = pgTable('consensus_sessions', {
426
+ workspace_id: text('workspace_id').notNull(),
427
+ id: text('id').notNull(),
428
+ block_id: text('block_id').notNull(),
429
+ execution_id: text('execution_id'),
430
+ step_index: integer('step_index').notNull(),
431
+ agent_kind: text('agent_kind').notNull(),
432
+ strategy: text('strategy').notNull(),
433
+ status: text('status').notNull(),
434
+ participants: text('participants').notNull().default('[]'),
435
+ rounds: text('rounds').notNull().default('[]'),
436
+ synthesis: text('synthesis'),
437
+ confidence: doublePrecision('confidence'),
438
+ dissent: text('dissent').notNull().default('[]'),
439
+ error: text('error'),
440
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
441
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
442
+ }, (t) => [
443
+ primaryKey({ columns: [t.workspace_id, t.id] }),
444
+ index('idx_consensus_sessions_step').on(t.workspace_id, t.execution_id, t.step_index),
445
+ index('idx_consensus_sessions_block').on(t.workspace_id, t.block_id, t.created_at),
446
+ ]);
447
+ // Clarity (bug-report triage) reviews (mirror of D1 migration 0002_clarity_reviews). The
448
+ // clarity analogue of `requirement_reviews`: items as a JSON array, at most one live review
449
+ // per block. `clarified_report` holds the standard-format clarified bug report.
450
+ export const clarityReviews = pgTable('clarity_reviews', {
451
+ workspace_id: text('workspace_id').notNull(),
452
+ id: text('id').notNull(),
453
+ block_id: text('block_id').notNull(),
454
+ status: text('status').notNull(),
455
+ items: text('items').notNull().default('[]'),
456
+ model: text('model'),
457
+ clarified_report: text('clarified_report'),
458
+ iteration: integer('iteration').notNull().default(1),
459
+ max_iterations: integer('max_iterations').notNull().default(1),
460
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
461
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
462
+ }, (t) => [
463
+ primaryKey({ columns: [t.workspace_id, t.id] }),
464
+ index('idx_clarity_reviews_block').on(t.workspace_id, t.block_id),
465
+ ]);
466
+ // A workspace's issue-tracker selection (mirror of D1 migration 0029).
467
+ export const trackerSettings = pgTable('tracker_settings', {
468
+ workspace_id: text('workspace_id').primaryKey(),
469
+ tracker: text('tracker'),
470
+ jira_project_key: text('jira_project_key'),
471
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
472
+ });
473
+ // Task-source integration (mirror of D1 migration 0014): a workspace's connections
474
+ // to external issue trackers (Jira) and local projections of the issues it imported.
475
+ // `credentials` is an encrypted JSON bag (AES-256-GCM envelope), never sent on the
476
+ // wire. At most one live connection per (workspace, source); a `deleted_at` tombstone
477
+ // lets a workspace disconnect/reconnect.
478
+ export const taskConnections = pgTable('task_connections', {
479
+ workspace_id: text('workspace_id').notNull(),
480
+ source: text('source').notNull(),
481
+ credentials: text('credentials').notNull(),
482
+ label: text('label').notNull().default(''),
483
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
484
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
485
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.source] })]);
486
+ export const tasks = pgTable('tasks', {
487
+ workspace_id: text('workspace_id').notNull(),
488
+ source: text('source').notNull(),
489
+ external_id: text('external_id').notNull(),
490
+ title: text('title').notNull(),
491
+ url: text('url').notNull(),
492
+ status: text('status').notNull().default(''),
493
+ type: text('type').notNull().default(''),
494
+ assignee: text('assignee'),
495
+ priority: text('priority'),
496
+ labels: text('labels').notNull().default('[]'),
497
+ description: text('description').notNull().default(''),
498
+ comments: text('comments').notNull().default('[]'),
499
+ excerpt: text('excerpt').notNull().default(''),
500
+ linked_block_id: text('linked_block_id'),
501
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
502
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
503
+ }, (t) => [
504
+ primaryKey({ columns: [t.workspace_id, t.source, t.external_id] }),
505
+ index('idx_tasks_block').on(t.workspace_id, t.linked_block_id),
506
+ ]);
507
+ // A workspace's binding to a self-hosted runner pool (mirror of D1 migration 0013):
508
+ // the validated manifest + the encrypted scheduler-API secret bundle. The container
509
+ // agent executor dispatches repo-operating jobs to this pool when one is registered.
510
+ // `secrets_cipher` is opaque ciphertext (WebCryptoSecretCipher); never plaintext.
511
+ export const runnerPoolConnections = pgTable('runner_pool_connections', {
512
+ workspace_id: text('workspace_id').notNull(),
513
+ provider_id: text('provider_id').notNull(),
514
+ label: text('label').notNull(),
515
+ base_url: text('base_url').notNull(),
516
+ manifest_json: text('manifest_json').notNull(),
517
+ secrets_cipher: text('secrets_cipher').notNull(),
518
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
519
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
520
+ }, (t) => [
521
+ primaryKey({ columns: [t.workspace_id, t.provider_id] }),
522
+ // A workspace has at most one live pool (the partial unique mirrors D1).
523
+ uniqueIndex('idx_runner_pool_conn_workspace')
524
+ .on(t.workspace_id)
525
+ .where(sql `deleted_at IS NULL`),
526
+ ]);
527
+ // Human-actionable notifications (mirror of D1 migration 0024). First-class items
528
+ // surfaced on the board that outlive the run that raised them (merge_review /
529
+ // pipeline_complete / ci_failed). The optional structured `payload` (assessment /
530
+ // PR url / pipeline name) is JSON text. Closing the Node parity gap so the
531
+ // notification subsystem — and any channel, including Slack — fires here too.
532
+ export const notifications = pgTable('notifications', {
533
+ workspace_id: text('workspace_id').notNull(),
534
+ id: text('id').notNull(),
535
+ type: text('type').notNull(),
536
+ status: text('status').notNull(),
537
+ block_id: text('block_id'),
538
+ execution_id: text('execution_id'),
539
+ title: text('title').notNull(),
540
+ body: text('body').notNull(),
541
+ payload: text('payload'),
542
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
543
+ resolved_at: bigint('resolved_at', { mode: 'number' }),
544
+ }, (t) => [
545
+ primaryKey({ columns: [t.workspace_id, t.id] }),
546
+ index('idx_notifications_open').on(t.workspace_id, t.status, t.created_at),
547
+ index('idx_notifications_block').on(t.workspace_id, t.block_id, t.type, t.status),
548
+ ]);
549
+ // Per-workspace merge threshold presets (mirror of D1 migration 0024's
550
+ // `merge_threshold_presets`). A task selects one via `blocks.merge_preset_id`; none →
551
+ // the workspace default (`is_default`, exactly one per workspace — the repository
552
+ // demotes the prior default when promoting a new one). `is_default` is 0/1 to mirror
553
+ // the D1 integer flag. Carries the auto-merge ceilings + `ci_max_attempts`.
554
+ export const mergeThresholdPresets = pgTable('merge_threshold_presets', {
555
+ workspace_id: text('workspace_id').notNull(),
556
+ id: text('id').notNull(),
557
+ name: text('name').notNull(),
558
+ max_complexity: doublePrecision('max_complexity').notNull(),
559
+ max_risk: doublePrecision('max_risk').notNull(),
560
+ max_impact: doublePrecision('max_impact').notNull(),
561
+ ci_max_attempts: integer('ci_max_attempts').notNull(),
562
+ max_requirement_iterations: integer('max_requirement_iterations').notNull().default(3),
563
+ max_requirement_concern_allowed: text('max_requirement_concern_allowed')
564
+ .notNull()
565
+ .default('none'),
566
+ is_default: integer('is_default').notNull().default(0),
567
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
568
+ }, (t) => [
569
+ primaryKey({ columns: [t.workspace_id, t.id] }),
570
+ // Fast lookup of a workspace's default preset (mirrors idx_merge_presets_default).
571
+ index('idx_merge_presets_default').on(t.workspace_id, t.is_default),
572
+ ]);
573
+ // Board-scan feature: the persisted "repository blueprint" — a repo decomposed into
574
+ // the canonical service → modules tree (mirror of D1 migration 0011). Exactly one
575
+ // blueprint per (workspace, repo): a re-scan replaces it in place (the unique index
576
+ // is the upsert key). The tree is stored whole as JSON in `service_json`.
577
+ export const repoBlueprints = pgTable('repo_blueprints', {
578
+ id: text('id').primaryKey(),
579
+ workspace_id: text('workspace_id').notNull(),
580
+ repo_owner: text('repo_owner').notNull(),
581
+ repo_name: text('repo_name').notNull(),
582
+ source: text('source').notNull(),
583
+ service_json: text('service_json').notNull(),
584
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
585
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
586
+ }, (t) => [
587
+ uniqueIndex('idx_repo_blueprints_repo').on(t.workspace_id, t.repo_owner, t.repo_name),
588
+ index('idx_repo_blueprints_workspace').on(t.workspace_id, t.updated_at),
589
+ ]);
590
+ // Document-source integration (mirror of D1 migration 0012). A `source`
591
+ // discriminator tags every row so one pair of tables serves every provider. The
592
+ // credential bag is encrypted at rest (a WebCryptoSecretCipher envelope), never sent
593
+ // on the wire; at most one live connection per (workspace, source) — reconnecting
594
+ // replaces the row.
595
+ export const documentConnections = pgTable('document_connections', {
596
+ workspace_id: text('workspace_id').notNull(),
597
+ source: text('source').notNull(),
598
+ credentials: text('credentials').notNull(),
599
+ label: text('label').notNull().default(''),
600
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
601
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
602
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.source] })]);
603
+ // One row per imported page: `body` holds the normalized Markdown the planner +
604
+ // agent-context injection consume, `linked_block_id` attaches it to a board block.
605
+ export const documents = pgTable('documents', {
606
+ workspace_id: text('workspace_id').notNull(),
607
+ source: text('source').notNull(),
608
+ external_id: text('external_id').notNull(),
609
+ title: text('title').notNull(),
610
+ url: text('url').notNull(),
611
+ excerpt: text('excerpt').notNull().default(''),
612
+ body: text('body').notNull().default(''),
613
+ linked_block_id: text('linked_block_id'),
614
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
615
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
616
+ }, (t) => [
617
+ primaryKey({ columns: [t.workspace_id, t.source, t.external_id] }),
618
+ index('idx_documents_block').on(t.workspace_id, t.linked_block_id),
619
+ ]);
620
+ // Ephemeral-environment integration (mirror of D1 migration 0008). A workspace's
621
+ // binding to its own environment-management API (a declarative manifest) and the
622
+ // registry of environments provisioned from it. Credentials are opaque ciphertext
623
+ // (SecretCipher envelopes), never plaintext. At most one live provider per workspace
624
+ // (the partial unique index lets a tombstoned binding be replaced).
625
+ export const environmentConnections = pgTable('environment_connections', {
626
+ workspace_id: text('workspace_id').notNull(),
627
+ provider_id: text('provider_id').notNull(),
628
+ label: text('label').notNull(),
629
+ base_url: text('base_url').notNull(),
630
+ manifest_json: text('manifest_json').notNull(),
631
+ secrets_cipher: text('secrets_cipher').notNull(),
632
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
633
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
634
+ }, (t) => [
635
+ primaryKey({ columns: [t.workspace_id, t.provider_id] }),
636
+ uniqueIndex('idx_environment_conn_workspace')
637
+ .on(t.workspace_id)
638
+ .where(sql `${t.deleted_at} IS NULL`),
639
+ ]);
640
+ // One row per provisioned environment. `access_cipher` holds the env's own access
641
+ // creds (what the tester uses); `provision_fields_cipher` holds the fields captured at
642
+ // provision time that status/teardown calls interpolate.
643
+ export const environments = pgTable('environments', {
644
+ id: text('id').primaryKey(),
645
+ workspace_id: text('workspace_id').notNull(),
646
+ block_id: text('block_id'),
647
+ execution_id: text('execution_id'),
648
+ provider_id: text('provider_id').notNull(),
649
+ external_id: text('external_id'),
650
+ url: text('url'),
651
+ status: text('status').notNull(),
652
+ access_cipher: text('access_cipher'),
653
+ provision_fields_cipher: text('provision_fields_cipher'),
654
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
655
+ expires_at: bigint('expires_at', { mode: 'number' }),
656
+ last_error: text('last_error'),
657
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
658
+ }, (t) => [
659
+ index('idx_environments_block')
660
+ .on(t.workspace_id, t.block_id)
661
+ .where(sql `${t.deleted_at} IS NULL`),
662
+ index('idx_environments_expiry')
663
+ .on(t.expires_at)
664
+ .where(sql `${t.deleted_at} IS NULL AND ${t.expires_at} IS NOT NULL`),
665
+ ]);
666
+ // Repo-bootstrap feature: managed reference architectures a new repo is bootstrapped
667
+ // from (mirror of D1 migration 0010). The bootstrap *runs* themselves are stored as
668
+ // kind='bootstrap' rows of the unified agent_runs table (no separate table), exactly
669
+ // like the Worker.
670
+ export const referenceArchitectures = pgTable('reference_architectures', {
671
+ id: text('id').primaryKey(),
672
+ workspace_id: text('workspace_id').notNull(),
673
+ name: text('name').notNull(),
674
+ description: text('description').notNull().default(''),
675
+ repo_owner: text('repo_owner').notNull(),
676
+ repo_name: text('repo_name').notNull(),
677
+ default_instructions: text('default_instructions').notNull().default(''),
678
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
679
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
680
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
681
+ }, (t) => [
682
+ index('idx_reference_architectures_workspace')
683
+ .on(t.workspace_id)
684
+ .where(sql `${t.deleted_at} IS NULL`),
685
+ ]);
686
+ // Slack integration (mirror of D1 migration 0037). An additional delivery transport
687
+ // for the notification mechanism. Per-account connection (+ encrypted bot token,
688
+ // `token_cipher` is a WebCryptoSecretCipher envelope, never plaintext), per-workspace
689
+ // routing, and the per-account GitHub→Slack member map for @-mentions.
690
+ export const slackConnections = pgTable('slack_connections', {
691
+ account_id: text('account_id').primaryKey(),
692
+ team_id: text('team_id').notNull(),
693
+ team_name: text('team_name').notNull(),
694
+ team_icon_url: text('team_icon_url'),
695
+ bot_user_id: text('bot_user_id'),
696
+ scopes: text('scopes'),
697
+ token_cipher: text('token_cipher').notNull(),
698
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
699
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
700
+ },
701
+ // A Slack team binds to at most one live account (mirrors the D1 partial unique).
702
+ (t) => [
703
+ uniqueIndex('idx_slack_conn_team')
704
+ .on(t.team_id)
705
+ .where(sql `deleted_at IS NULL`),
706
+ ]);
707
+ export const slackSettings = pgTable('slack_settings', {
708
+ workspace_id: text('workspace_id').primaryKey(),
709
+ routes: text('routes').notNull().default('{}'),
710
+ mentions_enabled: integer('mentions_enabled').notNull().default(0),
711
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
712
+ });
713
+ export const slackMemberMappings = pgTable('slack_member_mappings', {
714
+ account_id: text('account_id').primaryKey(),
715
+ entries: text('entries').notNull().default('[]'),
716
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
717
+ });
718
+ // Provider-subscription token pool (mirror of D1 migration 0035): per-workspace,
719
+ // per-vendor subscription credentials (Claude Pro/Max OAuth token, ChatGPT
720
+ // auth.json) authenticating the Claude Code / Codex harnesses. The credential is
721
+ // stored as an opaque SecretCipher envelope; usage counters drive usage-aware
722
+ // rotation. A workspace may hold many tokens per vendor (a pool).
723
+ export const providerSubscriptionTokens = pgTable('provider_subscription_tokens', {
724
+ id: text('id').primaryKey(),
725
+ workspace_id: text('workspace_id').notNull(),
726
+ vendor: text('vendor').notNull(),
727
+ label: text('label').notNull(),
728
+ token_cipher: text('token_cipher').notNull(),
729
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
730
+ last_used_at: bigint('last_used_at', { mode: 'number' }),
731
+ window_started_at: bigint('window_started_at', { mode: 'number' }),
732
+ input_tokens: bigint('input_tokens', { mode: 'number' }).notNull().default(0),
733
+ output_tokens: bigint('output_tokens', { mode: 'number' }).notNull().default(0),
734
+ request_count: integer('request_count').notNull().default(0),
735
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
736
+ }, (t) => [index('idx_provider_subs_pool').on(t.workspace_id, t.vendor, t.deleted_at)]);
737
+ // Direct-provider API-key pool: UI-onboarded vendor API keys scoped to an
738
+ // account, workspace, or user (mirror of D1 migration 0042). The key is stored as
739
+ // an opaque SecretCipher envelope — never plaintext.
740
+ export const providerApiKeys = pgTable('provider_api_keys', {
741
+ id: text('id').primaryKey(),
742
+ scope: text('scope').notNull(),
743
+ scope_id: text('scope_id').notNull(),
744
+ provider: text('provider').notNull(),
745
+ label: text('label').notNull(),
746
+ key_cipher: text('key_cipher').notNull(),
747
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
748
+ last_used_at: bigint('last_used_at', { mode: 'number' }),
749
+ window_started_at: bigint('window_started_at', { mode: 'number' }),
750
+ input_tokens: bigint('input_tokens', { mode: 'number' }).notNull().default(0),
751
+ output_tokens: bigint('output_tokens', { mode: 'number' }).notNull().default(0),
752
+ request_count: integer('request_count').notNull().default(0),
753
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
754
+ }, (t) => [index('idx_provider_api_keys_pool').on(t.scope, t.scope_id, t.provider, t.deleted_at)]);
755
+ // Individual-usage subscriptions (Claude): per-USER, never pooled (mirror of D1
756
+ // migration 0039). The credential is double-encrypted (password layer inside the
757
+ // system layer).
758
+ export const personalSubscriptions = pgTable('personal_subscriptions', {
759
+ id: text('id').primaryKey(),
760
+ user_id: text('user_id').notNull(),
761
+ vendor: text('vendor').notNull(),
762
+ label: text('label').notNull(),
763
+ token_cipher: text('token_cipher').notNull(),
764
+ expires_at: bigint('expires_at', { mode: 'number' }),
765
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
766
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
767
+ last_used_at: bigint('last_used_at', { mode: 'number' }),
768
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
769
+ }, (t) => [
770
+ uniqueIndex('idx_personal_subs_user_vendor')
771
+ .on(t.user_id, t.vendor)
772
+ .where(sql `${t.deleted_at} IS NULL`),
773
+ index('idx_personal_subs_expiry')
774
+ .on(t.expires_at)
775
+ .where(sql `${t.deleted_at} IS NULL`),
776
+ ]);
777
+ // Per-USER locally-run model endpoints (Ollama / LM Studio / llama.cpp / vLLM / custom),
778
+ // keyed by (user_id, provider). The optional bearer key is system-key-encrypted in
779
+ // `api_key_cipher`; `models` is a JSON array of enabled model ids (mirror of D1
780
+ // migration 0002).
781
+ export const localModelEndpoints = pgTable('local_model_endpoints', {
782
+ user_id: text('user_id').notNull(),
783
+ provider: text('provider').notNull(),
784
+ label: text('label').notNull(),
785
+ base_url: text('base_url').notNull(),
786
+ api_key_cipher: text('api_key_cipher'),
787
+ models: text('models').notNull(),
788
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
789
+ updated_at: bigint('updated_at', { mode: 'number' }).notNull(),
790
+ }, (t) => [primaryKey({ columns: [t.user_id, t.provider] })]);
791
+ // Per-run activations of a personal credential: the raw token re-encrypted with the
792
+ // system key only, scoped to one execution with a TTL (mirror of D1 migration 0039).
793
+ export const subscriptionActivations = pgTable('subscription_activations', {
794
+ id: text('id').primaryKey(),
795
+ execution_id: text('execution_id').notNull(),
796
+ user_id: text('user_id').notNull(),
797
+ vendor: text('vendor').notNull(),
798
+ token_cipher: text('token_cipher').notNull(),
799
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
800
+ expires_at: bigint('expires_at', { mode: 'number' }).notNull(),
801
+ }, (t) => [
802
+ uniqueIndex('idx_sub_activations_run').on(t.execution_id, t.user_id, t.vendor),
803
+ index('idx_sub_activations_expiry').on(t.expires_at),
804
+ ]);
805
+ // GitHub App installation bindings (mirror of D1 migration 0004 + the account_id /
806
+ // app_id columns from 0017 / 0019). The container executor reads this to resolve a
807
+ // run's installation id and mint a short-lived push token; tokens are cached
808
+ // in-memory by the auth adapter, never persisted here.
809
+ export const githubInstallations = pgTable('github_installations', {
810
+ installation_id: bigint('installation_id', { mode: 'number' }).primaryKey(),
811
+ workspace_id: text('workspace_id').notNull(),
812
+ account_id: text('account_id'),
813
+ account_login: text('account_login').notNull(),
814
+ target_type: text('target_type').notNull(),
815
+ app_id: text('app_id'),
816
+ cached_token: text('cached_token'),
817
+ token_expires_at: bigint('token_expires_at', { mode: 'number' }),
818
+ created_at: bigint('created_at', { mode: 'number' }).notNull(),
819
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
820
+ }, (t) => [
821
+ uniqueIndex('idx_gh_install_workspace')
822
+ .on(t.workspace_id)
823
+ .where(sql `deleted_at IS NULL`),
824
+ index('idx_gh_install_account')
825
+ .on(t.account_id)
826
+ .where(sql `deleted_at IS NULL`),
827
+ ]);
828
+ // Projection of a workspace's GitHub repositories (mirror of D1 migration 0004).
829
+ // `block_id` links a repo to a board service frame and is owned by the board link
830
+ // (never overwritten by sync). The container executor resolves a run's target repo
831
+ // from the service frame the block sits under.
832
+ export const githubRepos = pgTable('github_repos', {
833
+ workspace_id: text('workspace_id').notNull(),
834
+ github_id: bigint('github_id', { mode: 'number' }).notNull(),
835
+ installation_id: bigint('installation_id', { mode: 'number' }).notNull(),
836
+ owner: text('owner').notNull(),
837
+ name: text('name').notNull(),
838
+ default_branch: text('default_branch'),
839
+ private: integer('private').notNull().default(0),
840
+ block_id: text('block_id'),
841
+ // Whether the repo is a monorepo hosting several services (board-owned, like
842
+ // block_id — sync preserves it). See contracts `GitHubRepo.isMonorepo`.
843
+ is_monorepo: integer('is_monorepo').notNull().default(0),
844
+ etag: text('etag'),
845
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
846
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
847
+ }, (t) => [
848
+ primaryKey({ columns: [t.workspace_id, t.github_id] }),
849
+ index('idx_gh_repos_install').on(t.installation_id),
850
+ ]);
851
+ // GitHub projection tables (mirror of D1 migration 0004; sync cursors re-keyed by
852
+ // migration 0032). Local read models of a workspace's repos' branches / PRs / issues /
853
+ // commits / check runs, populated by the inline GitHub sync. `protected`/`merged` are
854
+ // 0/1 to mirror the D1 integer flags; soft-delete tombstones where the D1 tables have one.
855
+ export const githubBranches = pgTable('github_branches', {
856
+ workspace_id: text('workspace_id').notNull(),
857
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
858
+ name: text('name').notNull(),
859
+ head_sha: text('head_sha').notNull(),
860
+ protected: integer('protected').notNull().default(0),
861
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
862
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
863
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.repo_github_id, t.name] })]);
864
+ export const githubPullRequests = pgTable('github_pull_requests', {
865
+ workspace_id: text('workspace_id').notNull(),
866
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
867
+ number: integer('number').notNull(),
868
+ github_id: bigint('github_id', { mode: 'number' }).notNull(),
869
+ title: text('title').notNull(),
870
+ state: text('state').notNull(),
871
+ head_ref: text('head_ref'),
872
+ base_ref: text('base_ref'),
873
+ head_sha: text('head_sha'),
874
+ merged: integer('merged').notNull().default(0),
875
+ author: text('author'),
876
+ gh_updated_at: bigint('gh_updated_at', { mode: 'number' }),
877
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
878
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
879
+ }, (t) => [
880
+ primaryKey({ columns: [t.workspace_id, t.repo_github_id, t.number] }),
881
+ index('idx_gh_pr_state').on(t.workspace_id, t.state),
882
+ ]);
883
+ export const githubIssues = pgTable('github_issues', {
884
+ workspace_id: text('workspace_id').notNull(),
885
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
886
+ number: integer('number').notNull(),
887
+ github_id: bigint('github_id', { mode: 'number' }).notNull(),
888
+ title: text('title').notNull(),
889
+ state: text('state').notNull(),
890
+ author: text('author'),
891
+ labels: text('labels').notNull().default('[]'),
892
+ gh_updated_at: bigint('gh_updated_at', { mode: 'number' }),
893
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
894
+ deleted_at: bigint('deleted_at', { mode: 'number' }),
895
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.repo_github_id, t.number] })]);
896
+ export const githubCommits = pgTable('github_commits', {
897
+ workspace_id: text('workspace_id').notNull(),
898
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
899
+ sha: text('sha').notNull(),
900
+ message: text('message').notNull(),
901
+ author: text('author'),
902
+ authored_at: bigint('authored_at', { mode: 'number' }),
903
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
904
+ }, (t) => [primaryKey({ columns: [t.workspace_id, t.repo_github_id, t.sha] })]);
905
+ export const githubCheckRuns = pgTable('github_check_runs', {
906
+ workspace_id: text('workspace_id').notNull(),
907
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
908
+ github_id: bigint('github_id', { mode: 'number' }).notNull(),
909
+ head_sha: text('head_sha').notNull(),
910
+ name: text('name').notNull(),
911
+ status: text('status').notNull(),
912
+ conclusion: text('conclusion'),
913
+ synced_at: bigint('synced_at', { mode: 'number' }).notNull(),
914
+ }, (t) => [
915
+ primaryKey({ columns: [t.workspace_id, t.repo_github_id, t.github_id] }),
916
+ index('idx_gh_checks_sha').on(t.workspace_id, t.repo_github_id, t.head_sha),
917
+ ]);
918
+ // Incremental-sync bookkeeping, keyed by (installation, repo, kind) so a repo is
919
+ // fetched once per org and fanned out (mirror of D1 migration 0032).
920
+ export const githubSyncCursors = pgTable('github_sync_cursors', {
921
+ installation_id: bigint('installation_id', { mode: 'number' }).notNull(),
922
+ repo_github_id: bigint('repo_github_id', { mode: 'number' }).notNull(),
923
+ kind: text('kind').notNull(),
924
+ etag: text('etag'),
925
+ last_synced_at: bigint('last_synced_at', { mode: 'number' }),
926
+ since_iso: text('since_iso'),
927
+ }, (t) => [primaryKey({ columns: [t.installation_id, t.repo_github_id, t.kind] })]);
928
+ //# sourceMappingURL=schema.js.map