solstone Developer Guide#
This file is the developer guide for the solstone repository. Read it before writing code.
Audience:
- Coders (cwd = repo root, editing
observe/,think/,convey/,apps/,talent/,tests/) — you're in the right place. - Cogitate talents (cwd =
journal/, running inside the live system) — your entry istalent/journal/SKILL.md, installed intojournal/.claude/skills/journal/andjournal/.agents/skills/journal/. - Operators debugging a running system — see
docs/DOCTOR.md.
For the journal-side runtime entry point, see journal/AGENTS.md.
CLAUDE.md and GEMINI.md at the repo root are symlinks to this file.
1. Start here#
Read, in order, when you enter the repo for a coding task:
- This file through §8 — the invariants must be in working memory before your first edit.
think/sol_cli.py— the CLI entry point. Skim theCOMMANDS,ALIASES, andGROUPSdicts. ~340 lines, scannable in one pass. You now know the whole top-level command surface.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.- The area you're about to touch:
- User-visible feature or
sol call <app> <verb>→apps/<name>/call.py+apps/<name>/routes.py+apps/<name>/templates/. - Think pipeline →
think/<module>.py+ its tests. - AI talent prompt or behavior →
talent/<name>.md(+ optional.pypost-hook). - Capture / observe →
observe/<module>.py.
- User-visible feature or
- Run
sol(no args) — prints current journal status + grouped command list. Orients you to live state. make devormake sandboxwhen you need a running stack to iterate against.
If you cannot state in one sentence which module owns the data your change touches, stop and re-read §7 L2 (the domain ownership table). Writing to a domain from the wrong module is how we got the 14 layer violations the April 2026 audit catalogued.
2. Repo map#
| Dir | Purpose | Go here when | Depth doc |
|---|---|---|---|
think/sol_cli.py |
CLI entry point — COMMANDS / ALIASES / GROUPS dicts |
adding a top-level sol <cmd> |
docs/SOLCLI.md |
observe/ |
Multimodal capture — screen, audio, transcribe, describe, sense, transfer | capture-side bugs, new input modalities | docs/OBSERVE.md |
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 |
convey/ |
Web app framework — app discovery, routing, bridge | layout / framework-level UI changes | docs/CONVEY.md |
apps/ |
Convey apps — each self-contained (call.py Typer sub-app + routes.py + templates/) |
adding a user-facing feature, a sol call <app> verb, a UI surface |
docs/APPS.md (required reading before modifying apps/) |
talent/ |
AI talent configs (markdown prompts + optional .py post-hooks) + SKILL.mds (journal, coder, partner, …) |
defining or tuning a talent; adding a journal-side skill | talent/journal/SKILL.md, docs/PROMPT_TEMPLATES.md |
scripts/ |
Repo maintenance scripts — check_layer_hygiene.py |
tooling that guards the codebase; wired into make ci |
(none) |
tests/ |
Pytest suites + tests/fixtures/journal/ mock journal |
writing tests; debugging flakiness; make dev / make sandbox use fixtures as the journal |
docs/testing.md |
docs/ |
All longform documentation | reference lookups; never your first stop | §10 below |
journal/ |
The live journal (user data). Git-ignored content; checked-in template (AGENTS.md, skills symlinks) |
rarely as a coder — modify think/, apps/, or talent/, not journal data |
talent/journal/SKILL.md |
Top-level dirs intentionally not in the table: .venv/, scratch/, logs/, tmp/, observers/, routines/, skills/ — not active coder surfaces.
3. Mental model#
The pipeline: observe (capture) → JSON transcripts in journal/chronicle/YYYYMMDD/ → think (analyze) → SQLite index + derived artifacts → convey (web UI) and sol call CLIs.
Think is the center. observe feeds it raw material; convey + apps render its outputs; talent prompts + cortex run AI against it; indexer makes it searchable. A change in think/ usually ripples outward.
Key concepts, priority-ordered:
- Journal — the on-disk record rooted at
journal/in the repo. Every day is ajournal/chronicle/YYYYMMDD/directory. Segments (timestamped capture windows) are anchored to creation/modification time, not content "about" time.get_journal()fromthink.utilsis the single source of truth for journal path resolution; trust it unconditionally. Installed runs inheritSOLSTONE_JOURNALfrom the managed wrapper at~/.local/bin/sol; tests use the autouse fixture; sandboxes set it explicitly. Application code must not set it itself (see §8). - Talents — AI processors (markdown prompt + optional Python post-hook). Each has a config in
talent/<name>.mdwith frontmatter that declares hooks, priority, model, and output. Cortex spawns them as subprocesses. - Callosum — Unix-socket JSON message bus at
journal/health/callosum.sock. Real-time event distribution across services (tract+event+ payload). If components need to talk asynchronously, they talk through callosum. - Cortex — process manager for talent runs. Listens on callosum (
tract="cortex",event="request"), spawnspython -m think.talentssubprocesses, writes<talent>/<ts>_active.jsonlthen renames to<talent>/<ts>.jsonlon completion, broadcasts all events back through callosum. Readdocs/CORTEX.mdbefore modifying talent execution. - Facets — project/context scopes (
work,personal, …). Group related entities, activities, and relationships. Facet data lives underjournal/facets/<facet>/. - Entities — tracked people / projects / tools. Extracted from transcripts and accumulated across time. Canonical records in
journal/entities/<slug>/entity.json. - Activities — scheduled or observed "things that happen" (meetings, deadlines, anticipated events). Per-facet JSONL at
journal/facets/<facet>/activities/<day>.jsonl. Sources:anticipated(fromtalent/schedule.md),user(manual),cogitate(talent-inferred). - Indexer — reads journal state, builds SQLite + FTS5 index. Never mutates source data (§7 L6). Rerunning on unchanged data is a no-op.
- Supervisor — top-level process manager. Starts/restarts services, talks to callosum.
sol supervisor/sol start.
4. The sol CLI#
Two surfaces:
sol <command>— top-level commands registered inthink/sol_cli.py'sCOMMANDSdict (e.g.,sol import,sol think,sol indexer,sol supervisor,sol heartbeat).ALIASESprovides a couple of shorthand compound commands (sol start→sol supervisor,sol up/down→sol service up/down).sol call <app> <verb>— routes tothink/call.py, which discovers eachapps/*/call.pyTyper sub-app and mounts it as a subcommand. Example:sol call entities list,sol call activities create,sol call journal search.
Adding a top-level command: add an entry to COMMANDS in think/sol_cli.py; ensure the module has a main() function.
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.
sol call journal export is the CLI entry for portable journal ZIPs; read-only archive validation lives in think/importers/journal_archive.py.
Run sol (no args) for live status plus the full grouped command list.
5. Make commands#
Verified against Makefile. Grouped by use.
Install#
| Target | When to use |
|---|---|
make install |
First setup and whenever pyproject.toml or uv.lock changes. Creates .venv/, syncs deps, runs make skills. |
make skills |
After adding or renaming a SKILL.md under talent/ or apps/*/talent/. Rewrites the .claude/ + .agents/ skill symlinks into journal/. (make install depends on this; rarely run alone.) |
make update |
Upgrade all deps to latest, regenerate uv.lock. Expect test churn. |
make update-prices |
Refresh genai-prices model-cost data when adding a new provider model or when pricing tests fail. |
make clean |
Remove build artifacts, caches, and the skill symlinks. Does not touch .venv/. |
make clean-install |
Nuke .venv/ and .installed, then reinstall. Recovery path when the venv is wedged. |
Run the stack#
| Target | When to use |
|---|---|
make dev |
Start the full stack (supervisor + callosum + sense + cortex + convey) against tests/fixtures/journal/, no observers, no daily processing. Primary inner-loop for UI work. Ctrl-C to stop. |
make sandbox |
Ephemeral background sandbox: copies fixtures to a temp journal, starts supervisor in the background, waits for readiness, writes .sandbox.pid / .sandbox.journal. Pair with verify targets below. Always follow with make sandbox-stop. |
make sandbox-stop |
Terminate the backgrounded sandbox and clean up state files. |
Format, lint, test#
| Target | When to use |
|---|---|
make format |
Auto-fix formatting and imports with ruff. Safe to run anytime; modifies files. |
make format-check |
Format dry-run. Part of make ci; rarely run alone. |
make test |
Unit tests (tests/). Format-check runs first; failures block tests. Fast inner loop. |
make test-apps |
Run all apps/*/tests/ suites. |
make test-app APP=<name> |
Run a single app's tests. |
make test-only TEST=<path-or-pattern> |
Run a specific test file or pytest node id (TEST="-k test_name" also works). |
make test-integration |
Full integration suite. Requires .env API keys. Slow; run before shipping AI-behavior changes. |
make test-integration-only TEST=<path> |
Single integration test by path or pattern. |
make test-all |
Everything — core + apps + integration. Pre-ship gate. |
make coverage |
HTML coverage report under htmlcov/. Occasional. |
make watch |
pytest-watch — reruns tests on file change. Useful during a test-heavy sprint. |
make ci |
Format-check + ruff + layer-hygiene + tests. Run before every commit. |
make verify |
Same steps as make ci. Either name is fine. |
make install-checks |
The pre-test half of make ci (format-check + ruff + layer-hygiene). Called by ci / verify. |
make check-layer-hygiene |
Run scripts/check_layer_hygiene.py alone. Useful when iterating on an L1–L2 violation flagged by CI. |
Verification against a running sandbox#
| Target | When to use |
|---|---|
make verify-api |
Start a sandbox, run tests/verify_api.py against its convey port, stop the sandbox. API-regression check. |
make update-api-baselines |
Same, but update the baseline fixtures instead of failing on diff. Run after intentional API changes. |
make verify-browser |
Start a sandbox, run tests/verify_browser.py (pinchtab-driven browser scenarios), stop the sandbox. UI-regression check. |
make update-browser-baselines |
Browser-baselines equivalent of update-api-baselines. |
make review |
Full product verification: sandbox + API verify + browser verify, in one command. Pre-ship gate for anything user-visible. Requires pinchtab. |
make install-pinchtab |
One-time install of the pinchtab browser driver used by make review / make verify-browser. |
Service management (systemd / launchd)#
.venv/bin/sol setup is the source-checkout runtime install path after make install; it installs or refreshes the source-checkout wrapper, installs the Claude Code skill when Claude is configured, and starts the background service on port 5015 by default. After the first run, the wrapper at ~/.local/bin/sol lets you use sol setup from anywhere. Use sol service <install|start|stop|restart|status|logs> for manual service operations.
| Target | When to use |
|---|---|
make service-logs |
Tail the installed service's logs. |
Other#
| Target | When to use |
|---|---|
make pre-commit |
Install pre-commit hooks (optional; most coders rely on make ci directly). |
make versions |
Print versions of Python, uv, and key deps. Diagnostic. |
Don't use#
| Target | Why not |
|---|---|
make uninstall |
Disabled by design. Use sol service uninstall, sol skills uninstall, and python -m think.install_guard uninstall for installed user artifacts, or make clean-install to rebuild the local dev env. |
6. Testing quickstart#
- Framework: pytest. Files
test_*.py, functionstest_*. Shared fixtures intests/conftest.py. - Fixture journal:
tests/fixtures/journal/— a complete mock journal with facets, entities, segments, index state. The autouseset_test_journal_pathfixture intests/conftest.pysetsSOLSTONE_JOURNALto this path for unit tests. Individual tests may override it withmonkeypatch.setenvwhen they need an isolated tmp journal (see §8). - Run one test:
make test-only TEST=tests/test_utils.py::test_fooorTEST="-k test_foo". - Run app tests:
make test-appsormake test-app APP=<name>. - Integration tests (
tests/integration/): hit real provider APIs, require.envkeys, run viamake test-integration. - After editing
convey/orapps/:sol restart-conveyto reload code in a running stack. make dev+make sandboxboth write runtime artifacts into the fixtures journal;tests/fixtures/journal/.gitignorecovers those — never commit them.
Full depth: docs/testing.md.
7. Layer hygiene — required reading (L1–L9)#
Why this lives here. A codebase-wide audit in April 2026 found 14 layer-hygiene violations in think/ and apps/. Infrastructure modules (indexer, importers, schedulers) were silently writing domain state; CLI read-verbs were mutating; get-prefixed functions were creating records on miss. These invariants encode the rules the audit distilled, so the same landmines don't get re-planted. They're inlined here because a one-click-away invariant is a routinely-skipped invariant.
The low-bar grep enforcement is scripts/check_layer_hygiene.py, wired into make ci. Known audit-flagged files are allowlisted with audit-reference TODOs; the allowlist shrinks as remediation bundles ship.
L1 — Layer boundaries are load-bearing#
Each module family has a declared responsibility. Infrastructure modules (indexer, importer, scheduler, search, graph, stats) may write only their own output artifacts. They may not create, modify, or delete domain state (entities, facets, observations, activities, events, chronicle day content). If an infrastructure module needs to trigger a domain mutation, it emits a callosum event or invokes a sol call <domain> <verb> subprocess — never writes domain state directly.
L2 — Domain write ownership#
Each domain has exactly one write-owning module (or one tightly-scoped family of modules). No other module may call atomic_write, json.dump, open("w"), Path.write_text, unlink, rmtree, etc. on that domain's on-disk state.
| Domain | Write-owning module(s) |
|---|---|
Entities (entities/*/entity.json, entities/*/*.npz) |
think/entities/journal.py + think/entities/consolidation.py + think/entities/saving.py + think/entities/merge.py + apps/entities/call.py |
Facets (facets/*/facet.json, facets/*/relationships/) |
think/facets.py + apps/facets/* (if/when created) |
Observations (observations.jsonl) |
think/entities/observations.py |
Activities (facets/*/activities/*.jsonl) |
think/activities.py |
Chronicle day content (chronicle/YYYYMMDD/**) |
The capturing module (observer, importer) per its declared outputs |
Index (SQLite, indexer/*) |
think/indexer/* |
If you're about to write to a domain from a module not in this table, stop and route through the owner.
L3 — Naming is a contract#
Function and CLI-subcommand verbs signal read vs. write intent.
Read verbs (functions and CLI subcommands): load_*, get_*, read_*, scan_*, list_*, show_*, find_*, match_*, resolve_*, query_*, lookup_*, status_*, check_*, validate_*, discover_*, format_*, render_*, extract_*, parse_*, view_*, inspect_*, info_*, describe_*, search_*.
A read-verb function must not mutate on-disk state. No exceptions for caches. No exceptions for "create on miss."
If a function needs create-on-miss semantics, split it:
entity = load_entity(eid) or create_entity(eid, ...)
This makes the write visible at every call site.
Write verbs are the ones allowed to write — choose the right one: save_, create_, add_, insert_, append_, attach_, delete_, remove_, update_, rename_, move_, promote_, merge_, seed_, consolidate_, bootstrap_, backfill_, dispatch_, record_, ingest_, import_, rebuild_.
L4 — CLI read-verbs are read-only#
CLI subcommands with read verbs (list, show, status, get, search, find, check, validate, discover, inspect, info, describe, read, view) must not write to journal domain state under any flag combination. If a command needs a write path, split it into two commands — a read-verb reader and a write-verb writer.
L5 — Write-verb defaults#
CLI subcommands with write verbs default to safe.
- Preferred: no default mutation; an explicit
--commit(or--apply) flag is required to perform the write. - Acceptable alternative:
--dry-rundefaulting toFalseonly if the subcommand name is unambiguously a write verb AND the command's primary user journey is the write (e.g.,sol call entities create).
"Bootstrap", "backfill", and "resolve-names" are not unambiguous — default them to dry-run.
L6 — Indexers never mutate source data#
An indexer's job is to build indexes from source-of-truth data. Indexers may not mutate the source data they read. Re-running sol indexer --rescan on an unchanged journal must be a no-op for domain state.
L7 — Importers only write to imports/#
Importers write source material to imports/ and the raw-content areas of chronicle/. They may not create or modify entities, facets, observations, or other cross-cutting state. If an importer needs to create an entity for deduplication, it calls a domain-owned seed_entity() function in think/entities/ that surfaces the write explicitly.
L8 — Hooks have declared outputs#
Post-processing hooks (think/hooks.py, talent/*.py hook functions) declare every path they will write in their frontmatter. The hook runner validates that all actual writes match the declaration. Writes outside the declared set fail loudly — raise at runtime; assert in tests.
L9 — Event handlers are idempotent#
Any function that handles a callosum event, a scheduled tick, or a supervisor-started automation is idempotent w.r.t. on-disk state. Append-only history records dedupe by a natural key (usually (day, segment) or (day, segment, ts)). Before adding a write to an event handler, ask: "what happens if this event fires twice?"
8. Coding invariants#
The rules above govern where code lives. The rules below govern how code behaves. They exist because we got burned.
-
No backwards-compatibility shims. All code that depends on this project lives in this repository — never add fallback aliases, re-exports for moved symbols, deprecated-parameter handling, or legacy support code. When renaming or removing something, update every usage directly. For journal data-format changes, write a migration script (see
docs/APPS.mdformaintcommands); do not add a compatibility layer. Cogitate agents default to adding shims; resist this. -
Trust
get_journal()unconditionally.get_journal()fromthink.utilsis the single source of truth for journal path resolution. The managed wrapper at~/.local/bin/solsetsSOLSTONE_JOURNALfor installed runs; tests use the autouse fixture; Makefile sandboxes set it explicitly. Application code, agent prompts, subprocess environments, and service files must not setSOLSTONE_JOURNALthemselves. To rewrite the wrapper's embedded path usesol config journal <path>. Seedocs/environment.md. -
SPDX header on every source file. All Python (and other source) files begin with:
# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc(
//for JavaScript.) Markdown, text, and prompt files don't need it. -
Fail loudly, not silently. Raise specific exceptions with clear messages; use the
loggingmodule, notprint. Validate inputs at module boundaries. A silent swallow in production costs days of forensics — an error at the boundary is free. -
Trust internal code. Don't add defensive validation for things internal callers can't violate. Validate at system boundaries (user input, external APIs, imported files) — not between modules you control.
Generic software principles (DRY, KISS, YAGNI, single responsibility, small focused commits) apply; see docs/coding-standards.md for the full list.
9. File headers, naming, dependencies#
- SPDX header as above — mandatory on source code files.
- Naming: modules / functions / variables
snake_case; classesPascalCase; constantsUPPER_SNAKE_CASE; private members_leading_underscore. Full table indocs/coding-standards.md. - Imports: prefer absolute (
from think.utils import get_journal), grouped stdlib → third-party → local, one per line. - Type hints on function signatures;
mypyviamake check. - Dependencies: managed by uv.
pyproject.tomlis authoritative;uv.lockis committed;make installsyncs;make updaterefreshes. - Python 3.11+.
10. Commit hygiene#
- Small, focused commits with descriptive messages.
- Run
make cibefore every commit. - Run
gitcommands directly — notgit -C— you're already in the repo. - Don't commit runtime artifacts written under
tests/fixtures/journal/bymake dev/make sandbox(.gitignorecovers them; verify withgit statusanyway).
11. Where to go deeper#
Bare links don't motivate clicking. Each entry below says when you actually need the doc.
| Doc | When to read |
|---|---|
docs/APPS.md |
Required before modifying apps/ — pattern catalog for Convey apps, hook-idempotency guidance, Typer sub-app conventions, maint commands for data migrations |
docs/THINK.md |
Understanding the think-layer pipeline (importers, indexer, segment/stream processing) |
docs/CORTEX.md |
Modifying talent execution, cortex lifecycle, talent process management |
docs/CALLOSUM.md |
Adding a new tract/event, debugging message flow |
docs/CONVEY.md |
Framework-level web changes (as opposed to an individual app) |
docs/OBSERVE.md |
Capture-side work: new modalities, transcription, sensing |
docs/SOLCLI.md |
Adding a new sol <cmd> or sol call <app> <verb> |
docs/PROMPT_TEMPLATES.md |
Modifying talent prompt format or frontmatter |
docs/PROVIDERS.md |
Adding a new AI provider; debugging model selection |
docs/testing.md |
Writing integration tests; setting up fixtures; debugging test isolation |
docs/environment.md |
Journal path resolution, managed-wrapper behavior, service install details, and SOLSTONE_JOURNAL rules |
docs/coding-standards.md |
Full naming conventions, ruff / mypy config, dep-management details — reference for everything not promoted into this file |
docs/project-structure.md |
Canonical directory layout; resolving "where does this file go" debates |
docs/DOCTOR.md |
Diagnostics and debugging a running system |
docs/SCREEN_CATEGORIES.md |
Screen-understanding classifier taxonomy (observe side) |
docs/INTEGRATION_TESTS.md |
Deep integration-test setup |
docs/VENDOR.md |
Vendor-level integrations |
docs/design/ |
Per-subsystem design docs |
docs/JOURNAL.md |
Breadcrumb only — redirects to talent/journal/SKILL.md, the progressive-disclosure journal-layout reference |
talent/journal/SKILL.md |
Journal layout, vocabulary, and sol call journal CLI (loaded by cogitate talents on demand via skills) |
talent/journal/references/cli.md |
Full sol call journal reference, including Talent CLI Boundaries (which infrastructure commands cogitate talents must not call) |
The live journal also carries journal/AGENTS.md as its runtime-facing breadcrumb.
docs/BACKLOG.md and docs/ROADMAP.md are product-planning docs — CPO/CEO reading, not coder reading.
12. What this file is NOT#
- Not a runtime guide for cogitate talents. Runtime CLI restrictions on talents live in
talent/journal/references/cli.md§ Talent CLI Boundaries. If you're tuning what a talent can or cannot call, look there, not here. - Not the journal-layout reference.
talent/journal/SKILL.md+ itsreferences/is the cogitate-audience entry point. This file describes how those commands are implemented, not which ones talents can't call. - Not an operations manual. For debugging a live system see
docs/DOCTOR.md; for setup and service lifecycle, seedocs/INSTALL.md,sol setup, andsol service.
13. Owner-facing copy: the system-anatomy canon#
- The trinity. In owner-facing copy, name the system in canonical order:
solstone = observers + sol agent + journal. - The canon lives elsewhere. The source of truth is sol pbc's internal brand canon (system anatomy + voice terminology guides). This repo's branded prose follows it; the canon itself is not vendored here.
- Ban surveillance verbs in branded surfaces. Never use "capture", "watch", "record", "monitor", "track", or "collect" in template copy, settings labels, error messages, onboarding text, or README / INSTALL prose. Prefer "observe alongside", "experience along with", or "take in what you take in".
captureis code-only. Keep it in module names such asobserve/, function names, OS subsystem identifiers such ascom.solstone.capture, and internal architecture diagrams. That is intentional and aligned with the canon.- Name artifacts for owners, not pipelines. In branded prose, say "raw media", "the originals", or "observations". Never say "raw captures" or "screen captures" in owner-facing strings. Code-side artifact names stay as-is.
solis one thing.solis the running software; there is no homunculus behind it. Use two registers for one entity:solin conversation,sol agentin technical contexts.keeperis a surface-specific edge case.voice-terminology.mdmakeskeeperthe role noun forsolin product copy generally. Thesolstone-swiftsurface banskeeperbecause the mobile UX uses the owner's chosen identity, defaultsol. When writing copy for a specific surface, follow that surface's terminology covenant.- Edit with the right mental model. Internal architecture vocabulary in this repo stays as-is:
observe/, the capture pipeline, and screen capture log subsystems remain correct code language. Apply the canon to owner-facing strings only: UI copy, settings text, install / README prose, error messages, and onboarding. If an owner sees it, follow the canon; if it's code or internal docs about pipelines,captureis fine.
| Surface | Terminology rule |
|---|---|
| Code surfaces | capture is fine in code, module names, function names, subsystem ids, and internal architecture docs. |
| Branded surfaces | capture is banned. Use owner-facing phrasing such as "observe alongside", "experience along with", "take in what you take in", "raw media", "the originals", or "observations". |
Canon source of truth: sol pbc's internal brand canon (system-anatomy guide).