Update all user-facing strings (HTML title, manifest, header logo, browser title updater), code comments, and documentation references. Folder name retained as-is.
632 lines
25 KiB
TypeScript
632 lines
25 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'] })
|
|
.notNull()
|
|
.default('active'),
|
|
mergeRequiresApproval: integer('merge_requires_approval', { mode: 'boolean' })
|
|
.notNull()
|
|
.default(true),
|
|
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'),
|
|
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', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action'],
|
|
})
|
|
.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_approval', 'pending', 'in_progress', 'completed', 'blocked'],
|
|
})
|
|
.notNull()
|
|
.default('pending'),
|
|
requiresApproval: integer('requires_approval', { mode: 'boolean' }), // null = inherit from initiative
|
|
order: integer('order').notNull().default(0),
|
|
summary: text('summary'), // Agent result summary — propagated to dependent tasks as context
|
|
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'] })
|
|
.notNull()
|
|
.default('execute'),
|
|
pid: integer('pid'),
|
|
exitCode: integer('exit_code'), // Process exit code for debugging crashes
|
|
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'),
|
|
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),
|
|
]);
|
|
|
|
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
|
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|