- Add optional globalDir parameter to resolveProject(), getDefaultProject(), setDefaultProject(), and clearDefaultProject() for test isolation - Remove ESLint suppression by using void operator for intentionally unused var - Update all tests to use isolated globalDir parameter - Complete tests for default project resolution
28 KiB
Task: KB-310 - Migrate from file-based storage to SQLite (hybrid: DB for metadata, files for blobs)
Created: 2026-03-31 Size: L
Review Level: 3 (Full)
Assessment: This is a foundational infrastructure change affecting all six data stores (TaskStore, Config, Archive, Activity Log, AgentStore, AutomationStore) with complex migration requirements, WAL mode concurrency, and zero-downtime backward compatibility needs. High blast radius, novel SQLite patterns for the codebase, and requires full verification.
Score: 7/8 — Blast radius: 2, Pattern novelty: 2, Security: 1, Reversibility: 2
Mission
Migrate the kb task board system from its current file-based storage to a hybrid SQLite + filesystem architecture. All structured/queryable metadata moves to a SQLite database at .kb/kb.db using Node.js built-in node:sqlite. Large blob files (PROMPT.md, agent.log, attachments) remain on the filesystem under .kb/tasks/{ID}/.
This migration must be seamless: on first run, detect legacy file-based data and auto-migrate it to SQLite while preserving old files as backups. The existing TaskStore, AgentStore, and AutomationStore method signatures must be preserved for backward compatibility.
Dependencies
- None
Context to Read First
packages/core/src/types.ts— All data type definitions (Task, Agent, Automation, etc.)packages/core/src/store.ts— Current TaskStore implementation with file-based storagepackages/core/src/agent-store.ts— Current AgentStore implementationpackages/core/src/automation-store.ts— Current AutomationStore implementationpackages/core/src/global-settings.ts— GlobalSettingsStore (remains file-based in~/.pi/kb/)packages/dashboard/src/sse.ts— SSE endpoint for real-time updates
File Scope
New Files
packages/core/src/db.ts— Database module with SQLite connection, schema, and migrationpackages/core/src/db-migrate.ts— Migration logic from file-based to SQLite
Modified Files
packages/core/src/store.ts— Refactor TaskStore to use SQLite for all metadatapackages/core/src/agent-store.ts— Refactor AgentStore to use SQLitepackages/core/src/automation-store.ts— Refactor AutomationStore to use SQLitepackages/core/src/index.ts— Export Database modulepackages/dashboard/src/sse.ts— Update change detection to use polling
Unchanged (for reference)
packages/core/src/global-settings.ts— Global settings remain in~/.pi/kb/settings.jsonpackages/core/src/types.ts— Types remain unchanged
Steps
Step 1: Database Foundation
Create the Database module with SQLite schema and connection management.
- Create
packages/core/src/db.tswith Database class- Use
node:sqlite(Node 22.5+ built-in module) - Synchronous API for simplified transaction handling
- WAL mode enabled for concurrent reader/writer access
- Schema version tracking in
__metatable
- Use
- Define complete schema in
db.tsusing camelCase column names (matches TypeScript):-- Tasks table with JSON columns for nested data CREATE TABLE tasks ( id TEXT PRIMARY KEY, title TEXT, description TEXT NOT NULL, column TEXT NOT NULL, status TEXT, size TEXT, reviewLevel INTEGER, currentStep INTEGER DEFAULT 0, worktree TEXT, blockedBy TEXT, paused INTEGER DEFAULT 0, baseBranch TEXT, modelPresetId TEXT, modelProvider TEXT, modelId TEXT, validatorModelProvider TEXT, validatorModelId TEXT, mergeRetries INTEGER, error TEXT, summary TEXT, thinkingLevel TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, columnMovedAt TEXT, -- JSON columns for nested arrays/objects dependencies TEXT DEFAULT '[]', steps TEXT DEFAULT '[]', log TEXT DEFAULT '[]', attachments TEXT DEFAULT '[]', steeringComments TEXT DEFAULT '[]', workflowStepResults TEXT DEFAULT '[]', prInfo TEXT, issueInfo TEXT, breakIntoSubtasks INTEGER DEFAULT 0, enabledWorkflowSteps TEXT DEFAULT '[]' ); -- Config table (single row with project settings) CREATE TABLE config ( id INTEGER PRIMARY KEY CHECK (id = 1), nextId INTEGER DEFAULT 1, nextWorkflowStepId INTEGER DEFAULT 1, settings TEXT DEFAULT '{}', workflowSteps TEXT DEFAULT '[]', updatedAt TEXT ); -- Activity log with indexed columns for efficient queries CREATE TABLE activityLog ( id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, type TEXT NOT NULL, taskId TEXT, taskTitle TEXT, details TEXT NOT NULL, metadata TEXT ); CREATE INDEX idxActivityLogTimestamp ON activityLog(timestamp); CREATE INDEX idxActivityLogType ON activityLog(type); CREATE INDEX idxActivityLogTaskId ON activityLog(taskId); -- Archived tasks table (migrated from archive.jsonl) CREATE TABLE archivedTasks ( id TEXT PRIMARY KEY, data TEXT NOT NULL, -- Full ArchivedTaskEntry as JSON archivedAt TEXT NOT NULL ); CREATE INDEX idxArchivedTasksId ON archivedTasks(id); -- Automations table CREATE TABLE automations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, scheduleType TEXT NOT NULL, cronExpression TEXT NOT NULL, command TEXT NOT NULL, enabled INTEGER DEFAULT 1, timeoutMs INTEGER, steps TEXT, -- JSON array of AutomationStep nextRunAt TEXT, lastRunAt TEXT, lastRunResult TEXT, -- JSON runCount INTEGER DEFAULT 0, runHistory TEXT DEFAULT '[]', -- JSON array createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL ); -- Agents table CREATE TABLE agents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'idle', taskId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, lastHeartbeatAt TEXT, metadata TEXT DEFAULT '{}' -- JSON ); -- Agent heartbeat events CREATE TABLE agentHeartbeats ( id INTEGER PRIMARY KEY AUTOINCREMENT, agentId TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT NOT NULL, runId TEXT NOT NULL, FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE ); CREATE INDEX idxAgentHeartbeatsAgentId ON agentHeartbeats(agentId); CREATE INDEX idxAgentHeartbeatsRunId ON agentHeartbeats(runId); -- Schema version tracking CREATE TABLE __meta ( key TEXT PRIMARY KEY, value TEXT ); INSERT INTO __meta (key, value) VALUES ('schemaVersion', '1'); - Implement change detection polling mechanism:
- Store
lastModifiedtimestamp (epoch ms) in__metatable getLastModified(): numbermethod returns last change timestampbumpLastModified(): voidupdates timestamp on every write operation
- Store
- Implement helper methods for JSON serialization/deserialization:
toJson(value: unknown): string— stringifies arrays/objects, returns'[]'for empty arraysfromJson<T>(json: string | null): T | undefined— parses safely, returns undefined for null/empty
- Implement
Databaseclass with these methods:constructor(kbDir: string)— opens database at.kb/kb.db, enables WAL modeinit(): void— creates tables if they don't existclose(): void— closes database connectiontransaction<T>(fn: () => T): T— executes function inside SQLite transactionprepare(sql: string): Statement— returns prepared statement objectgetLastModified(): number— reads__meta.lastModifiedbumpLastModified(): void— updates__meta.lastModifiedto current epoch ms
- Export
createDatabase(kbDir: string): Databasefactory function - Run
pnpm buildto verify no TypeScript errors - Write unit tests in
packages/core/src/db.test.ts:- Schema creation works
- WAL mode is enabled
- Change detection timestamps work
- JSON serialization helpers work
- Transactions work atomically
Artifacts:
packages/core/src/db.ts(new)packages/core/src/db.test.ts(new)
Step 2: Migration System
Create auto-migration logic from file-based to SQLite.
- Create
packages/core/src/db-migrate.tswith migration functions:detectLegacyData(kbDir: string): boolean— returns true if.kb/tasks/exists OR.kb/config.jsonexists OR.kb/agents/exists OR.kb/automations/exists OR.kb/activity-log.jsonlexists OR.kb/archive.jsonlexists (but.kb/kb.dbdoes NOT exist)migrateFromLegacy(kbDir: string, db: Database): Promise<void>— performs full migration
- Implement migration steps in order:
- Migrate config.json →
configtable (preserve nextId as nextId, workflowSteps array, settings object) - Iterate
.kb/tasks/*/task.json→taskstable (map each JSON field to column, serialize arrays to JSON strings) - Migrate
.kb/activity-log.jsonl→activityLogtable (parse each line, map fields) - Migrate
.kb/archive.jsonl→archivedTaskstable (parse each line, store full entry asdataJSON) - Migrate
.kb/automations/*.json→automationstable (read each file, map fields) - Migrate
.kb/agents/*.json→agentstable (read agent files, map fields) - Migrate
.kb/agents/*-heartbeats.jsonl→agentHeartbeatstable (parse each heartbeat line)
- Migrate config.json →
- Create backup of old files after successful migration:
- Rename
.kb/tasks/→.kb/tasks.bak/(if exists) - Rename
.kb/config.json→.kb/config.json.bak(if exists) - Rename
.kb/activity-log.jsonl→.kb/activity-log.jsonl.bak(if exists) - Rename
.kb/archive.jsonl→.kb/archive.jsonl.bak(if exists) - Rename
.kb/automations/→.kb/automations.bak/(if exists) - Rename
.kb/agents/→.kb/agents.bak/(if exists)
- Rename
- Handle partial/corrupt legacy data gracefully:
- Skip invalid task.json files with console warning (don't fail entire migration)
- Skip malformed archive.jsonl lines (continue to next line)
- Skip malformed activity log lines
- Wrap each migration step in try/catch, log errors but continue
- Implement
getMigrationStatus(kbDir: string): { hasLegacy: boolean; hasDatabase: boolean; needsMigration: boolean } - Write tests for migration logic:
- Happy path: all legacy data migrates correctly, data integrity preserved
- Partial corruption: valid data migrates, corrupt data is skipped with warnings
- Idempotent: re-running migration with existing SQLite does nothing (detects already migrated)
- Empty project: fresh project with no legacy data creates empty database correctly
- Run
pnpm buildand verify tests pass
Artifacts:
packages/core/src/db-migrate.ts(new)packages/core/src/db-migrate.test.ts(new)
Step 3: TaskStore SQLite Migration
Refactor TaskStore to use SQLite for all structured metadata.
- Update TaskStore constructor to accept Database instance and store rootDir only (not kb paths):
constructor( private rootDir: string, private db: Database, globalSettingsDir?: string ) - Remove
kbDir,tasksDir,configPath,archiveLogPath,activityLogPath— these move to Database - Remove
withTaskLockandwithConfigLockpromise chains — usedb.transaction()instead - Remove
recentlyWritten,debounceTimers— change detection will use polling - Replace file-based config operations:
readConfig()→SELECT * FROM config WHERE id = 1, parse settings/workflowSteps JSONwriteConfig()→INSERT OR REPLACE INTO config ...
- Replace task CRUD operations with SQLite:
readTaskJson(dir)→SELECT * FROM tasks WHERE id = ?, parse JSON columnsatomicWriteTaskJson(dir, task)→INSERT OR REPLACE INTO tasks ...with JSON stringificationlistTasks()→SELECT * FROM tasks ORDER BY createdAt, parse JSON for each rowgetTask()→SELECT * FROM tasks WHERE id = ?+ read PROMPT.md from disk viataskDir()
- Update
allocateId()to use transaction:return this.db.transaction(() => { const row = this.db.prepare('SELECT nextId, settings FROM config WHERE id = 1').get() as { nextId: number; settings: string }; const settings = JSON.parse(row.settings || '{}'); const prefix = settings.taskPrefix || 'KB'; const id = `${prefix}-${String(row.nextId).padStart(3, '0')}`; this.db.prepare('UPDATE config SET nextId = ?').run(row.nextId + 1); this.db.bumpLastModified(); return id; }); - Update
createTask():- Insert row into
taskstable with JSON-serialized arrays - Write PROMPT.md to disk (unchanged)
- Call
db.bumpLastModified() - Emit
task:createdevent
- Insert row into
- Update
updateTask():- Use
db.prepare().run()to update columns - JSON-serialize dependencies, steps, etc.
- Call
db.bumpLastModified() - Write PROMPT.md if updates.prompt provided
- Use
- Update
moveTask(),pauseTask(),updateStep(),logEntry(),addSteeringComment():- All use
db.transaction()for atomicity - Update relevant columns
- Call
db.bumpLastModified()after write - Emit appropriate events
- All use
- Update
updatePrInfo()andupdateIssueInfo():- Use
db.transaction() - Read current task with
SELECT * FROM tasks WHERE id = ? - Compare previous prInfo/issueInfo to detect badgeChanged
- Update prInfo/issueInfo JSON columns
- Append to log JSON array
- Call
db.bumpLastModified() - Emit
task:updatedif badgeChanged
- Use
- Keep
appendAgentLog()file-based (agent.log remains JSONL file):- Use same implementation but ensure
taskDir()still works taskDir(id)returnsjoin(rootDir, '.kb', 'tasks', id)(computed, not stored)
- Use same implementation but ensure
- Keep
getAgentLogs()file-based:- Same implementation, just ensure paths work
- Update file-based operations for blob data:
taskDir(id)— compute path:join(this.rootDir, '.kb', 'tasks', id)addAttachment()— write file to disk, store metadata in SQLite attachments JSON columngetAttachment()— read metadata from SQLite, return disk pathdeleteAttachment()— delete from disk, update SQLite attachments JSON column
- Replace activity log operations:
recordActivity()→INSERT INTO activityLog ..., calldb.bumpLastModified()getActivityLog()→SELECT * FROM activityLog ORDER BY timestamp DESC, parse metadata JSONclearActivityLog()→DELETE FROM activityLog
- Replace archive operations:
readArchiveLog()→SELECT id, archivedAt, data FROM archivedTasks ORDER BY archivedAt DESCfindInArchive()→SELECT * FROM archivedTasks WHERE id = ?archiveTask()→ within transaction: insert to archivedTasks, delete from tasks if cleanupcleanupArchivedTasks()→ for each archived task with directory: insert compact entry to archivedTasks, delete directoryrestoreFromArchive()→ insert back to tasks from archivedTasks.data, regenerate PROMPT.mdunarchiveTask()→ check if dir exists (if not restore first), then update column in SQLite
- Replace workflow step operations:
createWorkflowStep()→ within transaction: read config.workflowSteps, append new step, update configlistWorkflowSteps()→ read config.workflowSteps, parse JSON arraygetWorkflowStep()→ find in parsed workflowSteps arrayupdateWorkflowStep()→ find in array, update, write back to configdeleteWorkflowStep()→ filter array, write back, then update tasks' enabledWorkflowSteps
- Update
duplicateTask(),refineTask():- Use SQLite for task creation
- Copy attachments via disk operations
- Update
mergeTask():- Use transaction to read/update task
- Git operations unchanged
- Call
db.bumpLastModified()
- Update
deleteTask():- SQLite:
DELETE FROM tasks WHERE id = ? - Disk: delete directory recursively (attachments, PROMPT.md, agent.log)
- Call
db.bumpLastModified()
- SQLite:
- Update
parseStepsFromPrompt(),parseDependenciesFromPrompt(),parseFileScopeFromPrompt():- Ensure they can read PROMPT.md from disk (these read files, unchanged)
- Update change detection (
watch(),stopWatching()):- Remove
fs.watch()based implementation - Implement polling: setInterval that checks
db.getLastModified()every 1 second - When timestamp changes, query for changes and emit events
- Maintain
taskCacheMap for diffing to determine event type - Preserve event emission:
task:created,task:moved,task:updated,task:deleted,task:merged
- Remove
- Update
init():- Ensure task directories exist for blobs (
.kb/tasks/) db.init()should already be called by caller
- Ensure task directories exist for blobs (
- Preserve
getRootDir()method (used by dashboard terminal service) - Run targeted tests:
pnpm test packages/core/src/store.test.ts - Fix any failing tests
Artifacts:
packages/core/src/store.ts(modified - large refactor)
Step 4: AgentStore SQLite Migration
Refactor AgentStore to use SQLite.
- Update AgentStore constructor:
constructor( options: AgentStoreOptions & { db: Database } ) - Remove
rootDir,agentsDir— these move to Database - Remove
locksMap — usedb.transaction()instead - Replace agent file operations:
readAgentFile()→SELECT * FROM agents WHERE id = ?, parse metadata JSONwriteAgent()→INSERT OR REPLACE INTO agents ...getAgent()→ query SQLite, parse metadata, return AgentlistAgents()→SELECT * FROM agents ORDER BY createdAt DESC, apply filters in code
- Replace heartbeat file operations:
recordHeartbeat()→ within transaction:INSERT INTO agentHeartbeats ..., update agent.lastHeartbeatAt if status === 'ok'getHeartbeatHistory()→SELECT * FROM agentHeartbeats WHERE agentId = ? ORDER BY timestamp DESC LIMIT ?
- Implement heartbeat run reconstruction from SQLite data:
getActiveHeartbeatRun(agentId)→ query heartbeats for agent, group by runId, find run without terminal status ('missed')getCompletedHeartbeatRuns(agentId)→ query heartbeats, group by runId, filter completed/terminatedstartHeartbeatRun(agentId)→ record heartbeat with new runId viarecordHeartbeat()endHeartbeatRun(runId, status)→ find agent for runId, record terminal heartbeat
- Update
createAgent(),updateAgent(),updateAgentState(),assignTask():- Use
db.transaction()for atomicity - Update agents table
- Call
db.bumpLastModified() - Emit events (unchanged)
- Use
- Update
deleteAgent():- SQLite handles cascade delete via foreign key
DELETE FROM agents WHERE id = ?- Call
db.bumpLastModified()
- Update
listAgents(filter?):- Query all, parse metadata, apply filter in code
- Remove
withLock()promise chaining — SQLite transactions handle serialization - Update
init()— no longer needs to create agents directory - Preserve all method signatures and EventEmitter behavior exactly
- Run targeted tests for AgentStore (create
packages/core/src/agent-store.test.tsif it doesn't exist) - Verify agent:created, agent:updated, agent:deleted, agent:heartbeat, agent:stateChanged events still fire
Artifacts:
packages/core/src/agent-store.ts(modified)packages/core/src/agent-store.test.ts(new or modified)
Step 5: AutomationStore SQLite Migration
Refactor AutomationStore to use SQLite.
- Update AutomationStore constructor:
constructor( private rootDir: string, private db: Database ) - Remove
automationsDir— automations now in SQLite - Remove
scheduleLocksMap — usedb.transaction()instead - Replace schedule file operations:
readScheduleJson()→SELECT * FROM automations WHERE id = ?, parse steps/runHistory JSONatomicWriteScheduleJson()→INSERT OR REPLACE INTO automations ...listSchedules()→SELECT * FROM automations ORDER BY createdAt
- Update
createSchedule():- Generate UUID using
randomUUID()(same as current) - Serialize
stepsandrunHistoryto JSON - Insert row with
db.prepare().run() - Call
db.bumpLastModified() - Emit
schedule:created
- Generate UUID using
- Update
updateSchedule():- Use
db.transaction() - Update columns, recompute nextRunAt if scheduleType/cronExpression changed
- Call
db.bumpLastModified() - Emit
schedule:updated
- Use
- Update
reorderSteps():- Read schedule, parse steps JSON, reorder, serialize, update
- Call
db.bumpLastModified()
- Update
recordRun():- Within transaction: read schedule, parse runHistory, unshift new result, cap at MAX_RUN_HISTORY, serialize, update
- Update lastRunAt, lastRunResult, runCount, recompute nextRunAt
- Call
db.bumpLastModified() - Emit
schedule:run
- Update
getDueSchedules():SELECT * FROM automations WHERE enabled = 1 AND nextRunAt <= ?- Pass current ISO timestamp as parameter
- Update
deleteSchedule():DELETE FROM automations WHERE id = ?- Call
db.bumpLastModified() - Emit
schedule:deleted
- Update
computeNextRun()— implementation unchanged (uses cron-parser) - Remove
withScheduleLock()promise chaining — usedb.transaction() - Update
init()— no longer needs to create automations directory - Preserve all method signatures and EventEmitter events exactly
- Run targeted tests:
pnpm test packages/core/src/automation-store.test.ts - Fix any failing tests
Artifacts:
packages/core/src/automation-store.ts(modified)
Step 6: Dashboard SSE Update
Update the change detection mechanism to use SQLite polling.
- Read
packages/dashboard/src/sse.tsto understand current implementation - The current implementation listens to store events (
task:created,task:moved,task:updated,task:deleted,task:merged) - Verify that Store continues to emit these events (implemented in Step 3 and 4)
- No changes needed to
sse.tsif stores continue to emit events correctly - Update any dashboard API routes that create stores to pass Database instance:
- Check
packages/dashboard/app/routes/for any route files that instantiate TaskStore, AgentStore, or AutomationStore - Update to create
Databaseinstance first, pass to store constructors
- Check
- Verify dashboard can still:
- Create tasks (through QuickEntryBox, InlineCreateCard)
- Move tasks between columns
- Update task details
- Archive/unarchive tasks
- Show real-time updates via SSE
- Run dashboard tests if available
Artifacts:
packages/dashboard/src/sse.ts(verify no changes needed, or update if required)- Any affected API routes in
packages/dashboard/app/routes/
Step 7: Store Initialization & Entry Points
Update initialization sequence to resolve Database creation before stores.
- Update
packages/core/src/index.ts:- Export
Databaseclass fromdb.ts - Export
createDatabase(kbDir: string): Databasefactory function - Export
detectLegacyData,migrateFromLegacy,getMigrationStatusfromdb-migrate.ts
- Export
- Update CLI initialization (check
packages/cli/src/for store creation):- Find where
TaskStore,AgentStore,AutomationStoreare instantiated - Update to create
Databasefirst:import { createDatabase, detectLegacyData, migrateFromLegacy } from '@kb/core'; const kbDir = join(rootDir, '.kb'); const db = createDatabase(kbDir); db.init(); if (detectLegacyData(kbDir)) { await migrateFromLegacy(kbDir, db); } const store = new TaskStore(rootDir, db, globalSettingsDir); const agentStore = new AgentStore({ db, rootDir }); const automationStore = new AutomationStore(rootDir, db);
- Find where
- Update dashboard server initialization (check
packages/dashboard/):- Same pattern: create Database, detect legacy, migrate, then create stores
- Verify
GlobalSettingsStoreremains unchanged (still file-based in~/.pi/kb/) - Ensure stores handle the case where they're initialized with fresh database (no data)
Artifacts:
packages/core/src/index.ts(modified)- Any CLI initialization code that creates stores
- Any dashboard server initialization code
Step 8: Testing & Verification
ZERO test failures allowed. Full test suite as quality gate.
- Run full test suite:
pnpm test - Verify all tests pass in
packages/core/:store.test.tspassesagent-store.test.tspasses (or create if missing)automation-store.test.tspassesdb.test.tspassesdb-migrate.test.tspasses
- Run
pnpm buildto ensure no TypeScript errors across all packages - Manual verification steps (perform these manually or document for QA):
- Create a fresh project:
mkdir test-project && cd test-project && git init - Run kb init (through CLI or dashboard)
- Verify
.kb/kb.dbis created - Create a task → verify it appears in database and on dashboard
- Move task through columns → verify state updates in both DB and UI
- Add steering comment → verify persisted
- Update PR info → verify badge appears
- Archive a task → verify moved to
archivedTaskstable - Unarchive → verify restored to
taskstable - Create an automation → verify in
automationstable - Create an agent → verify in
agentstable - Record heartbeat → verify in
agentHeartbeatstable - View agent logs → verify agent.log file still readable
- Create a fresh project:
- Test backward compatibility:
- Create project with old file-based version (checkout old commit or manually create file structure)
- Create some tasks, agents, automations
- Run new version → verify auto-migration triggers
- Verify
.kb/tasks.bak/exists with old data - Verify all data correctly migrated (IDs, columns, timestamps, JSON arrays)
- Verify tasks work correctly after migration
- Restart app → verify doesn't re-migrate (detects already migrated)
Step 9: Documentation & Delivery
- Update
AGENTS.mdwith new section:- "SQLite Storage Architecture" — explain hybrid approach (DB for metadata, files for blobs)
- "Migration Notes" — explain auto-migration, backup files, recovery
- Schema documentation — list tables and their purposes
- Create changeset file:
cat > .changeset/sqlite-migration.md << 'EOF' --- "@dustinbyrne/kb": minor --- Migrate storage from file-based JSON to SQLite (hybrid with files for blobs) - Improved performance for large task counts - Better concurrent access support (WAL mode) - Seamless auto-migration from legacy data - Zero breaking changes to store APIs EOF - Create follow-up tasks via
task_createfor any out-of-scope findings:- Performance optimization: Add additional indexes if slow queries identified
- Schema versioning: Upgrade system for future schema changes
- Global settings: Consider if
~/.pi/kb/settings.jsonshould also migrate
Documentation Requirements
Must Update:
AGENTS.md— Add "SQLite Storage Architecture" section explaining:- Database location at
.kb/kb.db - Hybrid architecture (SQLite for metadata, files for blobs)
- Auto-migration behavior and backup files
- WAL mode for concurrency
- Database location at
Check If Affected:
packages/core/README.md— Update if it describes storage mechanismspackages/dashboard/README.md— Update if it mentions file watching or storage
Completion Criteria
- All steps complete
- All tests passing (
pnpm testexits 0) - Build passes (
pnpm buildexits 0) - Auto-migration works seamlessly from file-based storage
- File blobs (PROMPT.md, agent.log, attachments) still work correctly
- Dashboard real-time updates work via store events (SSE unchanged)
- Steering comments, PR info, Issue info all persist correctly
- getRootDir() method preserved on TaskStore
- Documentation updated
- Changeset file created
Git Commit Convention
Commits at step boundaries. All commits include the task ID:
- Step completion:
feat(KB-310): complete Step N — description - Bug fixes:
fix(KB-310): description - Tests:
test(KB-310): description - Documentation:
docs(KB-310): description
Do NOT
- Modify
GlobalSettingsStore— global settings stay in~/.pi/kb/settings.json - Change any public method signatures on stores — preserve backward compatibility
- Use external SQLite dependencies — use only Node.js built-in
node:sqlite - Remove legacy file-based code until migration is verified working
- Delete
.kb/*.bakfiles — they are user data backups - Skip WAL mode — concurrent access requires it
- Skip transaction handling — data integrity is critical
- Use snake_case column names — use camelCase to match TypeScript
- Remove or rename
getRootDir()method — dashboard depends on it