Files
Codewalkers/docs/database.md
Lukas May 7e6921f01e feat: enrich listWaitingAgents with task/phase/initiative context via DB joins
Replaces the in-memory filter (agentManager.list() + filter) with a direct
repository query that LEFT JOINs tasks, phases, and initiatives to return
taskName, phaseName, initiativeName, and taskDescription alongside agent fields.

- Adds AgentWithContext interface and findWaitingWithContext() to AgentRepository port
- Implements findWaitingWithContext() in DrizzleAgentRepository using getTableColumns
- Wires agentRepository into TRPCContext, CreateContextOptions, and TrpcAdapterOptions
- Adds requireAgentRepository() helper following existing pattern
- Updates listWaitingAgents to use repository query instead of agentManager
- Adds 5 unit tests for findWaitingWithContext() covering all FK join edge cases
- Updates existing AgentRepository mocks to satisfy updated interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:29:49 +01:00

267 lines
11 KiB
Markdown

# Database Module
`apps/server/db/` — SQLite database via better-sqlite3 + Drizzle ORM with hexagonal architecture.
## Architecture
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 14 repository interfaces
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 14 Drizzle adapters
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
## Tables
### initiatives
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| name | text NOT NULL | |
| status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' |
| branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') |
| executionMode | text enum | 'yolo' \| 'review_per_phase', default 'review_per_phase' |
| qualityReview | integer (boolean) | default false; flags initiative for quality review workflow |
| createdAt, updatedAt | integer/timestamp | |
### phases
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| initiativeId | text FK → initiatives (cascade) | |
| name | text NOT NULL | |
| content | text nullable | Tiptap JSON |
| status | text enum | 'pending' \| 'approved' \| 'in_progress' \| 'completed' \| 'blocked' \| 'pending_review' |
| mergeBase | text nullable | git merge-base hash stored before phase merge, enables diff reconstruction for completed phases |
| createdAt, updatedAt | integer/timestamp | |
### phase_dependencies
`phaseId` FK → phases, `dependsOnPhaseId` FK → phases. Both cascade delete.
### tasks
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| phaseId | text nullable FK → phases (cascade) | |
| initiativeId | text nullable FK → initiatives (cascade) | |
| parentTaskId | text nullable self-ref FK (cascade) | decomposition hierarchy |
| name | text NOT NULL | |
| description | text nullable | |
| type | text enum | 'auto' |
| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' |
| priority | text enum | 'low' \| 'medium' \| 'high' |
| status | text enum | 'pending' \| 'in_progress' \| 'quality_review' \| 'completed' \| 'blocked' |
| order | integer | default 0 |
| summary | text nullable | Agent result summary — propagated to dependent tasks as context |
| retryCount | integer NOT NULL | default 0, incremented on agent crash auto-retry, reset on manual retry |
| createdAt, updatedAt | integer/timestamp | |
### task_dependencies
`taskId` FK → tasks, `dependsOnTaskId` FK → tasks. Both cascade delete.
### agents
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| name | text NOT NULL UNIQUE | human-readable alias (adjective-animal) |
| taskId | text nullable FK → tasks (set null) | |
| initiativeId | text nullable FK → initiatives (set null) | |
| sessionId | text nullable | CLI session ID for resume |
| worktreeId | text NOT NULL | path to agent's git worktree |
| provider | text NOT NULL | default 'claude' |
| accountId | text nullable FK → accounts (set null) | |
| status | text enum | 'idle' \| 'running' \| 'waiting_for_input' \| 'stopped' \| 'crashed' |
| mode | text enum | 'execute' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' |
| pid | integer nullable | OS process ID |
| exitCode | integer nullable | |
| prompt | text nullable | Full assembled prompt passed to agent at spawn; persisted for durability after log cleanup |
| outputFilePath | text nullable | |
| result | text nullable | JSON |
| pendingQuestions | text nullable | JSON |
| userDismissedAt | integer/timestamp nullable | |
| createdAt, updatedAt | integer/timestamp | |
### accounts
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| email | text NOT NULL | |
| provider | text NOT NULL | default 'claude' |
| configJson | text nullable | serialized .claude.json |
| credentials | text nullable | serialized .credentials.json |
| isExhausted | integer/boolean | default false |
| exhaustedUntil | integer/timestamp nullable | |
| lastUsedAt | integer/timestamp nullable | round-robin scheduling |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### proposals
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| agentId | text FK → agents (cascade) | |
| initiativeId | text FK → initiatives (cascade) | |
| targetType | text enum | 'page' \| 'phase' \| 'task' |
| targetId | text nullable | existing entity ID, null for creates |
| title, summary, content | text | markdown body |
| metadata | text nullable | JSON |
| status | text enum | 'pending' \| 'accepted' \| 'dismissed' |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### pages
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| initiativeId | text FK → initiatives (cascade) | |
| parentPageId | text nullable self-ref FK (cascade) | root page has NULL |
| title | text NOT NULL | |
| content | text nullable | Tiptap JSON |
| sortOrder | integer | |
| createdAt, updatedAt | integer/timestamp | |
### projects
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| name | text NOT NULL UNIQUE | |
| url | text NOT NULL UNIQUE | git repo URL |
| defaultBranch | text NOT NULL | default 'main' |
| lastFetchedAt | integer/timestamp | nullable, updated by ProjectSyncManager |
| createdAt, updatedAt | integer/timestamp | |
### initiative_projects (junction)
`initiativeId` + `projectId` with unique index. Both FK cascade.
### messages
Self-referencing (parentMessageId) for threading. Sender/recipient types: 'agent' | 'user'.
### agent_log_chunks
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| agentId | text NOT NULL | **NO FK** — survives agent deletion |
| agentName | text NOT NULL | snapshot for display |
| sessionNumber | integer | spawn=1, resume=prev+1 |
| content | text NOT NULL | raw JSONL chunk |
| createdAt | integer/timestamp | |
Index on `agentId` for fast queries.
### conversations
Inter-agent communication records.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | |
| fromAgentId | text NOT NULL | FK→agents ON DELETE CASCADE |
| toAgentId | text NOT NULL | FK→agents ON DELETE CASCADE |
| initiativeId | text | FK→initiatives ON DELETE SET NULL |
| phaseId | text | FK→phases ON DELETE SET NULL |
| taskId | text | FK→tasks ON DELETE SET NULL |
| question | text NOT NULL | |
| answer | text | nullable until answered |
| status | text enum | pending, answered |
| createdAt | integer/timestamp | |
| updatedAt | integer/timestamp | |
Indexes: `(toAgentId, status)` for listen polling, `(fromAgentId)`.
### chat_sessions
Persistent chat sessions for iterative refinement of phases/tasks.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| targetType | text enum | 'phase' \| 'task' |
| targetId | text NOT NULL | phase or task ID |
| initiativeId | text FK → initiatives (cascade) | |
| agentId | text FK → agents (set null) | linked agent |
| status | text enum | 'active' \| 'closed', default 'active' |
| createdAt, updatedAt | integer/timestamp | |
Indexes: `(targetType, targetId)`, `(initiativeId)`.
### chat_messages
Messages within a chat session.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| chatSessionId | text FK → chat_sessions (cascade) | |
| role | text enum | 'user' \| 'assistant' \| 'system' |
| content | text NOT NULL | |
| changeSetId | text FK → change_sets (set null) | links assistant messages to applied changes |
| createdAt | integer/timestamp | |
Index: `(chatSessionId)`.
### errands
Tracks errand work items linked to a project branch, optionally assigned to an agent.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | caller-supplied |
| description | text NOT NULL | human-readable description |
| branch | text NOT NULL | working branch name |
| baseBranch | text NOT NULL | default 'main' |
| agentId | text FK → agents (set null) | assigned agent; null if unassigned |
| projectId | text FK → projects (cascade) | owning project |
| status | text enum | active, pending_review, conflict, merged, abandoned; default 'active' |
| createdAt, updatedAt | integer/timestamp | |
### review_comments
Inline review comments on phase diffs, persisted across page reloads.
| Column | Type | Notes |
|--------|------|-------|
| id | text PK | nanoid |
| phaseId | text FK → phases (cascade) | scopes comment to phase |
| filePath | text NOT NULL | file in diff |
| lineNumber | integer NOT NULL | line number (new-side or old-side for deletions) |
| lineType | text enum | 'added' \| 'removed' \| 'context' |
| body | text NOT NULL | comment text |
| author | text NOT NULL | default 'you' |
| resolved | integer/boolean | default false |
| createdAt, updatedAt | integer/timestamp | |
Index: `(phaseId)`.
## Repository Interfaces
14 repositories, each with standard CRUD plus domain-specific methods:
| Repository | Key Methods |
|-----------|-------------|
| InitiativeRepository | create, findById, findAll, findByStatus, update, delete |
| PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId |
| TaskRepository | + findByParentTaskId, findByPhaseId, createDependency |
| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus, findWaitingWithContext (LEFT JOIN enriched) |
| MessageRepository | + findPendingForUser, findRequiringResponse, findReplies |
| PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId |
| ProjectRepository | + junction ops: setInitiativeProjects (diff-based), findProjectsByInitiativeId |
| AccountRepository | + findNextAvailable (round-robin), markExhausted, clearExpiredExhaustion |
| ProposalRepository | + findByAgentIdAndStatus, updateManyByAgentId, countByAgentIdAndStatus |
| LogChunkRepository | insertChunk, findByAgentId, findByAgentIds (batch), deleteByAgentId, getSessionCount |
| ConversationRepository | create, findById, findPendingForAgent, answer, countByFromAgentIds (batch), findByFromAgentId |
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
| ErrandRepository | create, findById, findAll (filter by projectId/status), update, delete |
## Migrations
Located in `drizzle/`. Applied via `ensureSchema()` on startup using Drizzle's `migrate()`.
Key rules:
- **Never use raw SQL** for schema initialization
- Run `npx drizzle-kit generate` to create migrations
- See [database-migrations.md](database-migrations.md) for full workflow
- Snapshots stale after 0008; migrations 0008+ are hand-written
Current migrations: 0000 through 0035 (36 total).