fix: wire database and repositories into server startup for tRPC context

The server never created a database or instantiated repositories, so all
tRPC procedures requiring initiativeRepository (and other repos) threw
"Initiative repository not available" on every request.

- Create ensureSchema() for shared database table initialization
- Extend TrpcAdapterOptions to accept all repository/manager dependencies
- Add ServerContextDeps to CoordinationServer for dependency injection
- Wire database, schema, and repositories in CLI startServer()
- Refactor test-helpers to use shared ensureSchema()
This commit is contained in:
Lukas May
2026-02-04 21:18:30 +01:00
parent 078e1dde30
commit cbf0ed28cb
8 changed files with 327 additions and 105 deletions

View File

@@ -13,6 +13,15 @@ import { ProcessManager, ProcessRegistry } from '../process/index.js';
import { LogManager } from '../logging/index.js';
import { createEventBus } from '../events/index.js';
import { createDefaultTrpcClient } from './trpc-client.js';
import {
createDatabase,
ensureSchema,
DrizzleInitiativeRepository,
DrizzlePhaseRepository,
DrizzlePlanRepository,
DrizzleTaskRepository,
DrizzleMessageRepository,
} from '../db/index.js';
/** Environment variable for custom port */
const CW_PORT_ENV = 'CW_PORT';
@@ -32,12 +41,30 @@ async function startServer(port?: number): Promise<void> {
const processManager = new ProcessManager(registry, eventBus);
const logManager = new LogManager();
// Create database and ensure schema
const db = createDatabase();
ensureSchema(db);
// Create repositories
const initiativeRepository = new DrizzleInitiativeRepository(db);
const phaseRepository = new DrizzlePhaseRepository(db);
const planRepository = new DrizzlePlanRepository(db);
const taskRepository = new DrizzleTaskRepository(db);
const messageRepository = new DrizzleMessageRepository(db);
// Create and start server
const server = new CoordinationServer(
{ port: serverPort },
processManager,
logManager,
eventBus
eventBus,
{
initiativeRepository,
phaseRepository,
planRepository,
taskRepository,
messageRepository,
}
);
try {

123
src/db/ensure-schema.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* Database Schema Initialization
*
* Ensures all required tables exist in the database.
* Uses CREATE TABLE IF NOT EXISTS so it's safe to call multiple times.
*/
import type { DrizzleDatabase } from './index.js';
import { sql } from 'drizzle-orm';
/**
* Individual CREATE TABLE statements for each table.
* Each must be a single statement for drizzle-orm compatibility.
* These mirror the schema defined in schema.ts.
*/
const TABLE_STATEMENTS = [
// Initiatives table
`CREATE TABLE IF NOT EXISTS initiatives (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
// Phases table
`CREATE TABLE IF NOT EXISTS phases (
id TEXT PRIMARY KEY NOT NULL,
initiative_id TEXT NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
number INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
// Phase dependencies table
`CREATE TABLE IF NOT EXISTS phase_dependencies (
id TEXT PRIMARY KEY NOT NULL,
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
depends_on_phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL
)`,
// Plans table
`CREATE TABLE IF NOT EXISTS plans (
id TEXT PRIMARY KEY NOT NULL,
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
number INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
// Tasks table
`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY NOT NULL,
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL DEFAULT 'auto',
priority TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'pending',
"order" INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
// Task dependencies table
`CREATE TABLE IF NOT EXISTS task_dependencies (
id TEXT PRIMARY KEY NOT NULL,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
depends_on_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL
)`,
// Agents table
`CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
session_id TEXT,
worktree_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'idle',
mode TEXT NOT NULL DEFAULT 'execute' CHECK(mode IN ('execute', 'discuss', 'breakdown', 'decompose')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
// Messages table
`CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY NOT NULL,
sender_type TEXT NOT NULL,
sender_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
recipient_type TEXT NOT NULL,
recipient_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
type TEXT NOT NULL DEFAULT 'info',
content TEXT NOT NULL,
requires_response INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
parent_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`,
];
/**
* Ensure all database tables exist.
*
* Uses CREATE TABLE IF NOT EXISTS, so safe to call on every startup.
* Must be called before any repository operations on a fresh database.
*
* @param db - Drizzle database instance
*/
export function ensureSchema(db: DrizzleDatabase): void {
for (const statement of TABLE_STATEMENTS) {
db.run(sql.raw(statement));
}
}

View File

@@ -39,6 +39,9 @@ export function createDatabase(path?: string): DrizzleDatabase {
// Re-export config utilities
export { getDbPath, ensureDbDirectory } from './config.js';
// Re-export schema initialization
export { ensureSchema } from './ensure-schema.js';
// Re-export schema and types
export * from './schema.js';

View File

@@ -8,107 +8,9 @@
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import type { DrizzleDatabase } from '../../index.js';
import { ensureSchema } from '../../ensure-schema.js';
import * as schema from '../../schema.js';
/**
* SQL statements to create the database schema.
* These mirror the schema defined in schema.ts.
*/
const CREATE_TABLES_SQL = `
-- Initiatives table
CREATE TABLE IF NOT EXISTS initiatives (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Phases table
CREATE TABLE IF NOT EXISTS phases (
id TEXT PRIMARY KEY NOT NULL,
initiative_id TEXT NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
number INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Phase dependencies table
CREATE TABLE IF NOT EXISTS phase_dependencies (
id TEXT PRIMARY KEY NOT NULL,
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
depends_on_phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL
);
-- Plans table
CREATE TABLE IF NOT EXISTS plans (
id TEXT PRIMARY KEY NOT NULL,
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
number INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Tasks table
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY NOT NULL,
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL DEFAULT 'auto',
priority TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'pending',
"order" INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Task dependencies table
CREATE TABLE IF NOT EXISTS task_dependencies (
id TEXT PRIMARY KEY NOT NULL,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
depends_on_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL
);
-- Agents table
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
session_id TEXT,
worktree_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'idle',
mode TEXT NOT NULL DEFAULT 'execute' CHECK(mode IN ('execute', 'discuss', 'breakdown', 'decompose')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Messages table
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY NOT NULL,
sender_type TEXT NOT NULL,
sender_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
recipient_type TEXT NOT NULL,
recipient_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
type TEXT NOT NULL DEFAULT 'info',
content TEXT NOT NULL,
requires_response INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
parent_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`;
/**
* Create an in-memory test database with schema applied.
* Returns a fresh Drizzle instance for each call.
@@ -119,8 +21,10 @@ export function createTestDatabase(): DrizzleDatabase {
// Enable foreign keys
sqlite.pragma('foreign_keys = ON');
// Create all tables
sqlite.exec(CREATE_TABLES_SQL);
const db = drizzle(sqlite, { schema });
return drizzle(sqlite, { schema });
// Create all tables
ensureSchema(db);
return db;
}

View File

@@ -15,7 +15,13 @@ import type { ServerConfig, ServerState, HealthResponse, StatusResponse } from '
import type { ProcessManager } from '../process/index.js';
import type { LogManager } from '../logging/index.js';
import type { EventBus, ServerStartedEvent, ServerStoppedEvent } from '../events/index.js';
import { createTrpcHandler } from './trpc-adapter.js';
import { createTrpcHandler, type TrpcAdapterOptions } from './trpc-adapter.js';
/**
* Optional dependencies for tRPC context.
* Passed through to the tRPC adapter for procedure access.
*/
export type ServerContextDeps = Omit<TrpcAdapterOptions, 'eventBus' | 'serverStartedAt' | 'processCount'>;
/** Default port for the coordination server */
const DEFAULT_PORT = 3847;
@@ -38,6 +44,7 @@ export class CoordinationServer {
private readonly processManager: ProcessManager;
private readonly logManager: LogManager;
private readonly eventBus: EventBus | undefined;
private readonly contextDeps: ServerContextDeps;
private server: Server | null = null;
private state: ServerState | null = null;
@@ -45,7 +52,8 @@ export class CoordinationServer {
config: Partial<ServerConfig>,
processManager: ProcessManager,
logManager: LogManager,
eventBus?: EventBus
eventBus?: EventBus,
contextDeps?: ServerContextDeps
) {
this.config = {
port: config.port ?? DEFAULT_PORT,
@@ -55,6 +63,7 @@ export class CoordinationServer {
this.processManager = processManager;
this.logManager = logManager;
this.eventBus = eventBus;
this.contextDeps = contextDeps ?? {};
}
/**
@@ -222,6 +231,7 @@ export class CoordinationServer {
eventBus: this.eventBus,
serverStartedAt: this.state.startedAt,
processCount: this.state.processCount,
...this.contextDeps,
});
trpcHandler(req, res).catch((error: Error) => {

View File

@@ -10,6 +10,13 @@ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter, createContext } from '../trpc/index.js';
import type { EventBus } from '../events/index.js';
import type { AgentManager } from '../agent/types.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { MessageRepository } from '../db/repositories/message-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { PlanRepository } from '../db/repositories/plan-repository.js';
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
import type { CoordinationManager } from '../coordination/types.js';
/**
* Options for creating the tRPC request handler.
@@ -23,6 +30,22 @@ export interface TrpcAdapterOptions {
processCount: number;
/** Agent manager for agent lifecycle operations (optional until full wiring) */
agentManager?: AgentManager;
/** Task repository for task CRUD operations */
taskRepository?: TaskRepository;
/** Message repository for agent-user communication */
messageRepository?: MessageRepository;
/** Initiative repository for initiative CRUD operations */
initiativeRepository?: InitiativeRepository;
/** Phase repository for phase CRUD operations */
phaseRepository?: PhaseRepository;
/** Plan repository for plan CRUD operations */
planRepository?: PlanRepository;
/** Dispatch manager for task queue operations */
dispatchManager?: DispatchManager;
/** Coordination manager for merge queue operations */
coordinationManager?: CoordinationManager;
/** Phase dispatch manager for phase queue operations */
phaseDispatchManager?: PhaseDispatchManager;
}
/**
@@ -85,6 +108,14 @@ export function createTrpcHandler(options: TrpcAdapterOptions) {
serverStartedAt: options.serverStartedAt,
processCount: options.processCount,
agentManager: options.agentManager,
taskRepository: options.taskRepository,
messageRepository: options.messageRepository,
initiativeRepository: options.initiativeRepository,
phaseRepository: options.phaseRepository,
planRepository: options.planRepository,
dispatchManager: options.dispatchManager,
coordinationManager: options.coordinationManager,
phaseDispatchManager: options.phaseDispatchManager,
}),
});