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,7 +1,7 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/schema/*.ts",
schema: "./dist/schema/*.js",
out: "./src/migrations",
dialect: "postgresql",
dbCredentials: {

View File

@@ -9,7 +9,7 @@
},
"scripts": {
"typecheck": "tsc --noEmit",
"generate": "drizzle-kit generate",
"generate": "tsc -p tsconfig.json && drizzle-kit generate",
"migrate": "tsx src/migrate.ts",
"seed": "tsx src/seed.ts"
},

View File

@@ -0,0 +1,208 @@
CREATE TABLE "activity_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"actor_type" text DEFAULT 'system' NOT NULL,
"actor_id" text NOT NULL,
"action" text NOT NULL,
"entity_type" text NOT NULL,
"entity_id" text NOT NULL,
"agent_id" uuid,
"details" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "agent_api_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"agent_id" uuid NOT NULL,
"company_id" uuid NOT NULL,
"name" text NOT NULL,
"key_hash" text NOT NULL,
"last_used_at" timestamp with time zone,
"revoked_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "agents" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"name" text NOT NULL,
"role" text DEFAULT 'general' NOT NULL,
"title" text,
"status" text DEFAULT 'idle' NOT NULL,
"reports_to" uuid,
"capabilities" text,
"adapter_type" text DEFAULT 'process' NOT NULL,
"adapter_config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"context_mode" text DEFAULT 'thin' NOT NULL,
"budget_monthly_cents" integer DEFAULT 0 NOT NULL,
"spent_monthly_cents" integer DEFAULT 0 NOT NULL,
"last_heartbeat_at" timestamp with time zone,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "approvals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"type" text NOT NULL,
"requested_by_agent_id" uuid,
"requested_by_user_id" text,
"status" text DEFAULT 'pending' NOT NULL,
"payload" jsonb NOT NULL,
"decision_note" text,
"decided_by_user_id" text,
"decided_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "companies" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"description" text,
"status" text DEFAULT 'active' NOT NULL,
"budget_monthly_cents" integer DEFAULT 0 NOT NULL,
"spent_monthly_cents" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "cost_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"issue_id" uuid,
"project_id" uuid,
"goal_id" uuid,
"billing_code" text,
"provider" text NOT NULL,
"model" text NOT NULL,
"input_tokens" integer DEFAULT 0 NOT NULL,
"output_tokens" integer DEFAULT 0 NOT NULL,
"cost_cents" integer NOT NULL,
"occurred_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "goals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"title" text NOT NULL,
"description" text,
"level" text DEFAULT 'task' NOT NULL,
"status" text DEFAULT 'planned' NOT NULL,
"parent_id" uuid,
"owner_agent_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "heartbeat_runs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"invocation_source" text DEFAULT 'manual' NOT NULL,
"status" text DEFAULT 'queued' NOT NULL,
"started_at" timestamp with time zone,
"finished_at" timestamp with time zone,
"error" text,
"external_run_id" text,
"context_snapshot" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "issue_comments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"author_agent_id" uuid,
"author_user_id" text,
"body" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "issues" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"project_id" uuid,
"goal_id" uuid,
"parent_id" uuid,
"title" text NOT NULL,
"description" text,
"status" text DEFAULT 'backlog' NOT NULL,
"priority" text DEFAULT 'medium' NOT NULL,
"assignee_agent_id" uuid,
"created_by_agent_id" uuid,
"created_by_user_id" text,
"request_depth" integer DEFAULT 0 NOT NULL,
"billing_code" text,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"cancelled_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "projects" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"goal_id" uuid,
"name" text NOT NULL,
"description" text,
"status" text DEFAULT 'backlog' NOT NULL,
"lead_agent_id" uuid,
"target_date" date,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "activity_log" ADD CONSTRAINT "activity_log_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "activity_log" ADD CONSTRAINT "activity_log_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_api_keys" ADD CONSTRAINT "agent_api_keys_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_api_keys" ADD CONSTRAINT "agent_api_keys_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agents" ADD CONSTRAINT "agents_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agents" ADD CONSTRAINT "agents_reports_to_agents_id_fk" FOREIGN KEY ("reports_to") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_requested_by_agent_id_agents_id_fk" FOREIGN KEY ("requested_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cost_events" ADD CONSTRAINT "cost_events_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "goals" ADD CONSTRAINT "goals_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "goals" ADD CONSTRAINT "goals_parent_id_goals_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "goals" ADD CONSTRAINT "goals_owner_agent_id_agents_id_fk" FOREIGN KEY ("owner_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD CONSTRAINT "heartbeat_runs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD CONSTRAINT "heartbeat_runs_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_author_agent_id_agents_id_fk" FOREIGN KEY ("author_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_parent_id_issues_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_assignee_agent_id_agents_id_fk" FOREIGN KEY ("assignee_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "issues" ADD CONSTRAINT "issues_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_lead_agent_id_agents_id_fk" FOREIGN KEY ("lead_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "activity_log_company_created_idx" ON "activity_log" USING btree ("company_id","created_at");--> statement-breakpoint
CREATE INDEX "agent_api_keys_key_hash_idx" ON "agent_api_keys" USING btree ("key_hash");--> statement-breakpoint
CREATE INDEX "agent_api_keys_company_agent_idx" ON "agent_api_keys" USING btree ("company_id","agent_id");--> statement-breakpoint
CREATE INDEX "agents_company_status_idx" ON "agents" USING btree ("company_id","status");--> statement-breakpoint
CREATE INDEX "agents_company_reports_to_idx" ON "agents" USING btree ("company_id","reports_to");--> statement-breakpoint
CREATE INDEX "approvals_company_status_type_idx" ON "approvals" USING btree ("company_id","status","type");--> statement-breakpoint
CREATE INDEX "cost_events_company_occurred_idx" ON "cost_events" USING btree ("company_id","occurred_at");--> statement-breakpoint
CREATE INDEX "cost_events_company_agent_occurred_idx" ON "cost_events" USING btree ("company_id","agent_id","occurred_at");--> statement-breakpoint
CREATE INDEX "goals_company_idx" ON "goals" USING btree ("company_id");--> statement-breakpoint
CREATE INDEX "heartbeat_runs_company_agent_started_idx" ON "heartbeat_runs" USING btree ("company_id","agent_id","started_at");--> statement-breakpoint
CREATE INDEX "issue_comments_issue_idx" ON "issue_comments" USING btree ("issue_id");--> statement-breakpoint
CREATE INDEX "issue_comments_company_idx" ON "issue_comments" USING btree ("company_id");--> statement-breakpoint
CREATE INDEX "issues_company_status_idx" ON "issues" USING btree ("company_id","status");--> statement-breakpoint
CREATE INDEX "issues_company_assignee_status_idx" ON "issues" USING btree ("company_id","assignee_agent_id","status");--> statement-breakpoint
CREATE INDEX "issues_company_parent_idx" ON "issues" USING btree ("company_id","parent_id");--> statement-breakpoint
CREATE INDEX "issues_company_project_idx" ON "issues" USING btree ("company_id","project_id");--> statement-breakpoint
CREATE INDEX "projects_company_idx" ON "projects" USING btree ("company_id");

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,13 @@
{"version":"7","dialect":"postgresql","entries":[]}
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1771300567463,
"tag": "0000_mature_masked_marvel",
"breakpoints": true
}
]
}

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),
}),
);

View File

@@ -1,6 +1,5 @@
import { eq, inArray } from "drizzle-orm";
import { createDb } from "./client.js";
import { agents, projects, issues, goals } from "./schema/index.js";
import { companies, agents, goals, projects, issues } from "./schema/index.js";
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");
@@ -9,56 +8,90 @@ const db = createDb(url);
console.log("Seeding database...");
const [ceo, engineer, researcher] = await db
.insert(agents)
.values([
{ name: "CEO Agent", role: "ceo", status: "active" },
{ name: "Engineer Agent", role: "engineer", status: "idle" },
{ name: "Researcher Agent", role: "researcher", status: "idle" },
])
const [company] = await db
.insert(companies)
.values({
name: "Paperclip Demo Co",
description: "A demo autonomous company",
status: "active",
budgetMonthlyCents: 50000,
})
.returning();
// Wire up reporting hierarchy: engineer and researcher report to CEO
await db
.update(agents)
.set({ reportsTo: ceo!.id })
.where(inArray(agents.id, [engineer!.id, researcher!.id]));
const [ceo] = await db
.insert(agents)
.values({
companyId: company!.id,
name: "CEO Agent",
role: "ceo",
title: "Chief Executive Officer",
status: "idle",
adapterType: "process",
adapterConfig: { command: "echo", args: ["hello from ceo"] },
budgetMonthlyCents: 15000,
})
.returning();
const [project] = await db
.insert(projects)
.values([{ name: "Paperclip MVP", description: "Build the initial paperclip management platform" }])
const [engineer] = await db
.insert(agents)
.values({
companyId: company!.id,
name: "Engineer Agent",
role: "engineer",
title: "Software Engineer",
status: "idle",
reportsTo: ceo!.id,
adapterType: "process",
adapterConfig: { command: "echo", args: ["hello from engineer"] },
budgetMonthlyCents: 10000,
})
.returning();
const [goal] = await db
.insert(goals)
.values([
{
title: "Launch MVP",
description: "Ship the minimum viable product",
level: "milestone",
ownerId: ceo!.id,
},
])
.values({
companyId: company!.id,
title: "Ship V1",
description: "Deliver first control plane release",
level: "company",
status: "active",
ownerAgentId: ceo!.id,
})
.returning();
const [project] = await db
.insert(projects)
.values({
companyId: company!.id,
goalId: goal!.id,
name: "Control Plane MVP",
description: "Implement core board + agent loop",
status: "in_progress",
leadAgentId: ceo!.id,
})
.returning();
await db.insert(issues).values([
{
title: "Set up database schema",
description: "Create initial Drizzle schema with all core tables",
status: "done",
priority: "high",
companyId: company!.id,
projectId: project!.id,
assigneeId: engineer!.id,
goalId: goal!.id,
title: "Implement atomic task checkout",
description: "Ensure in_progress claiming is conflict-safe",
status: "todo",
priority: "high",
assigneeAgentId: engineer!.id,
createdByAgentId: ceo!.id,
},
{
title: "Implement agent heartbeat",
description: "Add periodic heartbeat mechanism for agent health monitoring",
status: "in_progress",
priority: "medium",
companyId: company!.id,
projectId: project!.id,
assigneeId: engineer!.id,
goalId: goal!.id,
title: "Add budget auto-pause",
description: "Pause agent at hard budget ceiling",
status: "backlog",
priority: "medium",
createdByAgentId: ceo!.id,
},
]);

View File

@@ -2,9 +2,13 @@ export const API_PREFIX = "/api";
export const API = {
health: `${API_PREFIX}/health`,
companies: `${API_PREFIX}/companies`,
agents: `${API_PREFIX}/agents`,
projects: `${API_PREFIX}/projects`,
issues: `${API_PREFIX}/issues`,
goals: `${API_PREFIX}/goals`,
approvals: `${API_PREFIX}/approvals`,
costs: `${API_PREFIX}/costs`,
activity: `${API_PREFIX}/activity`,
dashboard: `${API_PREFIX}/dashboard`,
} as const;

View File

@@ -1,12 +1,33 @@
export const AGENT_STATUSES = ["active", "idle", "offline", "error"] as const;
export const COMPANY_STATUSES = ["active", "paused", "archived"] as const;
export type CompanyStatus = (typeof COMPANY_STATUSES)[number];
export const AGENT_STATUSES = [
"active",
"paused",
"idle",
"running",
"error",
"terminated",
] as const;
export type AgentStatus = (typeof AGENT_STATUSES)[number];
export const AGENT_CONTEXT_MODES = ["thin", "fat"] as const;
export type AgentContextMode = (typeof AGENT_CONTEXT_MODES)[number];
export const AGENT_ADAPTER_TYPES = ["process", "http"] as const;
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number];
export const AGENT_ROLES = [
"ceo",
"cto",
"cmo",
"cfo",
"engineer",
"designer",
"pm",
"qa",
"devops",
"researcher",
"general",
] as const;
export type AgentRole = (typeof AGENT_ROLES)[number];
@@ -17,17 +38,44 @@ export const ISSUE_STATUSES = [
"in_progress",
"in_review",
"done",
"blocked",
"cancelled",
] as const;
export type IssueStatus = (typeof ISSUE_STATUSES)[number];
export const ISSUE_PRIORITIES = [
"critical",
"high",
"medium",
"low",
] as const;
export const ISSUE_PRIORITIES = ["critical", "high", "medium", "low"] as const;
export type IssuePriority = (typeof ISSUE_PRIORITIES)[number];
export const GOAL_LEVELS = ["company", "team", "agent", "task"] as const;
export type GoalLevel = (typeof GOAL_LEVELS)[number];
export const GOAL_STATUSES = ["planned", "active", "achieved", "cancelled"] as const;
export type GoalStatus = (typeof GOAL_STATUSES)[number];
export const PROJECT_STATUSES = [
"backlog",
"planned",
"in_progress",
"completed",
"cancelled",
] as const;
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
export const APPROVAL_TYPES = ["hire_agent", "approve_ceo_strategy"] as const;
export type ApprovalType = (typeof APPROVAL_TYPES)[number];
export const APPROVAL_STATUSES = ["pending", "approved", "rejected", "cancelled"] as const;
export type ApprovalStatus = (typeof APPROVAL_STATUSES)[number];
export const HEARTBEAT_INVOCATION_SOURCES = ["scheduler", "manual", "callback"] as const;
export type HeartbeatInvocationSource = (typeof HEARTBEAT_INVOCATION_SOURCES)[number];
export const HEARTBEAT_RUN_STATUSES = [
"queued",
"running",
"succeeded",
"failed",
"cancelled",
"timed_out",
] as const;
export type HeartbeatRunStatus = (typeof HEARTBEAT_RUN_STATUSES)[number];

View File

@@ -1,41 +1,85 @@
export {
COMPANY_STATUSES,
AGENT_STATUSES,
AGENT_CONTEXT_MODES,
AGENT_ADAPTER_TYPES,
AGENT_ROLES,
ISSUE_STATUSES,
ISSUE_PRIORITIES,
GOAL_LEVELS,
GOAL_STATUSES,
PROJECT_STATUSES,
APPROVAL_TYPES,
APPROVAL_STATUSES,
HEARTBEAT_INVOCATION_SOURCES,
HEARTBEAT_RUN_STATUSES,
type CompanyStatus,
type AgentStatus,
type AgentContextMode,
type AgentAdapterType,
type AgentRole,
type IssueStatus,
type IssuePriority,
type GoalLevel,
type GoalStatus,
type ProjectStatus,
type ApprovalType,
type ApprovalStatus,
type HeartbeatInvocationSource,
type HeartbeatRunStatus,
} from "./constants.js";
export type {
Company,
Agent,
AgentKeyCreated,
Project,
Issue,
IssueComment,
Goal,
Approval,
CostEvent,
CostSummary,
HeartbeatRun,
DashboardSummary,
ActivityEvent,
} from "./types/index.js";
export {
createCompanySchema,
updateCompanySchema,
type CreateCompany,
type UpdateCompany,
createAgentSchema,
updateAgentSchema,
createAgentKeySchema,
type CreateAgent,
type UpdateAgent,
type CreateAgentKey,
createProjectSchema,
updateProjectSchema,
type CreateProject,
type UpdateProject,
createIssueSchema,
updateIssueSchema,
checkoutIssueSchema,
addIssueCommentSchema,
type CreateIssue,
type UpdateIssue,
type CheckoutIssue,
type AddIssueComment,
createGoalSchema,
updateGoalSchema,
type CreateGoal,
type UpdateGoal,
createApprovalSchema,
resolveApprovalSchema,
type CreateApproval,
type ResolveApproval,
createCostEventSchema,
updateBudgetSchema,
type CreateCostEvent,
type UpdateBudget,
} from "./validators/index.js";
export { API_PREFIX, API } from "./api.js";

View File

@@ -1,5 +1,8 @@
export interface ActivityEvent {
id: string;
companyId: string;
actorType: "agent" | "user" | "system";
actorId: string;
action: string;
entityType: string;
entityId: string;

View File

@@ -1,15 +1,33 @@
import type { AgentRole, AgentStatus } from "../constants.js";
import type {
AgentAdapterType,
AgentContextMode,
AgentRole,
AgentStatus,
} from "../constants.js";
export interface Agent {
id: string;
companyId: string;
name: string;
role: AgentRole;
title: string | null;
status: AgentStatus;
budgetCents: number;
spentCents: number;
lastHeartbeat: Date | null;
reportsTo: string | null;
capabilities: string | null;
adapterType: AgentAdapterType;
adapterConfig: Record<string, unknown>;
contextMode: AgentContextMode;
budgetMonthlyCents: number;
spentMonthlyCents: number;
lastHeartbeatAt: Date | null;
metadata: Record<string, unknown> | null;
createdAt: Date;
updatedAt: Date;
}
export interface AgentKeyCreated {
id: string;
name: string;
token: string;
createdAt: Date;
}

View File

@@ -0,0 +1,16 @@
import type { ApprovalStatus, ApprovalType } from "../constants.js";
export interface Approval {
id: string;
companyId: string;
type: ApprovalType;
requestedByAgentId: string | null;
requestedByUserId: string | null;
status: ApprovalStatus;
payload: Record<string, unknown>;
decisionNote: string | null;
decidedByUserId: string | null;
decidedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -0,0 +1,12 @@
import type { CompanyStatus } from "../constants.js";
export interface Company {
id: string;
name: string;
description: string | null;
status: CompanyStatus;
budgetMonthlyCents: number;
spentMonthlyCents: number;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -0,0 +1,23 @@
export interface CostEvent {
id: string;
companyId: string;
agentId: string;
issueId: string | null;
projectId: string | null;
goalId: string | null;
billingCode: string | null;
provider: string;
model: string;
inputTokens: number;
outputTokens: number;
costCents: number;
occurredAt: Date;
createdAt: Date;
}
export interface CostSummary {
companyId: string;
monthSpendCents: number;
monthBudgetCents: number;
monthUtilizationPercent: number;
}

View File

@@ -0,0 +1,22 @@
export interface DashboardSummary {
companyId: string;
agents: {
active: number;
running: number;
paused: number;
error: number;
};
tasks: {
open: number;
inProgress: number;
blocked: number;
done: number;
};
costs: {
monthSpendCents: number;
monthBudgetCents: number;
monthUtilizationPercent: number;
};
pendingApprovals: number;
staleTasks: number;
}

View File

@@ -1,12 +1,14 @@
import type { GoalLevel } from "../constants.js";
import type { GoalLevel, GoalStatus } from "../constants.js";
export interface Goal {
id: string;
companyId: string;
title: string;
description: string | null;
level: GoalLevel;
status: GoalStatus;
parentId: string | null;
ownerId: string | null;
ownerAgentId: string | null;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -0,0 +1,19 @@
import type {
HeartbeatInvocationSource,
HeartbeatRunStatus,
} from "../constants.js";
export interface HeartbeatRun {
id: string;
companyId: string;
agentId: string;
invocationSource: HeartbeatInvocationSource;
status: HeartbeatRunStatus;
startedAt: Date | null;
finishedAt: Date | null;
error: string | null;
externalRunId: string | null;
contextSnapshot: Record<string, unknown> | null;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -1,5 +1,10 @@
export type { Agent } from "./agent.js";
export type { Company } from "./company.js";
export type { Agent, AgentKeyCreated } from "./agent.js";
export type { Project } from "./project.js";
export type { Issue } from "./issue.js";
export type { Issue, IssueComment } from "./issue.js";
export type { Goal } from "./goal.js";
export type { Approval } from "./approval.js";
export type { CostEvent, CostSummary } from "./cost.js";
export type { HeartbeatRun } from "./heartbeat.js";
export type { DashboardSummary } from "./dashboard.js";
export type { ActivityEvent } from "./activity.js";

View File

@@ -2,13 +2,33 @@ import type { IssuePriority, IssueStatus } from "../constants.js";
export interface Issue {
id: string;
companyId: string;
projectId: string | null;
goalId: string | null;
parentId: string | null;
title: string;
description: string | null;
status: IssueStatus;
priority: IssuePriority;
projectId: string | null;
assigneeId: string | null;
goalId: string | null;
assigneeAgentId: string | null;
createdByAgentId: string | null;
createdByUserId: string | null;
requestDepth: number;
billingCode: string | null;
startedAt: Date | null;
completedAt: Date | null;
cancelledAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface IssueComment {
id: string;
companyId: string;
issueId: string;
authorAgentId: string | null;
authorUserId: string | null;
body: string;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -1,7 +1,14 @@
import type { ProjectStatus } from "../constants.js";
export interface Project {
id: string;
companyId: string;
goalId: string | null;
name: string;
description: string | null;
status: ProjectStatus;
leadAgentId: string | null;
targetDate: string | null;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -1,11 +1,21 @@
import { z } from "zod";
import { AGENT_ROLES, AGENT_STATUSES } from "../constants.js";
import {
AGENT_ADAPTER_TYPES,
AGENT_CONTEXT_MODES,
AGENT_ROLES,
AGENT_STATUSES,
} from "../constants.js";
export const createAgentSchema = z.object({
name: z.string().min(1),
role: z.enum(AGENT_ROLES),
budgetCents: z.number().int().nonnegative().optional().default(0),
role: z.enum(AGENT_ROLES).optional().default("general"),
title: z.string().optional().nullable(),
reportsTo: z.string().uuid().optional().nullable(),
capabilities: z.string().optional().nullable(),
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional().default("process"),
adapterConfig: z.record(z.unknown()).optional().default({}),
contextMode: z.enum(AGENT_CONTEXT_MODES).optional().default("thin"),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
metadata: z.record(z.unknown()).optional().nullable(),
});
@@ -15,6 +25,13 @@ export const updateAgentSchema = createAgentSchema
.partial()
.extend({
status: z.enum(AGENT_STATUSES).optional(),
spentMonthlyCents: z.number().int().nonnegative().optional(),
});
export type UpdateAgent = z.infer<typeof updateAgentSchema>;
export const createAgentKeySchema = z.object({
name: z.string().min(1).default("default"),
});
export type CreateAgentKey = z.infer<typeof createAgentKeySchema>;

View File

@@ -0,0 +1,17 @@
import { z } from "zod";
import { APPROVAL_TYPES } from "../constants.js";
export const createApprovalSchema = z.object({
type: z.enum(APPROVAL_TYPES),
requestedByAgentId: z.string().uuid().optional().nullable(),
payload: z.record(z.unknown()),
});
export type CreateApproval = z.infer<typeof createApprovalSchema>;
export const resolveApprovalSchema = z.object({
decisionNote: z.string().optional().nullable(),
decidedByUserId: z.string().optional().default("board"),
});
export type ResolveApproval = z.infer<typeof resolveApprovalSchema>;

View File

@@ -0,0 +1,19 @@
import { z } from "zod";
import { COMPANY_STATUSES } from "../constants.js";
export const createCompanySchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
});
export type CreateCompany = z.infer<typeof createCompanySchema>;
export const updateCompanySchema = createCompanySchema
.partial()
.extend({
status: z.enum(COMPANY_STATUSES).optional(),
spentMonthlyCents: z.number().int().nonnegative().optional(),
});
export type UpdateCompany = z.infer<typeof updateCompanySchema>;

View File

@@ -0,0 +1,23 @@
import { z } from "zod";
export const createCostEventSchema = z.object({
agentId: z.string().uuid(),
issueId: z.string().uuid().optional().nullable(),
projectId: z.string().uuid().optional().nullable(),
goalId: z.string().uuid().optional().nullable(),
billingCode: z.string().optional().nullable(),
provider: z.string().min(1),
model: z.string().min(1),
inputTokens: z.number().int().nonnegative().optional().default(0),
outputTokens: z.number().int().nonnegative().optional().default(0),
costCents: z.number().int().nonnegative(),
occurredAt: z.string().datetime(),
});
export type CreateCostEvent = z.infer<typeof createCostEventSchema>;
export const updateBudgetSchema = z.object({
budgetMonthlyCents: z.number().int().nonnegative(),
});
export type UpdateBudget = z.infer<typeof updateBudgetSchema>;

View File

@@ -1,12 +1,13 @@
import { z } from "zod";
import { GOAL_LEVELS } from "../constants.js";
import { GOAL_LEVELS, GOAL_STATUSES } from "../constants.js";
export const createGoalSchema = z.object({
title: z.string().min(1),
description: z.string().optional().nullable(),
level: z.enum(GOAL_LEVELS),
level: z.enum(GOAL_LEVELS).optional().default("task"),
status: z.enum(GOAL_STATUSES).optional().default("planned"),
parentId: z.string().uuid().optional().nullable(),
ownerId: z.string().uuid().optional().nullable(),
ownerAgentId: z.string().uuid().optional().nullable(),
});
export type CreateGoal = z.infer<typeof createGoalSchema>;

View File

@@ -1,8 +1,17 @@
export {
createCompanySchema,
updateCompanySchema,
type CreateCompany,
type UpdateCompany,
} from "./company.js";
export {
createAgentSchema,
updateAgentSchema,
createAgentKeySchema,
type CreateAgent,
type UpdateAgent,
type CreateAgentKey,
} from "./agent.js";
export {
@@ -15,8 +24,12 @@ export {
export {
createIssueSchema,
updateIssueSchema,
checkoutIssueSchema,
addIssueCommentSchema,
type CreateIssue,
type UpdateIssue,
type CheckoutIssue,
type AddIssueComment,
} from "./issue.js";
export {
@@ -25,3 +38,17 @@ export {
type CreateGoal,
type UpdateGoal,
} from "./goal.js";
export {
createApprovalSchema,
resolveApprovalSchema,
type CreateApproval,
type ResolveApproval,
} from "./approval.js";
export {
createCostEventSchema,
updateBudgetSchema,
type CreateCostEvent,
type UpdateBudget,
} from "./cost.js";

View File

@@ -2,13 +2,16 @@ import { z } from "zod";
import { ISSUE_PRIORITIES, ISSUE_STATUSES } from "../constants.js";
export const createIssueSchema = z.object({
projectId: z.string().uuid().optional().nullable(),
goalId: z.string().uuid().optional().nullable(),
parentId: z.string().uuid().optional().nullable(),
title: z.string().min(1),
description: z.string().optional().nullable(),
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
projectId: z.string().uuid().optional().nullable(),
assigneeId: z.string().uuid().optional().nullable(),
goalId: z.string().uuid().optional().nullable(),
assigneeAgentId: z.string().uuid().optional().nullable(),
requestDepth: z.number().int().nonnegative().optional().default(0),
billingCode: z.string().optional().nullable(),
});
export type CreateIssue = z.infer<typeof createIssueSchema>;
@@ -16,3 +19,16 @@ export type CreateIssue = z.infer<typeof createIssueSchema>;
export const updateIssueSchema = createIssueSchema.partial();
export type UpdateIssue = z.infer<typeof updateIssueSchema>;
export const checkoutIssueSchema = z.object({
agentId: z.string().uuid(),
expectedStatuses: z.array(z.enum(ISSUE_STATUSES)).nonempty(),
});
export type CheckoutIssue = z.infer<typeof checkoutIssueSchema>;
export const addIssueCommentSchema = z.object({
body: z.string().min(1),
});
export type AddIssueComment = z.infer<typeof addIssueCommentSchema>;

View File

@@ -1,8 +1,13 @@
import { z } from "zod";
import { PROJECT_STATUSES } from "../constants.js";
export const createProjectSchema = z.object({
goalId: z.string().uuid().optional().nullable(),
name: z.string().min(1),
description: z.string().optional().nullable(),
status: z.enum(PROJECT_STATUSES).optional().default("backlog"),
leadAgentId: z.string().uuid().optional().nullable(),
targetDate: z.string().optional().nullable(),
});
export type CreateProject = z.infer<typeof createProjectSchema>;

1734
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff