Expand data model with companies, approvals, costs, and heartbeats

Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-17 09:07:22 -06:00
parent fade29fc3e
commit 8c830eae70
41 changed files with 4464 additions and 121 deletions

View File

@@ -1,12 +1,22 @@
import { pgTable, uuid, text, timestamp, jsonb } from "drizzle-orm/pg-core";
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
export const activityLog = pgTable("activity_log", {
id: uuid("id").primaryKey().defaultRandom(),
action: text("action").notNull(),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
agentId: uuid("agent_id").references(() => agents.id),
details: jsonb("details"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const activityLog = pgTable(
"activity_log",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
actorType: text("actor_type").notNull().default("system"),
actorId: text("actor_id").notNull(),
action: text("action").notNull(),
entityType: text("entity_type").notNull(),
entityId: text("entity_id").notNull(),
agentId: uuid("agent_id").references(() => agents.id),
details: jsonb("details").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyCreatedIdx: index("activity_log_company_created_idx").on(table.companyId, table.createdAt),
}),
);

View File

@@ -0,0 +1,21 @@
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { companies } from "./companies.js";
export const agentApiKeys = pgTable(
"agent_api_keys",
{
id: uuid("id").primaryKey().defaultRandom(),
agentId: uuid("agent_id").notNull().references(() => agents.id),
companyId: uuid("company_id").notNull().references(() => companies.id),
name: text("name").notNull(),
keyHash: text("key_hash").notNull(),
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
keyHashIdx: index("agent_api_keys_key_hash_idx").on(table.keyHash),
companyAgentIdx: index("agent_api_keys_company_agent_idx").on(table.companyId, table.agentId),
}),
);

View File

@@ -1,15 +1,38 @@
import { type AnyPgColumn, pgTable, uuid, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
import {
type AnyPgColumn,
pgTable,
uuid,
text,
integer,
timestamp,
jsonb,
index,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
export const agents = pgTable("agents", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
role: text("role").notNull().default("general"),
status: text("status").notNull().default("idle"),
budgetCents: integer("budget_cents").notNull().default(0),
spentCents: integer("spent_cents").notNull().default(0),
lastHeartbeat: timestamp("last_heartbeat", { withTimezone: true }),
reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const agents = pgTable(
"agents",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
name: text("name").notNull(),
role: text("role").notNull().default("general"),
title: text("title"),
status: text("status").notNull().default("idle"),
reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id),
capabilities: text("capabilities"),
adapterType: text("adapter_type").notNull().default("process"),
adapterConfig: jsonb("adapter_config").$type<Record<string, unknown>>().notNull().default({}),
contextMode: text("context_mode").notNull().default("thin"),
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyStatusIdx: index("agents_company_status_idx").on(table.companyId, table.status),
companyReportsToIdx: index("agents_company_reports_to_idx").on(table.companyId, table.reportsTo),
}),
);

View File

@@ -0,0 +1,28 @@
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
export const approvals = pgTable(
"approvals",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
type: text("type").notNull(),
requestedByAgentId: uuid("requested_by_agent_id").references(() => agents.id),
requestedByUserId: text("requested_by_user_id"),
status: text("status").notNull().default("pending"),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
decisionNote: text("decision_note"),
decidedByUserId: text("decided_by_user_id"),
decidedAt: timestamp("decided_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyStatusTypeIdx: index("approvals_company_status_type_idx").on(
table.companyId,
table.status,
table.type,
),
}),
);

View File

@@ -0,0 +1,12 @@
import { pgTable, uuid, text, integer, timestamp } from "drizzle-orm/pg-core";
export const companies = pgTable("companies", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("active"),
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});

View File

@@ -0,0 +1,34 @@
import { pgTable, uuid, text, timestamp, integer, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { issues } from "./issues.js";
import { projects } from "./projects.js";
import { goals } from "./goals.js";
export const costEvents = pgTable(
"cost_events",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
agentId: uuid("agent_id").notNull().references(() => agents.id),
issueId: uuid("issue_id").references(() => issues.id),
projectId: uuid("project_id").references(() => projects.id),
goalId: uuid("goal_id").references(() => goals.id),
billingCode: text("billing_code"),
provider: text("provider").notNull(),
model: text("model").notNull(),
inputTokens: integer("input_tokens").notNull().default(0),
outputTokens: integer("output_tokens").notNull().default(0),
costCents: integer("cost_cents").notNull(),
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyOccurredIdx: index("cost_events_company_occurred_idx").on(table.companyId, table.occurredAt),
companyAgentOccurredIdx: index("cost_events_company_agent_occurred_idx").on(
table.companyId,
table.agentId,
table.occurredAt,
),
}),
);

View File

@@ -1,13 +1,29 @@
import { type AnyPgColumn, pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import {
type AnyPgColumn,
pgTable,
uuid,
text,
timestamp,
index,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { companies } from "./companies.js";
export const goals = pgTable("goals", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
description: text("description"),
level: text("level").notNull().default("task"),
parentId: uuid("parent_id").references((): AnyPgColumn => goals.id),
ownerId: uuid("owner_id").references(() => agents.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const goals = pgTable(
"goals",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
title: text("title").notNull(),
description: text("description"),
level: text("level").notNull().default("task"),
status: text("status").notNull().default("planned"),
parentId: uuid("parent_id").references((): AnyPgColumn => goals.id),
ownerAgentId: uuid("owner_agent_id").references(() => agents.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIdx: index("goals_company_idx").on(table.companyId),
}),
);

View File

@@ -0,0 +1,28 @@
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
export const heartbeatRuns = pgTable(
"heartbeat_runs",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
agentId: uuid("agent_id").notNull().references(() => agents.id),
invocationSource: text("invocation_source").notNull().default("manual"),
status: text("status").notNull().default("queued"),
startedAt: timestamp("started_at", { withTimezone: true }),
finishedAt: timestamp("finished_at", { withTimezone: true }),
error: text("error"),
externalRunId: text("external_run_id"),
contextSnapshot: jsonb("context_snapshot").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyAgentStartedIdx: index("heartbeat_runs_company_agent_started_idx").on(
table.companyId,
table.agentId,
table.startedAt,
),
}),
);

View File

@@ -1,5 +1,11 @@
export { companies } from "./companies.js";
export { agents } from "./agents.js";
export { agentApiKeys } from "./agent_api_keys.js";
export { projects } from "./projects.js";
export { goals } from "./goals.js";
export { issues } from "./issues.js";
export { issueComments } from "./issue_comments.js";
export { heartbeatRuns } from "./heartbeat_runs.js";
export { costEvents } from "./cost_events.js";
export { approvals } from "./approvals.js";
export { activityLog } from "./activity_log.js";

View File

@@ -0,0 +1,22 @@
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { agents } from "./agents.js";
export const issueComments = pgTable(
"issue_comments",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
authorAgentId: uuid("author_agent_id").references(() => agents.id),
authorUserId: text("author_user_id"),
body: text("body").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
issueIdx: index("issue_comments_issue_idx").on(table.issueId),
companyIdx: index("issue_comments_company_idx").on(table.companyId),
}),
);

View File

@@ -1,17 +1,48 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import {
type AnyPgColumn,
pgTable,
uuid,
text,
timestamp,
integer,
index,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { projects } from "./projects.js";
import { goals } from "./goals.js";
import { companies } from "./companies.js";
export const issues = pgTable("issues", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
priority: text("priority").notNull().default("medium"),
projectId: uuid("project_id").references(() => projects.id),
assigneeId: uuid("assignee_id").references(() => agents.id),
goalId: uuid("goal_id").references(() => goals.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const issues = pgTable(
"issues",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
projectId: uuid("project_id").references(() => projects.id),
goalId: uuid("goal_id").references(() => goals.id),
parentId: uuid("parent_id").references((): AnyPgColumn => issues.id),
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
priority: text("priority").notNull().default("medium"),
assigneeAgentId: uuid("assignee_agent_id").references(() => agents.id),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
createdByUserId: text("created_by_user_id"),
requestDepth: integer("request_depth").notNull().default(0),
billingCode: text("billing_code"),
startedAt: timestamp("started_at", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyStatusIdx: index("issues_company_status_idx").on(table.companyId, table.status),
assigneeStatusIdx: index("issues_company_assignee_status_idx").on(
table.companyId,
table.assigneeAgentId,
table.status,
),
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
}),
);

View File

@@ -1,9 +1,23 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { pgTable, uuid, text, timestamp, date, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { goals } from "./goals.js";
import { agents } from "./agents.js";
export const projects = pgTable("projects", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const projects = pgTable(
"projects",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
goalId: uuid("goal_id").references(() => goals.id),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
leadAgentId: uuid("lead_agent_id").references(() => agents.id),
targetDate: date("target_date"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIdx: index("projects_company_idx").on(table.companyId),
}),
);