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

@@ -0,0 +1,62 @@
---
status: resolved
trigger: "TRPCError: Initiative repository not available at requireInitiativeRepository (router.ts:247) on listInitiatives endpoint"
created: 2026-02-04T00:00:00Z
updated: 2026-02-04T00:00:00Z
resolved: 2026-02-04T00:00:00Z
---
## Current Focus
hypothesis: CONFIRMED - Server startup never creates database or repositories, so ctx.initiativeRepository is always undefined
test: traced full call chain from CLI -> CoordinationServer -> trpc-adapter -> createContext
expecting: N/A - confirmed
next_action: Fix by wiring database and repositories into server startup
## Symptoms
expected: Initiative page loads and displays list of initiatives via listInitiatives tRPC endpoint
actual: 500 Internal Server Error - "Initiative repository not available" thrown by requireInitiativeRepository at router.ts:247
errors: TRPCError: Initiative repository not available (code: INTERNAL_SERVER_ERROR, httpStatus: 500, path: listInitiatives)
reproduction: Load the initiative page in the browser
started: Never worked
## Eliminated
[none yet]
## Evidence
1. [2026-02-04 E1] `requireInitiativeRepository` at router.ts:245-253 throws when `ctx.initiativeRepository` is falsy.
2. [2026-02-04 E2] `TRPCContext.initiativeRepository` is typed as optional (`?`), set via `createContext()` in context.ts:85.
3. [2026-02-04 E3] `createContext` is called in production at `src/server/trpc-adapter.ts:82-88` -- only passes `eventBus`, `serverStartedAt`, `processCount`, `agentManager`. Does NOT pass `initiativeRepository`, `phaseRepository`, `planRepository`, `dispatchManager`, `coordinationManager`, or `phaseDispatchManager`.
4. [2026-02-04 E4] `TrpcAdapterOptions` interface (trpc-adapter.ts:17-26) only defines `eventBus`, `serverStartedAt`, `processCount`, `agentManager`. Missing all repository fields.
5. [2026-02-04 E5] `CoordinationServer.handleTrpc()` at server/index.ts:221-225 calls `createTrpcHandler` with only `eventBus`, `serverStartedAt`, `processCount`. No repositories passed.
6. [2026-02-04 E6] `CoordinationServer` class has no database or repository references at all.
7. [2026-02-04 E7] CLI `startServer()` at cli/index.ts:24-53 creates CoordinationServer with no database.
8. [2026-02-04 E8] `createDatabase()` exists at db/index.ts:20 but is never called in production server startup path.
9. [2026-02-04 E9] Test harness (test/harness.ts:408-450) shows correct wiring: creates db, all repositories, all managers, passes everything to createContext.
## Resolution
root_cause: Server startup path never created a database or repositories. The CLI startServer() created a CoordinationServer with only processManager, logManager, and eventBus. The tRPC adapter (trpc-adapter.ts) only passed eventBus, serverStartedAt, processCount, and agentManager to createContext(). The initiativeRepository (and all other repositories) were never instantiated or injected, so ctx.initiativeRepository was always undefined, causing requireInitiativeRepository() to throw on every call.
fix: |
1. Created src/db/ensure-schema.ts - shared schema initialization with individual CREATE TABLE IF NOT EXISTS statements
2. Extended TrpcAdapterOptions in src/server/trpc-adapter.ts to accept all repository/manager types and pass them to createContext()
3. Added ServerContextDeps type and contextDeps parameter to CoordinationServer constructor, spreads into createTrpcHandler() options
4. Updated CLI startServer() in src/cli/index.ts to create database, ensure schema, instantiate repositories, and pass them to CoordinationServer
5. Refactored src/db/repositories/drizzle/test-helpers.ts to use shared ensureSchema() instead of duplicating SQL
verification: |
- TypeScript compilation: zero errors (npx tsc --noEmit)
- All 452 tests pass, 0 failures (npx vitest run)
- ensureSchema correctly works with drizzle-orm by executing individual statements (fixed multi-statement SQL error)
files_changed:
- src/db/ensure-schema.ts (NEW - shared schema initialization)
- src/db/index.ts (added ensureSchema re-export)
- src/db/repositories/drizzle/test-helpers.ts (refactored to use shared ensureSchema)
- src/server/trpc-adapter.ts (extended options and context creation)
- src/server/index.ts (added ServerContextDeps, contextDeps to constructor, spread into handler)
- src/cli/index.ts (wired database, schema, and repositories into server startup)

View File

@@ -0,0 +1,62 @@
---
status: resolved
trigger: "TRPCError: Initiative repository not available at requireInitiativeRepository (router.ts:247) on listInitiatives endpoint"
created: 2026-02-04T00:00:00Z
updated: 2026-02-04T00:00:00Z
resolved: 2026-02-04T00:00:00Z
---
## Current Focus
hypothesis: CONFIRMED - Server startup never creates database or repositories, so ctx.initiativeRepository is always undefined
test: traced full call chain from CLI -> CoordinationServer -> trpc-adapter -> createContext
expecting: N/A - confirmed
next_action: Fix by wiring database and repositories into server startup
## Symptoms
expected: Initiative page loads and displays list of initiatives via listInitiatives tRPC endpoint
actual: 500 Internal Server Error - "Initiative repository not available" thrown by requireInitiativeRepository at router.ts:247
errors: TRPCError: Initiative repository not available (code: INTERNAL_SERVER_ERROR, httpStatus: 500, path: listInitiatives)
reproduction: Load the initiative page in the browser
started: Never worked
## Eliminated
[none yet]
## Evidence
1. [2026-02-04 E1] `requireInitiativeRepository` at router.ts:245-253 throws when `ctx.initiativeRepository` is falsy.
2. [2026-02-04 E2] `TRPCContext.initiativeRepository` is typed as optional (`?`), set via `createContext()` in context.ts:85.
3. [2026-02-04 E3] `createContext` is called in production at `src/server/trpc-adapter.ts:82-88` -- only passes `eventBus`, `serverStartedAt`, `processCount`, `agentManager`. Does NOT pass `initiativeRepository`, `phaseRepository`, `planRepository`, `dispatchManager`, `coordinationManager`, or `phaseDispatchManager`.
4. [2026-02-04 E4] `TrpcAdapterOptions` interface (trpc-adapter.ts:17-26) only defines `eventBus`, `serverStartedAt`, `processCount`, `agentManager`. Missing all repository fields.
5. [2026-02-04 E5] `CoordinationServer.handleTrpc()` at server/index.ts:221-225 calls `createTrpcHandler` with only `eventBus`, `serverStartedAt`, `processCount`. No repositories passed.
6. [2026-02-04 E6] `CoordinationServer` class has no database or repository references at all.
7. [2026-02-04 E7] CLI `startServer()` at cli/index.ts:24-53 creates CoordinationServer with no database.
8. [2026-02-04 E8] `createDatabase()` exists at db/index.ts:20 but is never called in production server startup path.
9. [2026-02-04 E9] Test harness (test/harness.ts:408-450) shows correct wiring: creates db, all repositories, all managers, passes everything to createContext.
## Resolution
root_cause: Server startup path never created a database or repositories. The CLI startServer() created a CoordinationServer with only processManager, logManager, and eventBus. The tRPC adapter (trpc-adapter.ts) only passed eventBus, serverStartedAt, processCount, and agentManager to createContext(). The initiativeRepository (and all other repositories) were never instantiated or injected, so ctx.initiativeRepository was always undefined, causing requireInitiativeRepository() to throw on every call.
fix: |
1. Created src/db/ensure-schema.ts - shared schema initialization with individual CREATE TABLE IF NOT EXISTS statements
2. Extended TrpcAdapterOptions in src/server/trpc-adapter.ts to accept all repository/manager types and pass them to createContext()
3. Added ServerContextDeps type and contextDeps parameter to CoordinationServer constructor, spreads into createTrpcHandler() options
4. Updated CLI startServer() in src/cli/index.ts to create database, ensure schema, instantiate repositories, and pass them to CoordinationServer
5. Refactored src/db/repositories/drizzle/test-helpers.ts to use shared ensureSchema() instead of duplicating SQL
verification: |
- TypeScript compilation: zero errors (npx tsc --noEmit)
- All 452 tests pass, 0 failures (npx vitest run)
- ensureSchema correctly works with drizzle-orm by executing individual statements (fixed multi-statement SQL error)
files_changed:
- src/db/ensure-schema.ts (NEW - shared schema initialization)
- src/db/index.ts (added ensureSchema re-export)
- src/db/repositories/drizzle/test-helpers.ts (refactored to use shared ensureSchema)
- src/server/trpc-adapter.ts (extended options and context creation)
- src/server/index.ts (added ServerContextDeps, contextDeps to constructor, spread into handler)
- src/cli/index.ts (wired database, schema, and repositories into server startup)

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