Files
Codewalkers/apps/server/db/schema.ts
Lukas May 28521e1c20 chore: merge main into cw/small-change-flow
Integrates main branch changes (headquarters dashboard, task retry count,
agent prompt persistence, remote sync improvements) with the initiative's
errand agent feature. Both features coexist in the merged result.

Key resolutions:
- Schema: take main's errands table (nullable projectId, no conflictFiles,
  with errandsRelations); migrate to 0035_faulty_human_fly
- Router: keep both errandProcedures and headquartersProcedures
- Errand prompt: take main's simpler version (no question-asking flow)
- Manager: take main's status check (running|idle only, no waiting_for_input)
- Tests: update to match removed conflictFiles field and undefined vs null
2026-03-06 16:48:12 +01:00

665 lines
26 KiB
TypeScript

/**
* Database schema for Codewalkers.
*
* Defines the three-level task hierarchy:
* - Initiative: Top-level project
* - Phase: Major milestone within initiative
* - Task: Individual work item (can have parentTaskId for decomposition relationships)
*
* Plus a task_dependencies table for task dependency relationships.
*/
import { sqliteTable, text, integer, uniqueIndex, index } from 'drizzle-orm/sqlite-core';
import { relations, type InferInsertModel, type InferSelectModel } from 'drizzle-orm';
// ============================================================================
// INITIATIVES
// ============================================================================
export const initiatives = sqliteTable('initiatives', {
id: text('id').primaryKey(),
name: text('name').notNull(),
status: text('status', { enum: ['active', 'completed', 'archived', 'pending_review'] })
.notNull()
.default('active'),
branch: text('branch'), // Auto-generated initiative branch (e.g., 'cw/user-auth')
executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] })
.notNull()
.default('review_per_phase'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const initiativesRelations = relations(initiatives, ({ many }) => ({
phases: many(phases),
pages: many(pages),
initiativeProjects: many(initiativeProjects),
tasks: many(tasks),
changeSets: many(changeSets),
}));
export type Initiative = InferSelectModel<typeof initiatives>;
export type NewInitiative = InferInsertModel<typeof initiatives>;
// ============================================================================
// PHASES
// ============================================================================
export const phases = sqliteTable('phases', {
id: text('id').primaryKey(),
initiativeId: text('initiative_id')
.notNull()
.references(() => initiatives.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
content: text('content'),
status: text('status', { enum: ['pending', 'approved', 'in_progress', 'completed', 'blocked', 'pending_review'] })
.notNull()
.default('pending'),
mergeBase: text('merge_base'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const phasesRelations = relations(phases, ({ one, many }) => ({
initiative: one(initiatives, {
fields: [phases.initiativeId],
references: [initiatives.id],
}),
tasks: many(tasks),
// Dependencies: phases this phase depends on
dependsOn: many(phaseDependencies, { relationName: 'dependentPhase' }),
// Dependents: phases that depend on this phase
dependents: many(phaseDependencies, { relationName: 'dependencyPhase' }),
}));
export type Phase = InferSelectModel<typeof phases>;
export type NewPhase = InferInsertModel<typeof phases>;
// ============================================================================
// PHASE DEPENDENCIES
// ============================================================================
export const phaseDependencies = sqliteTable('phase_dependencies', {
id: text('id').primaryKey(),
phaseId: text('phase_id')
.notNull()
.references(() => phases.id, { onDelete: 'cascade' }),
dependsOnPhaseId: text('depends_on_phase_id')
.notNull()
.references(() => phases.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
export const phaseDependenciesRelations = relations(phaseDependencies, ({ one }) => ({
phase: one(phases, {
fields: [phaseDependencies.phaseId],
references: [phases.id],
relationName: 'dependentPhase',
}),
dependsOnPhase: one(phases, {
fields: [phaseDependencies.dependsOnPhaseId],
references: [phases.id],
relationName: 'dependencyPhase',
}),
}));
export type PhaseDependency = InferSelectModel<typeof phaseDependencies>;
export type NewPhaseDependency = InferInsertModel<typeof phaseDependencies>;
// ============================================================================
// TASKS
// ============================================================================
/**
* Task category enum values.
* Defines what kind of work a task represents.
*/
export const TASK_CATEGORIES = [
'execute', // Standard execution task
'research', // Research/exploration task
'discuss', // Discussion/context gathering
'plan', // Plan initiative into phases
'detail', // Detail phase into tasks
'refine', // Refine/edit content
'verify', // Verification task
'merge', // Merge task
'review', // Review/approval task
] as const;
export type TaskCategory = (typeof TASK_CATEGORIES)[number];
export const tasks = sqliteTable('tasks', {
id: text('id').primaryKey(),
// Parent context - at least one should be set
phaseId: text('phase_id').references(() => phases.id, { onDelete: 'cascade' }),
initiativeId: text('initiative_id').references(() => initiatives.id, { onDelete: 'cascade' }),
// Parent task for detail hierarchy (child tasks link to parent detail task)
parentTaskId: text('parent_task_id').references((): ReturnType<typeof text> => tasks.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
type: text('type', {
enum: ['auto'],
})
.notNull()
.default('auto'),
category: text('category', {
enum: TASK_CATEGORIES,
})
.notNull()
.default('execute'),
priority: text('priority', { enum: ['low', 'medium', 'high'] })
.notNull()
.default('medium'),
status: text('status', {
enum: ['pending', 'in_progress', 'completed', 'blocked'],
})
.notNull()
.default('pending'),
order: integer('order').notNull().default(0),
summary: text('summary'), // Agent result summary — propagated to dependent tasks as context
retryCount: integer('retry_count').notNull().default(0),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const tasksRelations = relations(tasks, ({ one, many }) => ({
phase: one(phases, {
fields: [tasks.phaseId],
references: [phases.id],
}),
initiative: one(initiatives, {
fields: [tasks.initiativeId],
references: [initiatives.id],
}),
// Parent task (for detail hierarchy - child links to parent detail task)
parentTask: one(tasks, {
fields: [tasks.parentTaskId],
references: [tasks.id],
relationName: 'parentTask',
}),
// Child tasks (tasks created from decomposition of this task)
childTasks: many(tasks, { relationName: 'parentTask' }),
// Dependencies: tasks this task depends on
dependsOn: many(taskDependencies, { relationName: 'dependentTask' }),
// Dependents: tasks that depend on this task
dependents: many(taskDependencies, { relationName: 'dependencyTask' }),
}));
export type Task = InferSelectModel<typeof tasks>;
export type NewTask = InferInsertModel<typeof tasks>;
// ============================================================================
// TASK DEPENDENCIES
// ============================================================================
export const taskDependencies = sqliteTable('task_dependencies', {
id: text('id').primaryKey(),
taskId: text('task_id')
.notNull()
.references(() => tasks.id, { onDelete: 'cascade' }),
dependsOnTaskId: text('depends_on_task_id')
.notNull()
.references(() => tasks.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
export const taskDependenciesRelations = relations(taskDependencies, ({ one }) => ({
task: one(tasks, {
fields: [taskDependencies.taskId],
references: [tasks.id],
relationName: 'dependentTask',
}),
dependsOnTask: one(tasks, {
fields: [taskDependencies.dependsOnTaskId],
references: [tasks.id],
relationName: 'dependencyTask',
}),
}));
export type TaskDependency = InferSelectModel<typeof taskDependencies>;
export type NewTaskDependency = InferInsertModel<typeof taskDependencies>;
// ============================================================================
// ACCOUNTS
// ============================================================================
export const accounts = sqliteTable('accounts', {
id: text('id').primaryKey(),
email: text('email').notNull(),
provider: text('provider').notNull().default('claude'),
configJson: text('config_json'), // .claude.json content (JSON string)
credentials: text('credentials'), // .credentials.json content (JSON string)
isExhausted: integer('is_exhausted', { mode: 'boolean' }).notNull().default(false),
exhaustedUntil: integer('exhausted_until', { mode: 'timestamp' }),
lastUsedAt: integer('last_used_at', { mode: 'timestamp' }),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const accountsRelations = relations(accounts, ({ many }) => ({
agents: many(agents),
}));
export type Account = InferSelectModel<typeof accounts>;
export type NewAccount = InferInsertModel<typeof accounts>;
// ============================================================================
// AGENTS
// ============================================================================
export const agents = sqliteTable('agents', {
id: text('id').primaryKey(),
name: text('name').notNull().unique(), // Human-readable alias (e.g., 'jolly-penguin')
taskId: text('task_id').references(() => tasks.id, { onDelete: 'set null' }), // Task may be deleted
initiativeId: text('initiative_id').references(() => initiatives.id, { onDelete: 'set null' }),
sessionId: text('session_id'), // Claude CLI session ID for resumption (null until first run completes)
worktreeId: text('worktree_id').notNull(), // Agent alias (deterministic path: agent-workdirs/<alias>/)
provider: text('provider').notNull().default('claude'),
accountId: text('account_id').references(() => accounts.id, { onDelete: 'set null' }),
status: text('status', {
enum: ['idle', 'running', 'waiting_for_input', 'stopped', 'crashed'],
})
.notNull()
.default('idle'),
mode: text('mode', { enum: ['execute', 'discuss', 'plan', 'detail', 'refine', 'chat', 'errand'] })
.notNull()
.default('execute'),
pid: integer('pid'),
exitCode: integer('exit_code'), // Process exit code for debugging crashes
prompt: text('prompt'), // Full assembled prompt passed to the agent process (persisted for durability after log cleanup)
outputFilePath: text('output_file_path'),
result: text('result'),
pendingQuestions: text('pending_questions'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
userDismissedAt: integer('user_dismissed_at', { mode: 'timestamp' }),
});
export const agentsRelations = relations(agents, ({ one, many }) => ({
task: one(tasks, {
fields: [agents.taskId],
references: [tasks.id],
}),
initiative: one(initiatives, {
fields: [agents.initiativeId],
references: [initiatives.id],
}),
account: one(accounts, {
fields: [agents.accountId],
references: [accounts.id],
}),
changeSets: many(changeSets),
}));
export type Agent = InferSelectModel<typeof agents>;
export type NewAgent = InferInsertModel<typeof agents>;
// ============================================================================
// CHANGE SETS
// ============================================================================
export const changeSets = sqliteTable('change_sets', {
id: text('id').primaryKey(),
agentId: text('agent_id')
.references(() => agents.id, { onDelete: 'set null' }),
agentName: text('agent_name').notNull(),
initiativeId: text('initiative_id')
.notNull()
.references(() => initiatives.id, { onDelete: 'cascade' }),
mode: text('mode', { enum: ['plan', 'detail', 'refine', 'chat'] }).notNull(),
summary: text('summary'),
status: text('status', { enum: ['applied', 'reverted'] })
.notNull()
.default('applied'),
revertedAt: integer('reverted_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('change_sets_initiative_id_idx').on(table.initiativeId),
]);
export const changeSetsRelations = relations(changeSets, ({ one, many }) => ({
agent: one(agents, {
fields: [changeSets.agentId],
references: [agents.id],
}),
initiative: one(initiatives, {
fields: [changeSets.initiativeId],
references: [initiatives.id],
}),
entries: many(changeSetEntries),
}));
export type ChangeSet = InferSelectModel<typeof changeSets>;
export type NewChangeSet = InferInsertModel<typeof changeSets>;
export const changeSetEntries = sqliteTable('change_set_entries', {
id: text('id').primaryKey(),
changeSetId: text('change_set_id')
.notNull()
.references(() => changeSets.id, { onDelete: 'cascade' }),
entityType: text('entity_type', { enum: ['page', 'phase', 'task', 'phase_dependency', 'task_dependency'] }).notNull(),
entityId: text('entity_id').notNull(),
action: text('action', { enum: ['create', 'update', 'delete'] }).notNull(),
previousState: text('previous_state'), // JSON snapshot, null for creates
newState: text('new_state'), // JSON snapshot, null for deletes
sortOrder: integer('sort_order').notNull().default(0),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('change_set_entries_change_set_id_idx').on(table.changeSetId),
]);
export const changeSetEntriesRelations = relations(changeSetEntries, ({ one }) => ({
changeSet: one(changeSets, {
fields: [changeSetEntries.changeSetId],
references: [changeSets.id],
}),
}));
export type ChangeSetEntry = InferSelectModel<typeof changeSetEntries>;
export type NewChangeSetEntry = InferInsertModel<typeof changeSetEntries>;
// ============================================================================
// MESSAGES
// ============================================================================
export const messages = sqliteTable('messages', {
id: text('id').primaryKey(),
senderType: text('sender_type', { enum: ['agent', 'user'] }).notNull(),
senderId: text('sender_id').references(() => agents.id, { onDelete: 'set null' }), // Agent ID if senderType='agent', null for user
recipientType: text('recipient_type', { enum: ['agent', 'user'] }).notNull(),
recipientId: text('recipient_id').references(() => agents.id, { onDelete: 'set null' }), // Agent ID if recipientType='agent', null for user
type: text('type', { enum: ['question', 'info', 'error', 'response'] })
.notNull()
.default('info'),
content: text('content').notNull(),
requiresResponse: integer('requires_response', { mode: 'boolean' })
.notNull()
.default(false),
status: text('status', { enum: ['pending', 'read', 'responded'] })
.notNull()
.default('pending'),
parentMessageId: text('parent_message_id').references((): ReturnType<typeof text> => messages.id, { onDelete: 'set null' }), // Links response to original question
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const messagesRelations = relations(messages, ({ one, many }) => ({
// Sender agent (optional - null for user senders)
senderAgent: one(agents, {
fields: [messages.senderId],
references: [agents.id],
relationName: 'senderAgent',
}),
// Recipient agent (optional - null for user recipients)
recipientAgent: one(agents, {
fields: [messages.recipientId],
references: [agents.id],
relationName: 'recipientAgent',
}),
// Parent message (for threading responses)
parentMessage: one(messages, {
fields: [messages.parentMessageId],
references: [messages.id],
relationName: 'parentMessage',
}),
// Child messages (replies to this message)
childMessages: many(messages, { relationName: 'parentMessage' }),
}));
export type Message = InferSelectModel<typeof messages>;
export type NewMessage = InferInsertModel<typeof messages>;
// ============================================================================
// PAGES
// ============================================================================
export const pages = sqliteTable('pages', {
id: text('id').primaryKey(),
initiativeId: text('initiative_id')
.notNull()
.references(() => initiatives.id, { onDelete: 'cascade' }),
parentPageId: text('parent_page_id').references((): ReturnType<typeof text> => pages.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
content: text('content'), // JSON string from Tiptap
sortOrder: integer('sort_order').notNull().default(0),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const pagesRelations = relations(pages, ({ one, many }) => ({
initiative: one(initiatives, {
fields: [pages.initiativeId],
references: [initiatives.id],
}),
parentPage: one(pages, {
fields: [pages.parentPageId],
references: [pages.id],
relationName: 'parentPage',
}),
childPages: many(pages, { relationName: 'parentPage' }),
}));
export type Page = InferSelectModel<typeof pages>;
export type NewPage = InferInsertModel<typeof pages>;
// ============================================================================
// PROJECTS
// ============================================================================
export const projects = sqliteTable('projects', {
id: text('id').primaryKey(),
name: text('name').notNull().unique(),
url: text('url').notNull().unique(),
defaultBranch: text('default_branch').notNull().default('main'),
lastFetchedAt: integer('last_fetched_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const projectsRelations = relations(projects, ({ many }) => ({
initiativeProjects: many(initiativeProjects),
}));
export type Project = InferSelectModel<typeof projects>;
export type NewProject = InferInsertModel<typeof projects>;
// ============================================================================
// INITIATIVE PROJECTS (junction)
// ============================================================================
export const initiativeProjects = sqliteTable('initiative_projects', {
id: text('id').primaryKey(),
initiativeId: text('initiative_id')
.notNull()
.references(() => initiatives.id, { onDelete: 'cascade' }),
projectId: text('project_id')
.notNull()
.references(() => projects.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
uniqueIndex('initiative_project_unique').on(table.initiativeId, table.projectId),
]);
export const initiativeProjectsRelations = relations(initiativeProjects, ({ one }) => ({
initiative: one(initiatives, {
fields: [initiativeProjects.initiativeId],
references: [initiatives.id],
}),
project: one(projects, {
fields: [initiativeProjects.projectId],
references: [projects.id],
}),
}));
export type InitiativeProject = InferSelectModel<typeof initiativeProjects>;
export type NewInitiativeProject = InferInsertModel<typeof initiativeProjects>;
// ============================================================================
// AGENT LOG CHUNKS
// ============================================================================
export const agentLogChunks = sqliteTable('agent_log_chunks', {
id: text('id').primaryKey(),
agentId: text('agent_id').notNull(), // NO FK — survives agent deletion
agentName: text('agent_name').notNull(), // Snapshot for display after deletion
sessionNumber: integer('session_number').notNull().default(1),
content: text('content').notNull(), // Raw JSONL chunk from file
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('agent_log_chunks_agent_id_idx').on(table.agentId),
]);
export type AgentLogChunk = InferSelectModel<typeof agentLogChunks>;
export type NewAgentLogChunk = InferInsertModel<typeof agentLogChunks>;
// ============================================================================
// CONVERSATIONS (inter-agent communication)
// ============================================================================
export const conversations = sqliteTable('conversations', {
id: text('id').primaryKey(),
fromAgentId: text('from_agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
toAgentId: text('to_agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
initiativeId: text('initiative_id').references(() => initiatives.id, { onDelete: 'set null' }),
phaseId: text('phase_id').references(() => phases.id, { onDelete: 'set null' }),
taskId: text('task_id').references(() => tasks.id, { onDelete: 'set null' }),
question: text('question').notNull(),
answer: text('answer'),
status: text('status', { enum: ['pending', 'answered'] }).notNull().default('pending'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('conversations_to_agent_status_idx').on(table.toAgentId, table.status),
index('conversations_from_agent_idx').on(table.fromAgentId),
]);
export type Conversation = InferSelectModel<typeof conversations>;
export type NewConversation = InferInsertModel<typeof conversations>;
// ============================================================================
// CHAT SESSIONS
// ============================================================================
export const chatSessions = sqliteTable('chat_sessions', {
id: text('id').primaryKey(),
targetType: text('target_type', { enum: ['phase', 'task'] }).notNull(),
targetId: text('target_id').notNull(),
initiativeId: text('initiative_id')
.notNull()
.references(() => initiatives.id, { onDelete: 'cascade' }),
agentId: text('agent_id').references(() => agents.id, { onDelete: 'set null' }),
status: text('status', { enum: ['active', 'closed'] })
.notNull()
.default('active'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('chat_sessions_target_idx').on(table.targetType, table.targetId),
index('chat_sessions_initiative_id_idx').on(table.initiativeId),
]);
export const chatSessionsRelations = relations(chatSessions, ({ one, many }) => ({
initiative: one(initiatives, {
fields: [chatSessions.initiativeId],
references: [initiatives.id],
}),
agent: one(agents, {
fields: [chatSessions.agentId],
references: [agents.id],
}),
messages: many(chatMessages),
}));
export type ChatSession = InferSelectModel<typeof chatSessions>;
export type NewChatSession = InferInsertModel<typeof chatSessions>;
// ============================================================================
// CHAT MESSAGES
// ============================================================================
export const chatMessages = sqliteTable('chat_messages', {
id: text('id').primaryKey(),
chatSessionId: text('chat_session_id')
.notNull()
.references(() => chatSessions.id, { onDelete: 'cascade' }),
role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(),
content: text('content').notNull(),
changeSetId: text('change_set_id').references(() => changeSets.id, { onDelete: 'set null' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('chat_messages_session_id_idx').on(table.chatSessionId),
]);
export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
chatSession: one(chatSessions, {
fields: [chatMessages.chatSessionId],
references: [chatSessions.id],
}),
changeSet: one(changeSets, {
fields: [chatMessages.changeSetId],
references: [changeSets.id],
}),
}));
export type ChatMessage = InferSelectModel<typeof chatMessages>;
export type NewChatMessage = InferInsertModel<typeof chatMessages>;
// ============================================================================
// REVIEW COMMENTS
// ============================================================================
export const reviewComments = sqliteTable('review_comments', {
id: text('id').primaryKey(),
phaseId: text('phase_id')
.notNull()
.references(() => phases.id, { onDelete: 'cascade' }),
filePath: text('file_path').notNull(),
lineNumber: integer('line_number').notNull(),
lineType: text('line_type', { enum: ['added', 'removed', 'context'] }).notNull(),
body: text('body').notNull(),
author: text('author').notNull().default('you'),
parentCommentId: text('parent_comment_id').references((): ReturnType<typeof text> => reviewComments.id, { onDelete: 'cascade' }),
resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
}, (table) => [
index('review_comments_phase_id_idx').on(table.phaseId),
index('review_comments_parent_id_idx').on(table.parentCommentId),
]);
export type ReviewComment = InferSelectModel<typeof reviewComments>;
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
// ============================================================================
// ERRANDS
// ============================================================================
export const errands = sqliteTable('errands', {
id: text('id').primaryKey(),
description: text('description').notNull(),
branch: text('branch').notNull(),
baseBranch: text('base_branch').notNull().default('main'),
agentId: text('agent_id').references(() => agents.id, { onDelete: 'set null' }),
projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }),
status: text('status', {
enum: ['active', 'pending_review', 'conflict', 'merged', 'abandoned'],
}).notNull().default('active'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const errandsRelations = relations(errands, ({ one }) => ({
agent: one(agents, {
fields: [errands.agentId],
references: [agents.id],
}),
project: one(projects, {
fields: [errands.projectId],
references: [projects.id],
}),
}));
export type Errand = InferSelectModel<typeof errands>;
export type NewErrand = InferInsertModel<typeof errands>;