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.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- 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
|
+
}
|