Data persistence strategy using the Repository Pattern with SQLite.
The Repository Pattern abstracts data access, providing a collection-like interface for domain objects while hiding storage implementation details.
+-------------------------+
| Application Layer |
| |
| +-------------------+ |
| | IFeatureRepository| | <-- Interface (Port)
| +---------+---------+ |
+------------+------------+
| implements
+------------+------------+
| Infrastructure Layer |
| |
| +-------------------+ |
| |SqliteFeatureRepo | | <-- Implementation
| +---------+---------+ |
| | |
| +---------v---------+ |
| | SQLite Database | |
| +-------------------+ |
+-------------------------+
Defined in packages/core/src/application/ports/output/repositories/:
export interface IFeatureRepository {
create(feature: Feature): Promise<void>;
findById(id: string): Promise<Feature | null>;
findByIdPrefix(prefix: string): Promise<Feature | null>;
findBySlug(slug: string, repositoryPath: string): Promise<Feature | null>;
list(filters?: FeatureListFilters): Promise<Feature[]>;
update(feature: Feature): Promise<void>;
findByParentId(parentId: string): Promise<Feature[]>;
delete(id: string): Promise<void>;
}
export interface FeatureListFilters {
repositoryPath?: string;
lifecycle?: SdlcLifecycle;
}
export interface ISettingsRepository {
initialize(settings: Settings): Promise<void>;
load(): Promise<Settings | null>;
update(settings: Settings): Promise<void>;
}
export interface IRepositoryRepository {
create(repository: Repository): Promise<Repository>;
findById(id: string): Promise<Repository | null>;
findByPath(path: string): Promise<Repository | null>;
findByPathIncludingDeleted(path: string): Promise<Repository | null>;
list(): Promise<Repository[]>;
remove(id: string): Promise<void>;
softDelete(id: string): Promise<void>;
restore(id: string): Promise<void>;
}
Defined in packages/core/src/application/ports/output/agents/:
~/.shep/data (settings table)~/.shep/repos/<base64-encoded-repo-path>/data (features, agent_runs, etc.)The repo path is base64-encoded to create valid directory names while preserving uniqueness.
The schema is managed through 28 migrations in packages/core/src/infrastructure/persistence/sqlite/migrations.ts. Key tables:
Singleton row with flattened columns for nested configuration objects:
model_default, model_analyze, model_requirements, model_plan, model_implementuser_name, user_email, user_github_usernameenv_default_editor, env_shell_preferencesys_auto_update, sys_log_levelagent_type, agent_auth_method, agent_tokennotif_in_app_enabled, notif_browser_enabled, notif_desktop_enabled, plus event filtersworkflow_open_pr_on_impl_complete, approval gate defaultsfeature_flag_skills, feature_flag_env_deploy, feature_flag_debugci_max_fix_attempts, ci_watch_timeout_ms, ci_log_max_charsStores Feature entities with JSON columns for complex nested data (messages, plan, related_artifacts, ci_fix_history, attachments). Includes columns for lifecycle tracking, PR state, approval gates, worktree paths, and parent/child hierarchy.
Tracks agent execution records with status, timing, PID for background processes, approval configuration, and model selection.
Tracked code repositories with soft delete support via deleted_at column.
Per-phase timing data for agent runs, including approval wait tracking.
packages/core/src/infrastructure/
+-- persistence/
| +-- sqlite/
| +-- connection.ts # Database connection
| +-- migrations.ts # 28 migrations (user_version pragma)
| +-- mappers/ # Domain <-> Persistence mapping
+-- repositories/
+-- sqlite-feature.repository.ts
+-- sqlite-settings.repository.ts
+-- sqlite-repository.repository.ts
+-- agent-run.repository.ts
+-- sqlite-phase-timing.repository.ts
Migrations are managed via SQLite user_version pragma. All migration SQL is inlined in TypeScript (not separate .sql files) so it survives tsc compilation. The runSQLiteMigrations() function applies pending migrations transactionally.
Update when:
Related docs: