personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

retire root-level sol.py and media.py; install-time verify

- Move sol.py -> think/sol_cli.py and media.py -> think/media.py via git mv (rename-only; no content change).
- Drop [tool.setuptools] py-modules block from pyproject.toml; update console-script entry point to think.sol_cli:main. Existing packages.find include "think*" already covers the moved files.
- Update every import site: 6 production modules (think/retention.py, think/utils.py, think/importers/shared.py, observe/utils.py, observe/transcribe/main.py, apps/import/routes.py) plus 3 test modules (tests/test_sol.py, tests/test_heartbeat.py, tests/test_service.py). Update scripts/gate_agents_rename.py to track the new sol_cli.py path.
- Add .python-version-hash marker (cmp-guarded, driven by a FORCE prereq) as a prerequisite of .installed, so the editable install is rebuilt when the system Python minor version changes.
- Add an install-time smoke-verify on the phony `install` target: probe `from think.sol_cli import main` from /tmp using $(CURDIR)/$(VENV_BIN)/python; on failure, run one bounded `uv pip install -e . --no-deps` recovery and re-probe; if still broken, exit non-zero with a pointer to `make clean-install`. This lives on `install` (not inside `.installed`) so it fires on every `make install` invocation, which is what .pth-deletion recovery requires — a deliberate refinement of scope §5 to satisfy scope §6.
- New tests/test_install_verify.py (4 subprocess-based smoke tests; skip cleanly if .venv is absent): sol_cli import from /tmp, media import from /tmp, `.venv/bin/sol --version`, and editable-finder MAPPING has no 'sol'/'media' keys.
- Update active-layout doc mentions in AGENTS.md, docs/project-structure.md, docs/SOLCLI.md, docs/design/observer-actions.md, and the think/link/service.py docstring.
- .gitignore gains .python-version-hash.

Eliminates the structural fragility that caused Ramon's 2026-04-22 install failure (req_afbcvppi): the py-modules declaration combined with setuptools' with_suffix('.py') fallback + an interpreter-blind `.installed` marker meant that stale editable-finder mappings persisted silently across Python minor-version bumps. The editable install now registers cleanly, self-checks on install, and regenerates when the interpreter changes.

+128 -38
+1
.gitignore
··· 19 19 *.port 20 20 /config*.py 21 21 .installed 22 + .python-version-hash 22 23 /journal/* 23 24 .agents/ 24 25 .claude/
+4 -4
AGENTS.md
··· 17 17 Read, in order, when you enter the repo for a coding task: 18 18 19 19 1. **This file through §8** — the invariants must be in working memory before your first edit. 20 - 2. **`sol.py`** — the CLI entry point. Skim the `COMMANDS`, `ALIASES`, and `GROUPS` dicts. ~340 lines, scannable in one pass. You now know the whole top-level command surface. 20 + 2. **`think/sol_cli.py`** — the CLI entry point. Skim the `COMMANDS`, `ALIASES`, and `GROUPS` dicts. ~340 lines, scannable in one pass. You now know the whole top-level command surface. 21 21 3. **`think/top.py` (first ~100 lines)** — the interactive TUI. Ties callosum + supervisor + service status together in one vantage point. Good "oh, this is how it connects" moment. 22 22 4. **The area you're about to touch:** 23 23 - User-visible feature or `sol call <app> <verb>` → `apps/<name>/call.py` + `apps/<name>/routes.py` + `apps/<name>/templates/`. ··· 33 33 34 34 | Dir | Purpose | Go here when | Depth doc | 35 35 |-----|---------|--------------|-----------| 36 - | `sol.py` | CLI entry point — `COMMANDS` / `ALIASES` / `GROUPS` dicts | adding a top-level `sol <cmd>` | `docs/SOLCLI.md` | 36 + | `think/sol_cli.py` | CLI entry point — `COMMANDS` / `ALIASES` / `GROUPS` dicts | adding a top-level `sol <cmd>` | `docs/SOLCLI.md` | 37 37 | `observe/` | Multimodal capture — screen, audio, transcribe, describe, sense, transfer | capture-side bugs, new input modalities | `docs/OBSERVE.md` | 38 38 | `think/` | Post-processing core — cortex, talent, callosum, indexer, entities, facets, activities, scheduler, heartbeat, supervisor | anything downstream of capture; most coder work lives here | `docs/THINK.md`, `docs/CORTEX.md`, `docs/CALLOSUM.md` | 39 39 | `convey/` | Web app framework — app discovery, routing, bridge, screenshot tooling | layout / framework-level UI changes | `docs/CONVEY.md` | ··· 68 68 69 69 Two surfaces: 70 70 71 - - **`sol <command>`** — top-level commands registered in `sol.py`'s `COMMANDS` dict (e.g., `sol import`, `sol think`, `sol indexer`, `sol supervisor`, `sol heartbeat`). `ALIASES` provides a couple of shorthand compound commands (`sol start` → `sol supervisor`, `sol up/down` → `sol service up/down`). 71 + - **`sol <command>`** — top-level commands registered in `think/sol_cli.py`'s `COMMANDS` dict (e.g., `sol import`, `sol think`, `sol indexer`, `sol supervisor`, `sol heartbeat`). `ALIASES` provides a couple of shorthand compound commands (`sol start` → `sol supervisor`, `sol up/down` → `sol service up/down`). 72 72 - **`sol call <app> <verb>`** — routes to `think/call.py`, which discovers each `apps/*/call.py` Typer sub-app and mounts it as a subcommand. Example: `sol call entities list`, `sol call activities create`, `sol call journal search`. 73 73 74 - **Adding a top-level command:** add an entry to `COMMANDS` in `sol.py`; ensure the module has a `main()` function. 74 + **Adding a top-level command:** add an entry to `COMMANDS` in `think/sol_cli.py`; ensure the module has a `main()` function. 75 75 76 76 **Adding a `sol call` sub-verb:** add it to the app's `apps/<app>/call.py` Typer sub-app. No central registration needed — `think/call.py` discovers apps automatically. 77 77
+19 -2
Makefile
··· 7 7 # all runs to one path and pytest wipes it on startup, destroying concurrent state. 8 8 export TMPDIR := /var/tmp 9 9 10 - .PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check install-checks ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename check-layer-hygiene doctor 10 + .PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check install-checks ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename check-layer-hygiene doctor FORCE 11 11 12 12 # Default target - install package in editable mode 13 13 all: install ··· 36 36 # User bin directory for symlink (standard location, usually already in PATH) 37 37 USER_BIN := $(HOME)/.local/bin 38 38 39 + .python-version-hash: FORCE 40 + @tmp_file=$$(mktemp); \ 41 + python3 -c "import sys; print(sys.version_info[:2])" > "$$tmp_file"; \ 42 + if [ ! -f $@ ] || ! cmp -s "$$tmp_file" $@; then mv "$$tmp_file" $@; else rm -f "$$tmp_file"; fi 43 + 39 44 # Marker file to track installation 40 - .installed: pyproject.toml uv.lock 45 + .installed: pyproject.toml uv.lock .python-version-hash 41 46 @echo "Installing package with uv..." 42 47 $(UV) sync 43 48 @# Python 3.14+ needs onnxruntime from nightly (not yet on PyPI) ··· 57 62 58 63 # Install package in editable mode with isolated venv 59 64 install: doctor skills .installed 65 + @(cd /tmp && $(CURDIR)/$(VENV_BIN)/python -c "from think.sol_cli import main") 2>/dev/null || { \ 66 + echo ">>> re-registering editable install"; \ 67 + $(UV) pip install -e . --no-deps; \ 68 + if (cd /tmp && $(CURDIR)/$(VENV_BIN)/python -c "from think.sol_cli import main"); then \ 69 + echo ">>> re-registered successfully"; \ 70 + else \ 71 + echo ">>> editable install still broken; run make clean-install"; \ 72 + exit 1; \ 73 + fi; \ 74 + } 60 75 61 76 # Directories where AI coding agents look for skills 62 77 SKILL_DIRS := journal/.agents/skills journal/.claude/skills ··· 484 499 uninstall: 485 500 @echo "Error: 'make uninstall' is disabled. Use the 'uninstall-service' target to remove installed user/system artifacts, or 'make clean-install' to rebuild the local dev environment." >&2 486 501 @exit 1 502 + 503 + FORCE: 487 504 488 505 # Clean everything and reinstall 489 506 clean-install: clean
+1 -1
apps/import/routes.py
··· 15 15 16 16 from apps.utils import log_app_action 17 17 from convey import emit, state 18 - from media import MEDIA_EXTENSIONS 19 18 from think.detect_created import detect_created 20 19 from think.importers.utils import ( 21 20 build_import_info, ··· 28 27 update_import_metadata_fields, 29 28 write_import_metadata, 30 29 ) 30 + from think.media import MEDIA_EXTENSIONS 31 31 from think.utils import day_path, now_ms 32 32 33 33 from .journal_sources import (
+5 -5
docs/SOLCLI.md
··· 23 23 24 24 ### How they work 25 25 26 - `sol.py` contains a static `COMMANDS` dict mapping command names to module paths: 26 + `think/sol_cli.py` contains a static `COMMANDS` dict mapping command names to module paths: 27 27 28 28 ```python 29 29 COMMANDS: dict[str, str] = { ··· 52 52 # ... implementation 53 53 ``` 54 54 55 - 2. **Register in `sol.py`** — add to `COMMANDS`: 55 + 2. **Register in `think/sol_cli.py`** — add to `COMMANDS`: 56 56 57 57 ```python 58 58 COMMANDS: dict[str, str] = { ··· 69 69 70 70 | File | What to do | 71 71 |------|-----------| 72 - | `sol.py` COMMANDS dict | Register the command | 73 - | `sol.py` GROUPS dict | Add to appropriate group | 72 + | `think/sol_cli.py` `COMMANDS` dict | Register the command | 73 + | `think/sol_cli.py` `GROUPS` dict | Add to appropriate group | 74 74 | Module file (e.g., `think/my_cmd.py`) | Implement with `main()` | 75 75 76 76 ## Call Commands (`sol call <app> <cmd>`) ··· 252 252 253 253 ``` 254 254 solstone/ 255 - ├── sol.py # Entry point + COMMANDS registry 256 255 ├── think/ 256 + │ ├── sol_cli.py # Entry point + COMMANDS registry 257 257 │ ├── call.py # sol call gateway (Typer root + mounts) 258 258 │ ├── tools/ 259 259 │ │ ├── call.py # sol call journal (built-in)
+2 -2
docs/design/observer-actions.md
··· 246 246 - The client should upload `audio.m4a`, not an arbitrary stem like `meeting.m4a`, so transcription writes `audio.jsonl` and existing readers keep finding the transcript under their current glob conventions. 247 247 - The transcribe pipeline writes the transcript as a sibling JSONL using the raw file stem, so `audio.m4a` becomes `audio.jsonl` (`observe/transcribe/main.py:165`, `observe/transcribe/main.py:594-609`). 248 248 - Transcript/cluster consumers currently glob for `*audio.jsonl`, `audio.jsonl`, or `*_audio.jsonl`, not arbitrary stems (`think/cluster.py:132-136`, `think/cluster.py:397-405`, `apps/transcripts/routes.py:256-258`, `think/retention.py:73-100`). 249 - - `.m4a` itself is supported end-to-end: the media registry includes it, the sensor registers all `AUDIO_EXTENSIONS`, and transcribe accepts `.m4a` as a supported raw input (`media.py:6-25`, `observe/sense.py:606-667`, `observe/sense.py:1096-1100`, `observe/transcribe/main.py:58`, `observe/transcribe/main.py:885-889`). 249 + - `.m4a` itself is supported end-to-end: the media registry includes it, the sensor registers all `AUDIO_EXTENSIONS`, and transcribe accepts `.m4a` as a supported raw input (`think/media.py:6-25`, `observe/sense.py:606-667`, `observe/sense.py:1096-1100`, `observe/transcribe/main.py:58`, `observe/transcribe/main.py:885-889`). 250 250 251 251 Operational caveats: 252 252 ··· 327 327 - `apps/observer/tests/test_routes.py:1276-1368`, `apps/observer/tests/test_routes.py:1552-1586`, `apps/observer/tests/test_routes.py:1654-1694` 328 328 - `observe/transcribe/main.py:58`, `observe/transcribe/main.py:165`, `observe/transcribe/main.py:594-609`, `observe/transcribe/main.py:885-889` 329 329 - `observe/sense.py:606-667`, `observe/sense.py:1096-1100` 330 - - `media.py:6-25` 330 + - `think/media.py:6-25` 331 331 - `think/cluster.py:132-136`, `think/cluster.py:397-405` 332 332 - `apps/transcripts/routes.py:256-258` 333 333 - `think/retention.py:73-100`
+4 -4
docs/project-structure.md
··· 4 4 5 5 ```text 6 6 solstone/ 7 - ├── sol.py # Unified CLI entry point (run: sol <command>) 8 7 ├── observe/ # Multimodal capture & AI analysis 9 8 ├── think/ # Data post-processing, AI agents & orchestration 9 + │ └── sol_cli.py # Unified CLI entry point (run: sol <command>) 10 10 ├── convey/ # Web app frontend & backend 11 11 ├── apps/ # Convey app extensions (see docs/APPS.md) 12 12 ├── talent/ # Agent/generator configs + Agent Skills (talent/*/SKILL.md) ··· 24 24 - **Python**: Requires Python 3.10+ 25 25 - **Modules**: Each top-level folder is a Python package with `__init__.py` unless it is data-only (e.g., `tests/fixtures/`) 26 26 - **Imports**: Prefer absolute imports (e.g., `from think.utils import setup_cli`) whenever feasible 27 - - **Entry Points**: Commands are registered in `sol.py`'s `COMMANDS` dict (pyproject.toml just defines the `sol` entry point) 27 + - **Entry Points**: Commands are registered in `think/sol_cli.py`'s `COMMANDS` dict (pyproject.toml just defines the `sol` entry point) 28 28 - **Journal**: Data stored under `journal/` at the project root; day content lives under `journal/chronicle/` 29 29 - **Calling**: When calling other modules as a separate process always use `sol <command>` and never call using `python -m ...` (e.g., use `sol indexer`, NOT `python -m think.indexer`) 30 30 31 31 ## CLI Routing 32 32 33 - `sol.py`'s `COMMANDS` dict maps command names to module paths. The unified CLI is `sol`. Run `sol` to see status and available commands. `sol call` routes to `think/call.py`, which discovers `apps/*/call.py` Typer sub-apps and mounts them as subcommands. 33 + `think/sol_cli.py`'s `COMMANDS` dict maps command names to module paths. The unified CLI is `sol`. Run `sol` to see status and available commands. `sol call` routes to `think/call.py`, which discovers `apps/*/call.py` Typer sub-apps and mounts them as subcommands. 34 34 35 35 ## Agent & Skill Organization 36 36 ··· 38 38 39 39 ## File Locations 40 40 41 - - **Entry Points**: `sol.py` `COMMANDS` dict 41 + - **Entry Points**: `think/sol_cli.py` `COMMANDS` dict 42 42 - **Test Fixtures**: `tests/fixtures/journal/` - complete mock journal 43 43 - **Live Logs**: `journal/health/<service>.log` 44 44 - **Agent Personas**: `talent/*.md` (apps can add their own in `talent/`, see [docs/APPS.md](docs/APPS.md))
media.py think/media.py
+1 -1
observe/transcribe/main.py
··· 55 55 56 56 import numpy as np 57 57 58 - from media import AUDIO_EXTENSIONS as SUPPORTED_AUDIO_FORMATS 59 58 from observe.transcribe import ( 60 59 BACKEND_REGISTRY, 61 60 get_backend, ··· 72 71 run_vad, 73 72 ) 74 73 from think.callosum import callosum_send 74 + from think.media import AUDIO_EXTENSIONS as SUPPORTED_AUDIO_FORMATS 75 75 from think.utils import ( 76 76 day_dirs, 77 77 day_from_path,
+2 -2
observe/utils.py
··· 16 16 import numpy as np 17 17 import soundfile as sf 18 18 19 - from media import AUDIO_EXTENSIONS as _AUDIO_EXTENSIONS 20 - from media import VIDEO_EXTENSIONS as _VIDEO_EXTENSIONS 19 + from think.media import AUDIO_EXTENSIONS as _AUDIO_EXTENSIONS 20 + from think.media import VIDEO_EXTENSIONS as _VIDEO_EXTENSIONS 21 21 from think.utils import day_path 22 22 23 23 logger = logging.getLogger(__name__)
+1 -4
pyproject.toml
··· 94 94 ] 95 95 96 96 [project.scripts] 97 - sol = "sol:main" 97 + sol = "think.sol_cli:main" 98 98 99 99 [project.urls] 100 100 Homepage = "https://github.com/solpbc/solstone" ··· 104 104 105 105 [tool.setuptools.packages.find] 106 106 include = ["apps*", "think*", "convey*", "observe*", "talent*"] 107 - 108 - [tool.setuptools] 109 - py-modules = ["media", "sol"] 110 107 111 108 [tool.setuptools.package-data] 112 109 apps = ["*/templates/*.html", "*/talent/*.md"]
+1 -1
scripts/gate_agents_rename.py
··· 63 63 64 64 def is_production(path: Path) -> bool: 65 65 path_str = path.as_posix() 66 - return path_str == "sol.py" or path_str.startswith(PRODUCTION_PREFIXES) 66 + return path_str == "think/sol_cli.py" or path_str.startswith(PRODUCTION_PREFIXES) 67 67 68 68 69 69 def iter_lines(path: Path) -> list[tuple[int, str]]:
sol.py think/sol_cli.py
+1 -1
tests/test_heartbeat.py
··· 32 32 33 33 def test_heartbeat_command_mapping(): 34 34 """heartbeat key in COMMANDS maps to think.heartbeat module.""" 35 - from sol import COMMANDS 35 + from think.sol_cli import COMMANDS 36 36 37 37 assert COMMANDS["heartbeat"] == "think.heartbeat" 38 38
+75
tests/test_install_verify.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + import ast 5 + import subprocess 6 + from pathlib import Path 7 + 8 + import pytest 9 + 10 + REPO_ROOT = Path(__file__).resolve().parents[1] 11 + VENV_PYTHON = REPO_ROOT / ".venv/bin/python" 12 + SOL_BIN = REPO_ROOT / ".venv/bin/sol" 13 + 14 + 15 + def _run_in_tmp(args: list[str]) -> subprocess.CompletedProcess[str]: 16 + if not VENV_PYTHON.exists(): 17 + pytest.skip("venv not installed") 18 + return subprocess.run( 19 + args, 20 + cwd="/tmp", 21 + timeout=30, 22 + capture_output=True, 23 + text=True, 24 + ) 25 + 26 + 27 + def test_import_think_sol_cli_from_tmp(): 28 + result = _run_in_tmp([str(VENV_PYTHON), "-c", "from think.sol_cli import main"]) 29 + 30 + assert result.returncode == 0, result.stderr 31 + 32 + 33 + def test_import_think_media_from_tmp(): 34 + result = _run_in_tmp([str(VENV_PYTHON), "-c", "from think.media import MIME_TYPES"]) 35 + 36 + assert result.returncode == 0, result.stderr 37 + 38 + 39 + def test_sol_version_from_tmp(): 40 + if not SOL_BIN.exists(): 41 + pytest.skip("sol console script not installed") 42 + 43 + result = _run_in_tmp([str(SOL_BIN), "--version"]) 44 + 45 + assert result.returncode == 0, result.stderr 46 + assert result.stdout.strip() == "sol (solstone) 0.1.0" 47 + 48 + 49 + def test_editable_finder_mapping_has_no_root_aliases(): 50 + finder_paths = list( 51 + (REPO_ROOT / ".venv" / "lib").glob( 52 + "python*/site-packages/__editable___solstone_0_1_0_finder.py" 53 + ) 54 + ) 55 + if not finder_paths: 56 + pytest.skip("editable finder not installed") 57 + 58 + module = ast.parse(finder_paths[0].read_text(encoding="utf-8")) 59 + mapping = None 60 + for node in module.body: 61 + if isinstance(node, ast.Assign): 62 + for target in node.targets: 63 + if isinstance(target, ast.Name) and target.id == "MAPPING": 64 + mapping = ast.literal_eval(node.value) 65 + break 66 + elif isinstance(node, ast.AnnAssign): 67 + if isinstance(node.target, ast.Name) and node.target.id == "MAPPING": 68 + mapping = ast.literal_eval(node.value) 69 + else: 70 + continue 71 + if mapping is not None: 72 + break 73 + 74 + assert mapping is not None, "MAPPING assignment not found" 75 + assert "sol" not in mapping and "media" not in mapping
+4 -4
tests/test_service.py
··· 262 262 263 263 class TestRegistry: 264 264 def test_service_command_registered(self): 265 - import sol 265 + from think import sol_cli as sol 266 266 267 267 assert "service" in sol.COMMANDS 268 268 assert sol.COMMANDS["service"] == "think.service" 269 269 270 270 def test_up_alias(self): 271 - import sol 271 + from think import sol_cli as sol 272 272 273 273 assert "up" in sol.ALIASES 274 274 assert sol.ALIASES["up"] == ("think.service", ["up"]) 275 275 276 276 def test_down_alias(self): 277 - import sol 277 + from think import sol_cli as sol 278 278 279 279 assert "down" in sol.ALIASES 280 280 assert sol.ALIASES["down"] == ("think.service", ["down"]) 281 281 282 282 def test_service_group_exists(self): 283 - import sol 283 + from think import sol_cli as sol 284 284 285 285 assert "Service" in sol.GROUPS 286 286 assert "service" in sol.GROUPS["Service"]
+1 -1
tests/test_sol.py
··· 8 8 9 9 import pytest 10 10 11 - import sol 11 + from think import sol_cli as sol 12 12 13 13 14 14 class TestResolveCommand:
+1 -1
think/importers/shared.py
··· 12 12 from pathlib import Path 13 13 from typing import Any, Callable 14 14 15 - from media import MIME_TYPES 16 15 from think.importers.utils import save_import_file, write_import_metadata 16 + from think.media import MIME_TYPES 17 17 from think.utils import day_path, get_journal, now_ms 18 18 19 19 logger = logging.getLogger(__name__)
+1 -1
think/link/service.py
··· 3 3 4 4 """link service runtime. 5 5 6 - Registered with solstone's supervisor via `sol.py` COMMANDS (see `sol link`); 6 + Registered with solstone's supervisor via `think/sol_cli.py` COMMANDS (see `sol link`); 7 7 the supervisor launches this as a subprocess alongside callosum, cortex, 8 8 convey, etc. Service lifecycle: 9 9
+3 -3
think/retention.py
··· 29 29 from pathlib import Path 30 30 from typing import Any 31 31 32 - from media import AUDIO_EXTENSIONS as RAW_AUDIO_EXTENSIONS 33 - from media import MEDIA_EXTENSIONS as RAW_MEDIA_EXTENSIONS 34 - from media import VIDEO_EXTENSIONS as RAW_VIDEO_EXTENSIONS 32 + from think.media import AUDIO_EXTENSIONS as RAW_AUDIO_EXTENSIONS 33 + from think.media import MEDIA_EXTENSIONS as RAW_MEDIA_EXTENSIONS 34 + from think.media import VIDEO_EXTENSIONS as RAW_VIDEO_EXTENSIONS 35 35 from think.utils import day_dirs, get_journal, iter_segments 36 36 37 37 logger = logging.getLogger(__name__)
+1 -1
think/utils.py
··· 26 26 27 27 from timefhuman import timefhuman 28 28 29 - from media import MIME_TYPES 29 + from think.media import MIME_TYPES 30 30 31 31 DATE_RE = re.compile(r"\d{8}") 32 32 STREAM_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")