personal memory agent
1# solstone Makefile
2# Python-based AI-driven desktop journaling toolkit
3
4.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service
5
6# Default target - install package in editable mode
7all: install
8
9# Virtual environment directory
10VENV := .venv
11VENV_BIN := $(VENV)/bin
12PYTHON := $(VENV_BIN)/python
13
14# Require uv
15UV := $(shell command -v uv 2>/dev/null)
16ifndef UV
17$(error uv is not installed. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh)
18endif
19
20# User bin directory for symlink (standard location, usually already in PATH)
21USER_BIN := $(HOME)/.local/bin
22
23# Marker file to track installation
24.installed: pyproject.toml uv.lock
25 @echo "Installing package with uv..."
26 $(UV) sync
27 @# Python 3.14+ needs onnxruntime from nightly (not yet on PyPI)
28 @PY_MINOR=$$($(PYTHON) -c "import sys; print(sys.version_info.minor)"); \
29 if [ "$$PY_MINOR" -ge 14 ]; then \
30 echo "Python 3.14+ detected - installing onnxruntime from nightly feed..."; \
31 $(UV) pip install --pre --no-deps --index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/pypi/simple/ onnxruntime; \
32 fi
33 @echo "Installing Playwright browser for sol screenshot..."
34 $(VENV_BIN)/playwright install chromium
35 @if [ -d .git ]; then \
36 mkdir -p $(USER_BIN); \
37 ln -sf $(CURDIR)/$(VENV_BIN)/sol $(USER_BIN)/sol; \
38 echo ""; \
39 echo "Done! 'sol' command installed to $(USER_BIN)/sol"; \
40 if ! echo "$$PATH" | grep -q "$(USER_BIN)"; then \
41 echo ""; \
42 echo "NOTE: $(USER_BIN) is not in your PATH."; \
43 echo "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):"; \
44 echo " export PATH=\"\$$HOME/.local/bin:\$$PATH\""; \
45 echo ""; \
46 echo "Or run sol directly: $(CURDIR)/$(VENV_BIN)/sol"; \
47 fi; \
48 else \
49 echo ""; \
50 echo "Done! (worktree detected, skipping ~/.local/bin/sol symlink)"; \
51 fi
52 @$(MAKE) --no-print-directory skills
53 @if [ -d .git ] && [ -f skills/solstone/SKILL.md ]; then \
54 echo "Installing solstone skill user-wide..."; \
55 npx skills add ./skills/solstone -g -a claude-code -y; \
56 fi
57 @touch .installed
58
59# Generate lock file if missing
60uv.lock: pyproject.toml
61 $(UV) lock
62
63# Install package in editable mode with isolated venv
64install: .installed
65
66# Directories where AI coding agents look for skills
67SKILL_DIRS := .agents/skills .claude/skills
68
69# Discover SKILL.md files in talent/ and apps/*/talent/, symlink into agent skill dirs
70skills:
71 @# Collect all skill directories (containing SKILL.md)
72 @SKILLS=""; \
73 for skill_md in talent/*/SKILL.md apps/*/talent/*/SKILL.md; do \
74 [ -f "$$skill_md" ] || continue; \
75 skill_dir=$$(dirname "$$skill_md"); \
76 skill_name=$$(basename "$$skill_dir"); \
77 if echo "$$SKILLS" | grep -qw "$$skill_name"; then \
78 echo "Error: duplicate skill name '$$skill_name' found in $$skill_dir" >&2; \
79 echo "Each skill directory name must be unique across talent/ and apps/*/talent/." >&2; \
80 exit 1; \
81 fi; \
82 SKILLS="$$SKILLS $$skill_name"; \
83 done; \
84 for dir in $(SKILL_DIRS); do \
85 mkdir -p "$$dir"; \
86 for link in "$$dir"/*; do \
87 [ -L "$$link" ] && rm -f "$$link"; \
88 done; \
89 done; \
90 count=0; \
91 for skill_md in talent/*/SKILL.md apps/*/talent/*/SKILL.md; do \
92 [ -f "$$skill_md" ] || continue; \
93 skill_dir=$$(dirname "$$skill_md"); \
94 skill_name=$$(basename "$$skill_dir"); \
95 for dir in $(SKILL_DIRS); do \
96 ln -sf "../../$$skill_dir" "$$dir/$$skill_name"; \
97 done; \
98 count=$$((count + 1)); \
99 done; \
100 if [ "$$count" -gt 0 ]; then \
101 echo "Linked $$count skill(s) into $(SKILL_DIRS)"; \
102 fi
103 @$(PYTHON) scripts/generate_agents_md.py
104
105# Start local dev stack against fixture journal (no observers, no daily processing)
106dev: .installed
107 $(TEST_ENV) PATH=$(CURDIR)/$(VENV_BIN):$$PATH $(VENV_BIN)/sol supervisor 0 --no-observers --no-daily
108
109# Restart solstone service (noop in dev mode)
110sail: .installed
111 $(VENV_BIN)/sol service restart --if-installed
112
113# Start sandbox stack: fixture copy + background supervisor + readiness wait
114sandbox: .installed
115 @# Fail if sandbox already running
116 @if [ -f .sandbox.pid ] && kill -0 $$(cat .sandbox.pid) 2>/dev/null; then \
117 echo "Sandbox already running (PID $$(cat .sandbox.pid))"; \
118 echo "Run 'make sandbox-stop' first."; \
119 exit 1; \
120 fi
121 @# Clean up stale state from a previous crashed sandbox
122 @if [ -f .sandbox.journal ]; then \
123 rm -rf "$$(cat .sandbox.journal)" 2>/dev/null; \
124 rm -f .sandbox.pid .sandbox.journal; \
125 fi
126 @# Copy fixtures to temp dir
127 @SANDBOX_JOURNAL=$$(mktemp -d /tmp/solstone-sandbox-XXXXXX); \
128 cp -r tests/fixtures/journal/* "$$SANDBOX_JOURNAL/"; \
129 echo "$$SANDBOX_JOURNAL" > .sandbox.journal; \
130 echo "Sandbox journal: $$SANDBOX_JOURNAL"; \
131 # Boot supervisor in background \
132 _SOLSTONE_JOURNAL_OVERRIDE="$$SANDBOX_JOURNAL" PATH=$(CURDIR)/$(VENV_BIN):$$PATH \
133 $(VENV_BIN)/sol supervisor 0 --no-observers --no-daily \
134 > "$$SANDBOX_JOURNAL/health/supervisor.log" 2>&1 & \
135 echo $$! > .sandbox.pid; \
136 echo "Supervisor PID: $$(cat .sandbox.pid)"; \
137 # Poll for readiness \
138 echo "Waiting for services..."; \
139 READY=false; \
140 for i in $$(seq 1 20); do \
141 if _SOLSTONE_JOURNAL_OVERRIDE="$$SANDBOX_JOURNAL" $(VENV_BIN)/sol health > /dev/null 2>&1; then \
142 READY=true; \
143 break; \
144 fi; \
145 sleep 1; \
146 done; \
147 if [ "$$READY" = "false" ]; then \
148 echo "Readiness timeout - killing supervisor"; \
149 kill $$(cat .sandbox.pid) 2>/dev/null || true; \
150 rm -rf "$$SANDBOX_JOURNAL" .sandbox.pid .sandbox.journal; \
151 exit 1; \
152 fi; \
153 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port" 2>/dev/null); \
154 echo ""; \
155 echo "Sandbox is ready!"; \
156 echo " Convey: http://localhost:$$CONVEY_PORT/"; \
157 echo " Journal: $$SANDBOX_JOURNAL"; \
158 echo " Stop: make sandbox-stop"
159
160# Stop sandbox: terminate supervisor, clean up temp dir and state files
161sandbox-stop:
162 @if [ ! -f .sandbox.pid ]; then \
163 echo "No sandbox running."; \
164 exit 0; \
165 fi; \
166 PID=$$(cat .sandbox.pid); \
167 echo "Stopping supervisor (PID $$PID)..."; \
168 kill "$$PID" 2>/dev/null || true; \
169 # Wait up to 5s for clean shutdown \
170 for i in $$(seq 1 10); do \
171 kill -0 "$$PID" 2>/dev/null || break; \
172 sleep 0.5; \
173 done; \
174 kill -9 "$$PID" 2>/dev/null || true; \
175 if [ -f .sandbox.journal ]; then \
176 SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
177 rm -rf "$$SANDBOX_JOURNAL"; \
178 echo "Removed $$SANDBOX_JOURNAL"; \
179 fi; \
180 rm -f .sandbox.pid .sandbox.journal; \
181 echo "Sandbox stopped."
182
183# Verify API baselines against running sandbox
184verify-api: .installed
185 @echo "Verifying API baselines (sandbox)..."
186 @$(MAKE) sandbox
187 @SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
188 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
189 RESULT=0; \
190 _SOLSTONE_JOURNAL_OVERRIDE="$$SANDBOX_JOURNAL" $(VENV_BIN)/python tests/verify_api.py verify --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
191 $(MAKE) sandbox-stop; \
192 exit $$RESULT
193
194# Regenerate all API baseline files from current responses (uses sandbox for consistency)
195update-api-baselines: .installed
196 @echo "Updating API baselines (sandbox)..."
197 @$(MAKE) sandbox
198 @SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
199 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
200 RESULT=0; \
201 _SOLSTONE_JOURNAL_OVERRIDE="$$SANDBOX_JOURNAL" $(VENV_BIN)/python tests/verify_api.py update --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
202 $(MAKE) sandbox-stop; \
203 exit $$RESULT
204
205
206# Install pinchtab browser automation tool
207install-pinchtab:
208 @if command -v pinchtab >/dev/null 2>&1; then \
209 echo "pinchtab already installed: $$(pinchtab --version 2>/dev/null || echo 'unknown')"; \
210 else \
211 echo "Installing pinchtab..."; \
212 curl -fsSL https://pinchtab.com/install.sh | bash; \
213 fi
214
215# Run browser scenarios against sandbox
216verify-browser: .installed
217 @echo "Running browser scenarios (sandbox)..."
218 @$(MAKE) sandbox
219 @SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
220 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
221 RESULT=0; \
222 $(VENV_BIN)/python tests/verify_browser.py verify --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
223 $(MAKE) sandbox-stop; \
224 exit $$RESULT
225
226# Re-capture all browser baseline screenshots
227update-browser-baselines: .installed
228 @echo "Updating browser baselines (sandbox)..."
229 @$(MAKE) sandbox
230 @SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
231 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
232 RESULT=0; \
233 $(VENV_BIN)/python tests/verify_browser.py update --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
234 $(MAKE) sandbox-stop; \
235 exit $$RESULT
236
237# Full product verification: API baselines + browser scenarios
238review: .installed
239 @command -v pinchtab >/dev/null 2>&1 || { \
240 echo "pinchtab is required for browser verification."; \
241 echo "Run 'make install-pinchtab' to install it."; \
242 exit 1; \
243 }
244 @echo "=== Starting review ==="
245 @$(MAKE) sandbox
246 @SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
247 CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
248 BASE_URL="http://localhost:$$CONVEY_PORT"; \
249 RESULT_API=0; \
250 RESULT_BROWSER=0; \
251 echo ""; \
252 echo "=== API baseline verification ==="; \
253 _SOLSTONE_JOURNAL_OVERRIDE="$$SANDBOX_JOURNAL" $(VENV_BIN)/python tests/verify_api.py verify --base-url "$$BASE_URL" || RESULT_API=$$?; \
254 echo ""; \
255 echo "=== Browser scenario verification ==="; \
256 $(VENV_BIN)/python tests/verify_browser.py verify --base-url "$$BASE_URL" || RESULT_BROWSER=$$?; \
257 echo ""; \
258 echo "=== Stopping sandbox ==="; \
259 $(MAKE) sandbox-stop; \
260 echo ""; \
261 echo "=== Review Summary ==="; \
262 if [ $$RESULT_API -eq 0 ]; then \
263 echo " API: PASS"; \
264 else \
265 echo " API: FAIL"; \
266 fi; \
267 if [ $$RESULT_BROWSER -eq 0 ]; then \
268 echo " Browser: PASS"; \
269 else \
270 echo " Browser: FAIL"; \
271 fi; \
272 echo ""; \
273 if [ $$RESULT_API -eq 0 ] && [ $$RESULT_BROWSER -eq 0 ]; then \
274 echo "Review: ALL PASS"; \
275 else \
276 echo "Review: FAIL"; \
277 exit 1; \
278 fi
279
280# Test environment - use fixtures journal for all tests
281TEST_ENV = _SOLSTONE_JOURNAL_OVERRIDE=tests/fixtures/journal
282
283# Venv tool shortcuts
284PYTEST := $(VENV_BIN)/pytest
285RUFF := $(VENV_BIN)/ruff
286MYPY := $(VENV_BIN)/mypy
287
288# Run core tests (excluding integration and app tests)
289test: .installed
290 @echo "Running core tests..."
291 $(TEST_ENV) $(PYTEST) tests/ -q --cov=. --ignore=tests/integration
292
293# Run app tests
294test-apps: .installed
295 @echo "Running app tests..."
296 $(TEST_ENV) $(PYTEST) apps/ -q
297
298# Run specific app tests
299test-app: .installed
300 @if [ -z "$(APP)" ]; then \
301 echo "Usage: make test-app APP=<app_name>"; \
302 echo "Example: make test-app APP=todos"; \
303 exit 1; \
304 fi
305 $(TEST_ENV) $(PYTEST) apps/$(APP)/tests/ -v
306
307# Run specific test file or pattern
308test-only: .installed
309 @if [ -z "$(TEST)" ]; then \
310 echo "Usage: make test-only TEST=<test_file_or_pattern>"; \
311 echo "Example: make test-only TEST=tests/test_utils.py"; \
312 echo "Example: make test-only TEST=\"-k test_function_name\""; \
313 exit 1; \
314 fi
315 $(TEST_ENV) $(PYTEST) $(TEST)
316
317# Run integration tests
318test-integration: .installed
319 @echo "Running integration tests..."
320 $(TEST_ENV) $(PYTEST) tests/integration/ -v --tb=short --timeout=20
321
322# Run specific integration test
323test-integration-only: .installed
324 @if [ -z "$(TEST)" ]; then \
325 echo "Usage: make test-integration-only TEST=<test_file_or_pattern>"; \
326 echo "Example: make test-integration-only TEST=test_api.py"; \
327 exit 1; \
328 fi
329 $(TEST_ENV) $(PYTEST) tests/integration/$(TEST) --timeout=20
330
331# Run all tests (core + apps + integration)
332test-all: .installed
333 @echo "Running all tests (core + apps + integration)..."
334 $(TEST_ENV) $(PYTEST) tests/ -v --cov=. && $(TEST_ENV) $(PYTEST) apps/ -v --cov=. --cov-append
335
336# Auto-format and fix code, then report any remaining issues
337format: .installed
338 @echo "Formatting and fixing code with ruff..."
339 @$(RUFF) format .
340 @$(RUFF) check --fix .
341 @echo ""
342 @echo "Checking for remaining issues..."
343 @RUFF_OK=true; MYPY_OK=true; \
344 $(RUFF) check . || RUFF_OK=false; \
345 $(MYPY) . || MYPY_OK=false; \
346 if $$RUFF_OK && $$MYPY_OK; then \
347 echo ""; \
348 echo "All clean!"; \
349 else \
350 echo ""; \
351 echo "Issues above need manual fixes."; \
352 fi
353
354# Clean build artifacts and cache files
355clean:
356 @echo "Cleaning build artifacts and cache files..."
357 rm -rf build/ dist/ *.egg-info/
358 rm -rf .pytest_cache/ .coverage .mypy_cache/
359 rm -rf .agents/ .claude/
360 find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
361 find . -type f -name "*.pyc" -delete
362 find . -type f -name "*.pyo" -delete
363 find . -type f -name ".DS_Store" -delete
364 rm -f .installed
365
366# Service management (override port: make install-service PORT=8000)
367install-service: .installed
368 $(VENV_BIN)/sol service install --port $(or $(PORT),5015)
369 $(VENV_BIN)/sol service start
370 $(VENV_BIN)/sol service status
371
372uninstall-service:
373 -$(VENV_BIN)/sol service uninstall
374
375# Uninstall - remove venv and sol symlink
376uninstall: uninstall-service clean
377 @echo "Removing virtual environment..."
378 rm -rf $(VENV)
379 @if [ -L $(USER_BIN)/sol ]; then \
380 echo "Removing sol symlink from $(USER_BIN)..."; \
381 rm -f $(USER_BIN)/sol; \
382 fi
383
384# Clean everything and reinstall
385clean-install: uninstall install
386
387# Run continuous integration checks (what CI would run)
388ci: .installed
389 @echo "Running CI checks..."
390 @echo "=== Checking formatting ==="
391 @$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; }
392 @echo ""
393 @echo "=== Running ruff ==="
394 @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; }
395 @echo ""
396 @echo "=== Running mypy ==="
397 @$(MYPY) . || true
398 @echo ""
399 @echo "=== Running tests ==="
400 @$(MAKE) test
401 @echo ""
402 @echo "All CI checks passed!"
403
404# Watch for changes and run tests (requires pytest-watch)
405watch: .installed
406 @$(UV) pip show pytest-watch >/dev/null 2>&1 || { echo "Installing pytest-watch..."; $(UV) pip install pytest-watch; }
407 $(VENV_BIN)/ptw -- -q
408
409# Generate coverage report (core + apps, excluding core integration tests)
410coverage: .installed
411 $(TEST_ENV) $(PYTEST) tests/ --cov=. --cov-report=html --cov-report=term --ignore=tests/integration
412 $(TEST_ENV) $(PYTEST) apps/ --cov=. --cov-report=html --cov-report=term --cov-append
413 @echo "Coverage report generated in htmlcov/index.html"
414
415# Update all dependencies to latest versions and refresh genai-prices
416update: .installed
417 @echo "Updating all dependencies to latest versions..."
418 $(UV) lock --upgrade
419 $(UV) sync
420 @echo "Done. All packages updated to latest."
421
422# Update genai-prices to get latest model pricing data
423# Run this when adding new models or if pricing tests fail
424update-prices: .installed
425 @echo "Updating genai-prices to latest version..."
426 $(UV) lock --upgrade-package genai-prices
427 $(UV) sync
428 @echo "Done. Re-run tests to verify model pricing support."
429
430# Show installed package versions
431versions: .installed
432 @echo "=== Python version ==="
433 $(PYTHON) --version
434 @echo ""
435 @echo "=== Key package versions ==="
436 @$(UV) pip list | grep -E "^(pytest|ruff|mypy|Flask|numpy|Pillow|openai|anthropic|google-genai)" || true
437
438# Install pre-commit hooks (if using pre-commit)
439pre-commit: .installed
440 @$(UV) pip show pre-commit >/dev/null 2>&1 || { echo "Installing pre-commit..."; $(UV) pip install pre-commit; }
441 $(VENV_BIN)/pre-commit install
442 @echo "Pre-commit hooks installed!"