anvil-dev-framework 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Anvil Memory Database Module
3
+ *
4
+ * SQLite database operations using bun:sqlite with FTS5 full-text search.
5
+ */
6
+
7
+ import { Database } from 'bun:sqlite';
8
+ import { homedir } from 'os';
9
+ import { join, dirname } from 'path';
10
+ import { existsSync, mkdirSync } from 'fs';
11
+ import type { DbConfig, Observation, Session, Checkpoint, RalphIteration, Prompt } from './types.ts';
12
+
13
+ // Current schema version
14
+ const SCHEMA_VERSION = 1;
15
+
16
+ // Default database path
17
+ const DEFAULT_DB_PATH = join(homedir(), '.anvil', 'memory.db');
18
+
19
+ /**
20
+ * SQL schema for anvil-memory database
21
+ */
22
+ const SCHEMA_SQL = `
23
+ -- Schema version tracking
24
+ CREATE TABLE IF NOT EXISTS schema_version (
25
+ version INTEGER PRIMARY KEY,
26
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
27
+ );
28
+
29
+ -- Observations table (main records)
30
+ CREATE TABLE IF NOT EXISTS observations (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
33
+ type TEXT NOT NULL CHECK (type IN (
34
+ 'bugfix', 'feature', 'refactor', 'discovery', 'decision',
35
+ 'change', 'checkpoint', 'ralph_iteration', 'handoff',
36
+ 'shard', 'linear_sync', 'session_request'
37
+ )),
38
+ title TEXT NOT NULL,
39
+ content TEXT NOT NULL,
40
+ project TEXT,
41
+ files TEXT, -- JSON array of file paths
42
+ concepts TEXT, -- JSON array of concept tags
43
+ work_tokens INTEGER,
44
+ session_id INTEGER REFERENCES sessions(id),
45
+ linear_issue TEXT
46
+ );
47
+
48
+ -- Observations FTS5 virtual table
49
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
50
+ title,
51
+ content,
52
+ concepts,
53
+ files,
54
+ content='observations',
55
+ content_rowid='id',
56
+ tokenize='porter unicode61'
57
+ );
58
+
59
+ -- Sessions table (conversation boundaries)
60
+ CREATE TABLE IF NOT EXISTS sessions (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
63
+ ended_at TEXT,
64
+ project TEXT NOT NULL,
65
+ branch TEXT,
66
+ git_hash TEXT,
67
+ context_peak_percent INTEGER,
68
+ summary TEXT
69
+ );
70
+
71
+ -- Sessions FTS5 virtual table
72
+ CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
73
+ summary,
74
+ project,
75
+ content='sessions',
76
+ content_rowid='id',
77
+ tokenize='porter unicode61'
78
+ );
79
+
80
+ -- Checkpoints table (CCS events)
81
+ CREATE TABLE IF NOT EXISTS checkpoints (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
84
+ level TEXT NOT NULL CHECK (level IN ('L1', 'L2', 'L3')),
85
+ context_percent INTEGER NOT NULL,
86
+ handoff_file TEXT,
87
+ session_id INTEGER REFERENCES sessions(id),
88
+ resumed INTEGER DEFAULT 0,
89
+ resume_session_id INTEGER REFERENCES sessions(id)
90
+ );
91
+
92
+ -- Ralph iterations table
93
+ CREATE TABLE IF NOT EXISTS ralph_iterations (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ session_id TEXT NOT NULL, -- Ralph session ID (not FK)
96
+ iteration INTEGER NOT NULL,
97
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
98
+ ended_at TEXT,
99
+ status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'checkpointed')),
100
+ items_completed INTEGER,
101
+ items_total INTEGER,
102
+ checkpoint_id INTEGER REFERENCES checkpoints(id)
103
+ );
104
+
105
+ -- Prompts table (user messages)
106
+ CREATE TABLE IF NOT EXISTS prompts (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
109
+ content TEXT NOT NULL,
110
+ project TEXT,
111
+ session_id INTEGER REFERENCES sessions(id)
112
+ );
113
+
114
+ -- Prompts FTS5 virtual table
115
+ CREATE VIRTUAL TABLE IF NOT EXISTS prompts_fts USING fts5(
116
+ content,
117
+ content='prompts',
118
+ content_rowid='id',
119
+ tokenize='porter unicode61'
120
+ );
121
+
122
+ -- Indexes for common queries
123
+ CREATE INDEX IF NOT EXISTS idx_observations_timestamp ON observations(timestamp DESC);
124
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
125
+ CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
126
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
127
+ CREATE INDEX IF NOT EXISTS idx_observations_linear ON observations(linear_issue);
128
+
129
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
130
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
131
+
132
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
133
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_timestamp ON checkpoints(timestamp DESC);
134
+
135
+ CREATE INDEX IF NOT EXISTS idx_ralph_session ON ralph_iterations(session_id);
136
+ CREATE INDEX IF NOT EXISTS idx_ralph_status ON ralph_iterations(status);
137
+
138
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);
139
+ CREATE INDEX IF NOT EXISTS idx_prompts_timestamp ON prompts(timestamp DESC);
140
+ `;
141
+
142
+ /**
143
+ * FTS5 triggers to keep search indexes in sync
144
+ */
145
+ const TRIGGER_SQL = `
146
+ -- Observations FTS triggers
147
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
148
+ INSERT INTO observations_fts(rowid, title, content, concepts, files)
149
+ VALUES (NEW.id, NEW.title, NEW.content, NEW.concepts, NEW.files);
150
+ END;
151
+
152
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
153
+ INSERT INTO observations_fts(observations_fts, rowid, title, content, concepts, files)
154
+ VALUES ('delete', OLD.id, OLD.title, OLD.content, OLD.concepts, OLD.files);
155
+ END;
156
+
157
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
158
+ INSERT INTO observations_fts(observations_fts, rowid, title, content, concepts, files)
159
+ VALUES ('delete', OLD.id, OLD.title, OLD.content, OLD.concepts, OLD.files);
160
+ INSERT INTO observations_fts(rowid, title, content, concepts, files)
161
+ VALUES (NEW.id, NEW.title, NEW.content, NEW.concepts, NEW.files);
162
+ END;
163
+
164
+ -- Sessions FTS triggers
165
+ CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions
166
+ WHEN NEW.summary IS NOT NULL BEGIN
167
+ INSERT INTO sessions_fts(rowid, summary, project)
168
+ VALUES (NEW.id, NEW.summary, NEW.project);
169
+ END;
170
+
171
+ CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions
172
+ WHEN OLD.summary IS NOT NULL BEGIN
173
+ INSERT INTO sessions_fts(sessions_fts, rowid, summary, project)
174
+ VALUES ('delete', OLD.id, OLD.summary, OLD.project);
175
+ END;
176
+
177
+ -- Update trigger: only delete from FTS if old row had summary (was in FTS)
178
+ CREATE TRIGGER IF NOT EXISTS sessions_au AFTER UPDATE ON sessions
179
+ WHEN OLD.summary IS NOT NULL BEGIN
180
+ INSERT INTO sessions_fts(sessions_fts, rowid, summary, project)
181
+ VALUES ('delete', OLD.id, OLD.summary, OLD.project);
182
+ INSERT INTO sessions_fts(rowid, summary, project)
183
+ VALUES (NEW.id, COALESCE(NEW.summary, ''), NEW.project);
184
+ END;
185
+
186
+ -- Insert into FTS when summary is added for first time (was NULL, now has value)
187
+ CREATE TRIGGER IF NOT EXISTS sessions_au_insert AFTER UPDATE ON sessions
188
+ WHEN OLD.summary IS NULL AND NEW.summary IS NOT NULL BEGIN
189
+ INSERT INTO sessions_fts(rowid, summary, project)
190
+ VALUES (NEW.id, NEW.summary, NEW.project);
191
+ END;
192
+
193
+ -- Prompts FTS triggers
194
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON prompts BEGIN
195
+ INSERT INTO prompts_fts(rowid, content)
196
+ VALUES (NEW.id, NEW.content);
197
+ END;
198
+
199
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON prompts BEGIN
200
+ INSERT INTO prompts_fts(prompts_fts, rowid, content)
201
+ VALUES ('delete', OLD.id, OLD.content);
202
+ END;
203
+
204
+ CREATE TRIGGER IF NOT EXISTS prompts_au AFTER UPDATE ON prompts BEGIN
205
+ INSERT INTO prompts_fts(prompts_fts, rowid, content)
206
+ VALUES ('delete', OLD.id, OLD.content);
207
+ INSERT INTO prompts_fts(rowid, content)
208
+ VALUES (NEW.id, NEW.content);
209
+ END;
210
+ `;
211
+
212
+ /**
213
+ * Database manager for Anvil Memory
214
+ */
215
+ export class AnvilMemoryDb {
216
+ private db: Database;
217
+ private path: string;
218
+
219
+ constructor(dbPath?: string) {
220
+ this.path = dbPath ?? DEFAULT_DB_PATH;
221
+
222
+ // Ensure directory exists
223
+ const dir = dirname(this.path);
224
+ if (!existsSync(dir)) {
225
+ mkdirSync(dir, { recursive: true });
226
+ }
227
+
228
+ // Open database
229
+ this.db = new Database(this.path, { create: true });
230
+
231
+ // Enable WAL mode for better concurrency
232
+ this.db.exec('PRAGMA journal_mode = WAL');
233
+ this.db.exec('PRAGMA foreign_keys = ON');
234
+ }
235
+
236
+ /**
237
+ * Initialize database with schema
238
+ */
239
+ init(): { created: boolean; version: number } {
240
+ const existingVersion = this.getSchemaVersion();
241
+
242
+ if (existingVersion === 0) {
243
+ // Fresh database - create schema
244
+ this.db.exec(SCHEMA_SQL);
245
+ this.db.exec(TRIGGER_SQL);
246
+ this.setSchemaVersion(SCHEMA_VERSION);
247
+ return { created: true, version: SCHEMA_VERSION };
248
+ }
249
+
250
+ if (existingVersion < SCHEMA_VERSION) {
251
+ // TODO: Run migrations
252
+ this.setSchemaVersion(SCHEMA_VERSION);
253
+ return { created: false, version: SCHEMA_VERSION };
254
+ }
255
+
256
+ return { created: false, version: existingVersion };
257
+ }
258
+
259
+ /**
260
+ * Get current schema version
261
+ */
262
+ getSchemaVersion(): number {
263
+ try {
264
+ const result = this.db.query<{ version: number }, []>(
265
+ 'SELECT MAX(version) as version FROM schema_version'
266
+ ).get();
267
+ return result?.version ?? 0;
268
+ } catch {
269
+ // Table doesn't exist yet
270
+ return 0;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Set schema version
276
+ */
277
+ private setSchemaVersion(version: number): void {
278
+ this.db.run('INSERT INTO schema_version (version) VALUES (?)', [version]);
279
+ }
280
+
281
+ /**
282
+ * Run integrity check
283
+ */
284
+ integrityCheck(): { ok: boolean; errors: string[] } {
285
+ const results = this.db.query<{ integrity_check: string }, []>(
286
+ 'PRAGMA integrity_check'
287
+ ).all();
288
+
289
+ const errors = results
290
+ .map((r) => r.integrity_check)
291
+ .filter((r) => r !== 'ok');
292
+
293
+ return {
294
+ ok: errors.length === 0,
295
+ errors,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Get database config info
301
+ */
302
+ getConfig(): DbConfig {
303
+ return {
304
+ path: this.path,
305
+ migrated: this.getSchemaVersion() > 0,
306
+ version: this.getSchemaVersion(),
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Close database connection
312
+ */
313
+ close(): void {
314
+ this.db.close();
315
+ }
316
+
317
+ /**
318
+ * Get raw database handle (for advanced queries)
319
+ */
320
+ getDb(): Database {
321
+ return this.db;
322
+ }
323
+
324
+ // ============================================
325
+ // Observation CRUD
326
+ // ============================================
327
+
328
+ createObservation(obs: Omit<Observation, 'id'>): Observation {
329
+ const stmt = this.db.prepare(`
330
+ INSERT INTO observations (timestamp, type, title, content, project, files, concepts, work_tokens, session_id, linear_issue)
331
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
332
+ `);
333
+
334
+ const result = stmt.run(
335
+ obs.timestamp ?? new Date().toISOString(),
336
+ obs.type,
337
+ obs.title,
338
+ obs.content,
339
+ obs.project ?? null,
340
+ obs.files ? JSON.stringify(obs.files) : null,
341
+ obs.concepts ? JSON.stringify(obs.concepts) : null,
342
+ obs.work_tokens ?? null,
343
+ obs.session_id ?? null,
344
+ obs.linear_issue ?? null
345
+ );
346
+
347
+ return { ...obs, id: Number(result.lastInsertRowid) };
348
+ }
349
+
350
+ getObservation(id: number): Observation | null {
351
+ const row = this.db.query<Record<string, unknown>, [number]>(`
352
+ SELECT * FROM observations WHERE id = ?
353
+ `).get(id);
354
+
355
+ if (!row) return null;
356
+
357
+ return this.rowToObservation(row);
358
+ }
359
+
360
+ searchObservations(query: string, limit = 20): Observation[] {
361
+ const rows = this.db.query<Record<string, unknown>, [string, number]>(`
362
+ SELECT o.* FROM observations o
363
+ JOIN observations_fts fts ON o.id = fts.rowid
364
+ WHERE observations_fts MATCH ?
365
+ ORDER BY rank
366
+ LIMIT ?
367
+ `).all(query, limit);
368
+
369
+ return rows.map((row) => this.rowToObservation(row));
370
+ }
371
+
372
+ getRecentObservations(limit = 20, types?: string[], project?: string): Observation[] {
373
+ let sql = 'SELECT * FROM observations WHERE 1=1';
374
+ const params: (string | number)[] = [];
375
+
376
+ if (types && types.length > 0) {
377
+ sql += ` AND type IN (${types.map(() => '?').join(',')})`;
378
+ params.push(...types);
379
+ }
380
+
381
+ if (project) {
382
+ sql += ' AND project = ?';
383
+ params.push(project);
384
+ }
385
+
386
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
387
+ params.push(limit);
388
+
389
+ // Use run for dynamic queries to avoid type issues
390
+ const stmt = this.db.prepare(sql);
391
+ const rows = stmt.all(...params) as Record<string, unknown>[];
392
+ return rows.map((row) => this.rowToObservation(row));
393
+ }
394
+
395
+ private rowToObservation(row: Record<string, unknown>): Observation {
396
+ return {
397
+ id: row.id as number,
398
+ timestamp: row.timestamp as string,
399
+ type: row.type as Observation['type'],
400
+ title: row.title as string,
401
+ content: row.content as string,
402
+ project: row.project as string | undefined,
403
+ files: row.files ? JSON.parse(row.files as string) : undefined,
404
+ concepts: row.concepts ? JSON.parse(row.concepts as string) : undefined,
405
+ work_tokens: row.work_tokens as number | undefined,
406
+ session_id: row.session_id as number | undefined,
407
+ linear_issue: row.linear_issue as string | undefined,
408
+ };
409
+ }
410
+
411
+ // ============================================
412
+ // Session CRUD
413
+ // ============================================
414
+
415
+ createSession(session: Omit<Session, 'id'>): Session {
416
+ const stmt = this.db.prepare(`
417
+ INSERT INTO sessions (started_at, ended_at, project, branch, git_hash, context_peak_percent, summary)
418
+ VALUES (?, ?, ?, ?, ?, ?, ?)
419
+ `);
420
+
421
+ const result = stmt.run(
422
+ session.started_at ?? new Date().toISOString(),
423
+ session.ended_at ?? null,
424
+ session.project,
425
+ session.branch ?? null,
426
+ session.git_hash ?? null,
427
+ session.context_peak_percent ?? null,
428
+ session.summary ?? null
429
+ );
430
+
431
+ return { ...session, id: Number(result.lastInsertRowid) };
432
+ }
433
+
434
+ getSession(id: number): Session | null {
435
+ const row = this.db.query<Record<string, unknown>, [number]>(`
436
+ SELECT * FROM sessions WHERE id = ?
437
+ `).get(id);
438
+
439
+ if (!row) return null;
440
+
441
+ return {
442
+ id: row.id as number,
443
+ started_at: row.started_at as string,
444
+ ended_at: row.ended_at as string | undefined,
445
+ project: row.project as string,
446
+ branch: row.branch as string | undefined,
447
+ git_hash: row.git_hash as string | undefined,
448
+ context_peak_percent: row.context_peak_percent as number | undefined,
449
+ summary: row.summary as string | undefined,
450
+ };
451
+ }
452
+
453
+ endSession(id: number, summary?: string, contextPeakPercent?: number): void {
454
+ this.db.run(`
455
+ UPDATE sessions
456
+ SET ended_at = datetime('now'),
457
+ summary = COALESCE(?, summary),
458
+ context_peak_percent = COALESCE(?, context_peak_percent)
459
+ WHERE id = ?
460
+ `, [summary ?? null, contextPeakPercent ?? null, id]);
461
+ }
462
+
463
+ // ============================================
464
+ // Checkpoint CRUD
465
+ // ============================================
466
+
467
+ createCheckpoint(checkpoint: Omit<Checkpoint, 'id'>): Checkpoint {
468
+ const stmt = this.db.prepare(`
469
+ INSERT INTO checkpoints (timestamp, level, context_percent, handoff_file, session_id, resumed, resume_session_id)
470
+ VALUES (?, ?, ?, ?, ?, ?, ?)
471
+ `);
472
+
473
+ const result = stmt.run(
474
+ checkpoint.timestamp ?? new Date().toISOString(),
475
+ checkpoint.level,
476
+ checkpoint.context_percent,
477
+ checkpoint.handoff_file ?? null,
478
+ checkpoint.session_id ?? null,
479
+ checkpoint.resumed ? 1 : 0,
480
+ checkpoint.resume_session_id ?? null
481
+ );
482
+
483
+ return { ...checkpoint, id: Number(result.lastInsertRowid) };
484
+ }
485
+
486
+ getCheckpoint(id: number): Checkpoint | null {
487
+ const row = this.db.query<Record<string, unknown>, [number]>(`
488
+ SELECT * FROM checkpoints WHERE id = ?
489
+ `).get(id);
490
+
491
+ if (!row) return null;
492
+
493
+ return {
494
+ id: row.id as number,
495
+ timestamp: row.timestamp as string,
496
+ level: row.level as Checkpoint['level'],
497
+ context_percent: row.context_percent as number,
498
+ handoff_file: row.handoff_file as string | undefined,
499
+ session_id: row.session_id as number | undefined,
500
+ resumed: Boolean(row.resumed),
501
+ resume_session_id: row.resume_session_id as number | undefined,
502
+ };
503
+ }
504
+
505
+ // ============================================
506
+ // Ralph Iteration CRUD
507
+ // ============================================
508
+
509
+ createRalphIteration(iteration: Omit<RalphIteration, 'id'>): RalphIteration {
510
+ const stmt = this.db.prepare(`
511
+ INSERT INTO ralph_iterations (session_id, iteration, started_at, ended_at, status, items_completed, items_total, checkpoint_id)
512
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
513
+ `);
514
+
515
+ const result = stmt.run(
516
+ iteration.session_id,
517
+ iteration.iteration,
518
+ iteration.started_at ?? new Date().toISOString(),
519
+ iteration.ended_at ?? null,
520
+ iteration.status,
521
+ iteration.items_completed ?? null,
522
+ iteration.items_total ?? null,
523
+ iteration.checkpoint_id ?? null
524
+ );
525
+
526
+ return { ...iteration, id: Number(result.lastInsertRowid) };
527
+ }
528
+
529
+ updateRalphIteration(id: number, updates: Partial<RalphIteration>): void {
530
+ const sets: string[] = [];
531
+ const params: (string | number | null)[] = [];
532
+
533
+ if (updates.ended_at !== undefined) {
534
+ sets.push('ended_at = ?');
535
+ params.push(updates.ended_at ?? null);
536
+ }
537
+ if (updates.status !== undefined) {
538
+ sets.push('status = ?');
539
+ params.push(updates.status);
540
+ }
541
+ if (updates.items_completed !== undefined) {
542
+ sets.push('items_completed = ?');
543
+ params.push(updates.items_completed ?? null);
544
+ }
545
+ if (updates.items_total !== undefined) {
546
+ sets.push('items_total = ?');
547
+ params.push(updates.items_total ?? null);
548
+ }
549
+ if (updates.checkpoint_id !== undefined) {
550
+ sets.push('checkpoint_id = ?');
551
+ params.push(updates.checkpoint_id ?? null);
552
+ }
553
+
554
+ if (sets.length > 0) {
555
+ params.push(id);
556
+ const stmt = this.db.prepare(`UPDATE ralph_iterations SET ${sets.join(', ')} WHERE id = ?`);
557
+ stmt.run(...params);
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Retrieves a Ralph iteration by ID.
563
+ *
564
+ * @param id - The unique identifier of the Ralph iteration
565
+ * @returns The Ralph iteration record, or null if not found
566
+ */
567
+ getRalphIteration(id: number): RalphIteration | null {
568
+ const row = this.db.query<Record<string, unknown>, [number]>(`
569
+ SELECT * FROM ralph_iterations WHERE id = ?
570
+ `).get(id);
571
+
572
+ if (!row) return null;
573
+
574
+ return {
575
+ id: row.id as number,
576
+ session_id: row.session_id as string,
577
+ iteration: row.iteration as number,
578
+ started_at: row.started_at as string,
579
+ ended_at: row.ended_at as string | undefined,
580
+ status: row.status as RalphIteration['status'],
581
+ items_completed: row.items_completed as number | undefined,
582
+ items_total: row.items_total as number | undefined,
583
+ checkpoint_id: row.checkpoint_id as number | undefined,
584
+ };
585
+ }
586
+
587
+ // ============================================
588
+ // Prompt CRUD
589
+ // ============================================
590
+
591
+ createPrompt(prompt: Omit<Prompt, 'id'>): Prompt {
592
+ const stmt = this.db.prepare(`
593
+ INSERT INTO prompts (timestamp, content, project, session_id)
594
+ VALUES (?, ?, ?, ?)
595
+ `);
596
+
597
+ const result = stmt.run(
598
+ prompt.timestamp ?? new Date().toISOString(),
599
+ prompt.content,
600
+ prompt.project ?? null,
601
+ prompt.session_id ?? null
602
+ );
603
+
604
+ return { ...prompt, id: Number(result.lastInsertRowid) };
605
+ }
606
+
607
+ searchPrompts(query: string, limit = 20): Prompt[] {
608
+ const rows = this.db.query<Record<string, unknown>, [string, number]>(`
609
+ SELECT p.* FROM prompts p
610
+ JOIN prompts_fts fts ON p.id = fts.rowid
611
+ WHERE prompts_fts MATCH ?
612
+ ORDER BY rank
613
+ LIMIT ?
614
+ `).all(query, limit);
615
+
616
+ return rows.map((row) => ({
617
+ id: row.id as number,
618
+ timestamp: row.timestamp as string,
619
+ content: row.content as string,
620
+ project: row.project as string | undefined,
621
+ session_id: row.session_id as number | undefined,
622
+ }));
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Get default database path
628
+ */
629
+ export function getDefaultDbPath(): string {
630
+ return DEFAULT_DB_PATH;
631
+ }
632
+
633
+ /**
634
+ * Check if database exists
635
+ */
636
+ export function dbExists(path?: string): boolean {
637
+ return existsSync(path ?? DEFAULT_DB_PATH);
638
+ }