personal memory agent
solstone AGENTS.md
50 kB · 531 lines
Markdown
at main

solstone Developer Guide #

This file is the developer guide for the solstone repository. Read it before writing code.

Audience:

  • Coders (cwd = repo root, editing solstone/observe/, solstone/think/, solstone/convey/, solstone/apps/, solstone/talent/, tests/) — you're in the right place.
  • Cogitate talents (cwd = journal/, running inside the live system) — your journal-side entry is solstone/talent/journal/SKILL.md, installed into journal/.claude/skills/journal/ and journal/.agents/skills/journal/ alongside the sol router skill. The runtime contract you operate under — tools, reads vs writes, finalization, access tiers, and what is not in your context — is docs/COGITATE.md.
  • 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:

  1. This file through §8 — the invariants must be in working memory before your first edit.
  2. docs/SOLCLI.md — the CLI routing map. Public sol / solstone commands are native Rust authority entries; solstone/think/sol_cli.py is now the journal host dispatcher.
  3. solstone/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.
  4. The area you're about to touch:
    • User-visible feature or sol call <app> <verb> → solstone/apps/<name>/native/{authority.toml,command.rs} + solstone/apps/<name>/routes.py + solstone/apps/<name>/templates/.
    • Think pipeline → solstone/think/<module>.py + its tests.
    • AI talent prompt or behavior → solstone/talent/<name>.md (+ optional .py post-hook).
    • Capture / observe → solstone/observe/<module>.py.
  5. Run sol (no args) — prints current journal status + grouped command list. Orients you to live state.
  6. make dev or make sandbox when 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
solstone/think/sol_cli.py Journal-host CLI dispatcher — service/universal COMMANDS and ALIASES for the journal script adding a journal <cmd> or host-only Python command docs/SOLCLI.md
solstone/observe/ Multimodal capture — screen, audio, transcribe, describe, sense, transfer capture-side bugs, new input modalities docs/OBSERVE.md
solstone/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/COGITATE.md, docs/CALLOSUM.md
solstone/convey/ Web app framework — app discovery, routing, bridge layout / framework-level UI changes docs/CONVEY.md
solstone/apps/ Convey apps — each self-contained (native/ authority + Rust command, routes.py, templates/) adding a user-facing feature, a sol call <app> verb, a UI surface docs/APPS.md (required reading before modifying solstone/apps/)
solstone/talent/ AI talent configs (markdown prompts + optional .py post-hooks) + installed router skills (sol, journal); app fragments feed generated router references defining or tuning a talent; updating router guidance solstone/talent/journal/SKILL.md, docs/PROMPT_TEMPLATES.md
core/ Rust wave-0 workspace — thin solstone-core bin plus library-first adapter crates Rust scaffold, gates, or Python→Rust porting doctrine docs/PORTING.md
scripts/ Repo maintenance scripts — check_layer_hygiene.py tooling that guards the codebase; wired into make install-checks, not reached by the frozen make ci channel adapters: docs/CHANNEL_ADAPTERS.md
tests/ Pytest suites + tests/fixtures/journal/ mock journal writing tests; debugging flakiness; make dev / make sandbox use fixtures as the journal docs/testing.md
tests/js/ JavaScript harnesses driven by Python node tests testing browser scripts without a real browser 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 solstone/think/, solstone/apps/, or solstone/talent/, not journal data solstone/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 solstone/think/ usually ripples outward.

Key concepts, priority-ordered:

  • Journal — the on-disk record rooted at journal/ in the repo. Every day is a journal/chronicle/YYYYMMDD/ directory. Segments (timestamped capture windows) are anchored to creation/modification time, not content "about" time. get_journal() from solstone.think.utils is the single source of truth for journal path resolution; trust it unconditionally. Source-checkout installs inherit SOLSTONE_JOURNAL from the managed bash wrapper at ~/.local/bin/sol; packaged installs (uv tool install solstone or pipx install solstone) install sol directly at ~/.local/bin/sol and rely on get_journal() to resolve the default journal location; 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 solstone/talent/<name>.md with 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"), spawns python -m solstone.think.talents subprocesses, writes <talent>/<ts>_active.jsonl then renames to <talent>/<ts>.jsonl on completion, broadcasts all events back through callosum. Read docs/CORTEX.md before modifying talent execution.
  • Facets — project/context scopes (work, personal, …). Group related entities, activities, and relationships. Facet data lives under journal/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 (from solstone/talent/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. journal supervisor / journal start.

4. The sol CLI #

Two surfaces:

  • sol <command> — native access commands declared under solstone/think/native/<command>/authority.toml and implemented by the matching Rust command.rs (e.g., sol import, sol chat).
  • journal <command> — host/service commands from solstone/think/sol_cli.py (e.g., journal think, journal supervisor, journal heartbeat). ALIASES provides shorthand compound commands (journal start → journal supervisor, journal up/down → journal service up/down). doctor is universal: sol doctor checks CLI usability; journal doctor checks journal-host health.
  • sol call <app> <verb> — native app commands declared under solstone/apps/<app>/native/ and generated into the native aggregate inventory. The only remaining Python subtree is the finite private compatibility path for sol call journal; see docs/PORTING.md for the inventory and removal criterion.

Adding a top-level sol command: add a native authority under solstone/think/native/<command>/ and wire the Rust handler into the generated native inventory path. Use solstone/think/native/chat/ and solstone/think/native/import/ as the current patterns.

Adding a sol call sub-verb: update solstone/apps/<app>/native/authority.toml, implement the handler in command.rs, and regenerate the native inventory. sol call journal export is the CLI entry for portable journal ZIPs; read-only archive validation lives in solstone/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 speakers-analyze-helper Reinstall the published speakers-analyze helper into .venv after a source-checkout uv sync prunes it. Normally run by make install; useful after manual syncs.
make skills Regenerate generated router references, then rewrite the sol + journal router 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 / make all / make build Build the native Rust workspace, excluding the Python-provisioned ONNX crate pair during the conversion freeze.
make format Format the Rust workspace with Cargo fmt; modifies Rust source.
make format-check Cargo fmt dry-run (cargo fmt --all -- --check); one of the Rust-only CI checks.
make test Alias for make check-rust-test: Rust workspace tests only, excluding the ONNX crate pair from host coverage.
make test-cov Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make test-integration Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make test-release Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make release-checks Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make test-performance Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make test-app APP=<name> Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make test-only TEST=<path-or-pattern> Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make coverage Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make watch Frozen for the Rust-conversion effort; fails immediately. See docs/PORTING.md#rust-conversion-freeze.
make ci Rust-only gate: check-rust-fmt, MSRV, clippy, Rust tests, iOS canary, and Rust dependency policy.
make verify Alias for make ci during the Rust-conversion freeze.
make install-checks Directly runnable full Python-and-Rust preflight chain (format, ruff, layer hygiene, and related checks); no longer called by ci or verify.
make check-layer-hygiene Run scripts/check_layer_hygiene.py alone. Useful when iterating on an L1–L2 violation flagged by make install-checks.

During the Rust-conversion freeze, use the narrowest applicable make check-rust-* target, then make ci as the settled Rust gate. The focused Python Make targets are frozen; run the Python suite directly when it is needed. Do not rerun an unchanged failure merely to seek green.

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.

Service management (systemd / launchd) #

.venv/bin/journal setup is the source-checkout runtime install path after make install; it installs or refreshes the source-checkout wrappers, installs the Claude Code skill when Claude is configured, and starts the background service on port 5015 by default. After the first run, the wrappers at ~/.local/bin/sol and ~/.local/bin/journal let you use sol and journal from anywhere. Use journal 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).
make versions Print versions of Python, uv, and key deps. Diagnostic.

Release rail #

Rust-conversion freeze: this rail's documented entry points are frozen: scripts/release.sh refuses unconditionally in every mode, and the Make release targets fail immediately. See docs/PORTING.md#rust-conversion-freeze.

The rest of this section documents the retained release implementation for post-freeze restoration. These commands do not reach that implementation while the freeze is active.

DESTRUCTIVE: bash scripts/release.sh --candidate is fresh construction; before policy or build work it first runs make release-checks, then deletes prior raw build/dist outputs and that version's stale payload/evidence. It verifies the expected source commit and lock state, gathers target evidence through configured build/proof-host channels, pair-promotes payload and evidence, and prints canonical local readiness JSON. This is candidate evidence only, not publication authorization. Advisory acquisition is a separate operator operation documented in scripts/release_advisory_policy.py. If retained dist/release-candidate/<version> or target/release-evidence/<version> bytes already exist, --candidate refuses before cleanup unless the operator sets RELEASE_CANDIDATE_DISCARD_RETAINED=<version> for an unpublished version or RELEASE_CANDIDATE_DISCARD_PUBLISHED_TAG=<version>+<tag> for a tagged version; undeterminable retained state is not authorizable.

Every candidate attempt generates a fresh proof challenge and requires, for each configured native target, a challenge-bound nvattest receipt alongside the unchanged install/smoke receipt. Before the ledger is written and before any proof host is contacted, the rail extracts the nvattest authority from the candidate's own root wheels, requires every target's extraction to agree byte-for-byte, and materializes the locked support-wheel set declared by uv.lock, verifying each wheel's bytes against the lock. Challenge, authority, and that exact support set are then handed to every target lane. Two attempts over identical version, source, and payload inputs differ only in the challenge, the receipts bound to it, and observation timestamps.

The retained support declarations recorded in the ledger are the lock-derived declarations captured when the candidate was cut. Disagreement with the release lock therefore fails at cut time rather than being re-litigated later, which is why a retained candidate keeps validating after an unrelated dependency bump. That durability is specific to dependency and lock drift. It does not extend to the retained ledger's own shape: changing a registered top-level or nested ledger key set breaks candidates that were already cut. The retained-ledger schema registry and frozen fixture tests make that visible; read docs/release-evidence-contract.md before changing the registry, and publish any outstanding retained candidate before merging such a change.

bash scripts/release.sh --recover <version> <source-commit> is retained-byte-only, read-only validation. It preserves retained payload and evidence, revalidates them, and reports retained-candidate-valid for v2 evidence or retained-pre-nvattest-candidate-valid for registered pre-nvattest v1 evidence; publication requires v2. It never rebuilds, refreshes advisories, contacts hosts, installs wheels, reads authentication, or uses the network.

For v2 evidence, recovery re-derives the nvattest authority from the retained candidate wheel bytes rather than reading it from the ledger, requires every target's extraction to agree, and holds that extraction against the ledger's authority payload, its byte digest, and every receipt's installed authority. It re-hashes the retained support wheels against the ledger declarations and against every receipt, and binds the challenge and the candidate identities through both receipt classes. Because the anchor is the retained wheel bytes, a forgery that rewrites the ledger and every receipt to agree with itself is still rejected. Recovery reads no release lock, no clock, and no network, and writes nothing on either the success or the failure path.

bash scripts/release.sh --dry-run-linux validates the Linux structural plan only. It emits no ready payload, manifest, ledger, proof, or clean-source claim.

Candidate readiness means candidate payload, ledger, retained support wheels, and both per-target receipt classes — install/smoke and nvattest — are locally consistent for the retained bytes. Proofs do not authorize publication and do not prove external distribution. Live SPP composite acceptance, real remote lanes, and production URL reach are VPE post-ship work; this rail does not attest them.

Aggregate release publication is the retryable delivery step for an already finalized retained candidate. make publish-release RELEASE_DIR=<retained ready dir> runs scripts/release_publish.py --mode production: it revalidates retained candidate bytes, uploads the canonical PyPI artifact set, verifies PyPI digests, tags v<version> at the retained source commit, and records a GitHub Release witness. The independently versioned solstone-journal-models wheel and sdist may be reused when the package index already holds that ledger-declared version and its canonical archive manifests match the retained archives; reused model archives are removed from twine upload and train-owned exact-digest verification, then the model index is rechecked before tag/witness. The GitHub Release witness still receives the full retained asset list. make publish-release-test RELEASE_DIR=<retained ready dir> is TestPyPI upload+verify only; it does not validate changelog or tag readiness and does not invoke git or gh.

Publication additionally refuses when the retained candidate's nvattest authority does not bind to the current checkout's canonical authority. That prerequisite runs immediately after retained-candidate revalidation and before the mode branch, so production and test alike fail with no index, upload, tag, or witness call attempted. The repair is to publish from a checkout whose authority matches the retained candidate, or to cut a new candidate; the retained bytes are never rewritten to satisfy the check.

The no-arg release path, --test, make release, and make release-test stay locked out. They fail before any token, transport, build-host, git, upload, tag, or hosted-release seam is reached. Do not add publication behavior to this candidate rail.

Transparency publication is the separate retryable evidence step after delivery; it never gates delivery. It is soft-gated while inactive: set TRANSPARENCY_ACTIVATED=1 (or make TRANSPARENCY_ACTIVATED=1 <target>) to reach the real implementation; see docs/PORTING.md#rust-conversion-freeze. make publish-transparency RELEASE_DIR=<retained ready dir> is env-driven: operator endpoints, bucket, credentials, minisign key paths, and archive channel come from TRANSPARENCY_* env vars, while the public base defaults to https://transparency.solstone.app. RELEASE_DIR must resolve to dist/release-candidate/<version>/; the corresponding rail evidence is derived from target/release-evidence/<version>/. The evidence directory is inventory-exact within its retained ledger schema version: a missing entry and an unexpected extra both fail closed. Current v2 evidence holds ledger.json, one install/smoke receipt per configured native target under proofs/, one challenge-bound nvattest receipt per the same targets under nvattest/, and the retained locked support wheels under support/; registered pre-nvattest v1 evidence holds only ledger.json and the install/smoke receipts under proofs/. After the prerequisite is published, the single post-finalization tombstone verification record is the only optional extra allowed for either schema version.

The public layout is fixed: releases/<product>/v/<version>/ledger-entry.json, ledger-entry.json.minisig, the byte-identical companion manifests, and the byte-identical native proof receipts of both classes are immutable create-only objects. Install/smoke receipts publish under their target name; nvattest receipts publish under <target>.nvattest.json, mapped from their retained sibling directory at a single point in the publisher. releases/<product>/ledger.jsonl is mutable and derived. releases/<product>/latest.json.minisig and latest.json are the signed pointer pair, written in that order. The archive channel retains the full candidate artifact bytes; the public surface carries evidence only.

Transparency attests what was released, that the public version history is immutable, and that history is publicly reconstructible; it does not attest that binaries provably match source. A version key is one-shot and permanent. If a version entry is uploaded but the pointer is not updated, rerun the publisher to reuse the staged bytes and finish the mutable pointer. If a different object is locked at a version key, the recovery is cutting the next version; a locked-zone object can never be replaced. Latest-pointer writes deliberately use an immediately re-fetched ETag as If-Match, even though fetch-time pointer ETag presence is also asserted.

make resign-transparency-pointer refreshes only the signed latest pointer's signed_at and valid_until; it does not require RELEASE_DIR and does not change chain_length, tip_sha256, or version.

The in-repo append-only head witness is transparency-head-log.jsonl. It is JSONL with no header row and is committed before the first publish. The public minisign trust-anchor filename is solpbc-transparency-1.pub, served at releases/keys/solpbc-transparency-1.pub; rotation increments the numeric suffix and uses cross-signed successor files. TRANSPARENCY_MINISIGN_PUB sets only the local verification path and may use any local filename.

Don't use #

Target Why not
make uninstall Disabled by design. Use journal service uninstall, sol skills uninstall, and python -m solstone.think.install_guard uninstall for installed user artifacts, or make clean-install to rebuild the local dev env.

6. Testing quickstart #

  • Rust gate: make / make all, make ci, make test, make verify, and make build operate only on the native core/ Cargo workspace during the Rust-conversion freeze. make ci runs formatting, MSRV, clippy, Rust tests, the iOS canary, and Rust dependency policy.
  • Python suite: pytest files remain test_*.py with test_* functions, shared fixtures in tests/conftest.py, and the fixture journal at tests/fixtures/journal/. The autouse set_test_journal_path fixture is unchanged; tests that write, scan, or rebuild journal/index state must use journal_copy or a smaller tmp_path journal (see §8). Run this suite directly with bare pytest when needed. tests/ and solstone/apps/*/tests/ are unchanged, but the former Python Make rails, including make test-app, make test-only, and the other make test-* targets, now fail with the freeze diagnostic.
  • Marked Python tests: integration, performance, and release tests remain in the suite and can be selected with bare pytest as needed; their former Make rails are frozen. Live product verification still uses make sandbox.
  • After editing solstone/convey/ or solstone/apps/: journal restart-convey to reload code in a running stack.
  • Runtime artifacts: make dev writes them into the fixtures journal, where tests/fixtures/journal/.gitignore covers them. make sandbox uses an ephemeral copy and leaves only its .sandbox.pid and .sandbox.journal state files until make sandbox-stop removes them.
  • Test invariants, not snapshots. A test asserts what must hold in every valid state of the system — not what happens to be true today. Never pin a test to hand-edited prose (CHANGELOG / README / docs), to a value the system is designed to change (a version, a date, a growing count), or to a transient state. The tell: if doing the correct next thing — cut a release, rename a label, graduate a shipped changelog entry — turns the test red, the test is wrong, not the system. And test the code that produces a fact, never the rendered text about it. (A [Unreleased]-pinned changelog test was exactly this anti-pattern — its pass condition required the release process to not run; removed 2026-05-30.)

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 solstone/think/ and solstone/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 install-checks and not reached by the frozen 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) solstone/think/entities/journal.py + solstone/think/entities/relationships.py + solstone/think/entities/saving.py + solstone/think/entities/merge.py + solstone/think/entities/voiceprints.py + solstone/apps/speakers/owner.py + solstone/apps/speakers/routes.py
Entity history content (entities/*/history/{events,prepared,private}/**) solstone/think/entities/history.py is the sole writer of history events, prepared staging, and private merge payloads. Whole-entity deletion by entity owners removes history/ only as part of removing entities/<id>/.
Owner voice candidate (awareness/owner_candidate.npz) solstone/apps/speakers/owner.py
Speaker discovery clusters (awareness/discovery_clusters.json, awareness/discovery_clusters.resolved.json) solstone/apps/speakers/discovery.py
Speaker candidate pool (awareness/speaker_candidates.json) solstone/apps/speakers/candidate_tracker.py
Speaker identify operation ledger (speakers/identify-operations.jsonl) solstone/think/speaker_identify_operations.py
Entity resolution ambiguities (entities/ambiguities.jsonl) solstone/think/entities/ambiguities.py
Entity merge candidates (entities/review-candidates.jsonl) solstone/think/entities/review_candidates.py
Facet review candidates (facets/review-candidates.jsonl) solstone/think/facet_review_candidates.py
Speaker review candidates (speakers/review-candidates.jsonl) solstone/think/speaker_review_candidates.py
Speaker candidate-pair review candidates (speakers/candidate-pair-review-candidates.jsonl) solstone/think/speaker_candidate_pair_review_candidates.py
Speaker discovery cluster dismissals (speakers/cluster-dismissals.jsonl) solstone/think/speaker_cluster_dismissals.py
Speaker keep-separate assertions (speakers/keep-separate.jsonl) solstone/think/speaker_keep_separate.py
Facets (facets/*/facet.json, facets/*/relationships/) solstone/think/facets.py + solstone/apps/facets/* (if/when created)
Observations (observations.jsonl) solstone/think/entities/observations.py
Activities (facets/*/activities/*.jsonl) solstone/think/activities.py
Timeline (chronicle/<day>/timeline.json, chronicle/**/<seg>/timeline.json, root timeline.json) solstone/apps/timeline/maintenance.py + solstone/apps/timeline/talent/segment_summary.py
Per-segment sense outputs (chronicle/**/<seg>/talents/{sense.json,facets.json,speakers.json,density.json,change.json,activity.md,sense.md}) solstone/think/sense_splitter.py
_solstone_processing records on header-only native describe/transcribe outputs (chronicle/**/<seg>/{screen,*_screen,audio,*_audio}.jsonl) Primary, automatic: sense re-entry via should_reenter_analysis_output in solstone/observe/processing_record.py — a record-less screen output is re-attempted and the handler determines the verdict. Operator bulk tool: solstone/think/backfill_processing_records.py, which stamps a guessed state=empty and is CLI-only, for marker-less, chunk-less legacy fleets; it declines anything carrying a marker or an existing record (SKIP_MARKER / SKIP_HAS_RECORD, unchanged).
Awareness (awareness/current.json, awareness/YYYYMMDD.jsonl) solstone/think/awareness.py
Awareness activity state (awareness/activity_state.json) solstone/think/thinking.py
Identity (identity/*.md, identity/history.jsonl audit log) solstone/think/identity.py
Todos (facets/*/todos/*.jsonl) solstone/apps/todos/todo.py
Chronicle chat stream (chronicle/**/chat/<seg>/chat.jsonl) solstone/convey/chat_stream.py
Day talent-output accumulator (chronicle/<day>/talents/<name>.jsonl) solstone/think/day_accumulator.py
Talent provenance sidecars (chronicle/<day>/health/talent-provenance/**) solstone/think/talent_provenance.py
Config (config/journal.json) solstone/think/journal_config.py
Schedules (config/schedules.json) solstone/think/schedule_config.py
Push devices (config/push_devices.json) solstone/think/push/devices.py
Local inference operational telemetry (health/local-inference/YYYYMMDD.jsonl) solstone/think/providers/local_admission.py
Active-brain state (health/brain.json, health/brain-fingerprint.key, health/brain-refresh.lease) solstone/think/providers/brain_state.py
Provider install status records and proof cache (health/providers/{local,parakeet}.json, health/providers/{local,parakeet}.proof-cache.json) solstone/think/providers/install_state.py + solstone/think/providers/artifact_proof.py
Provider install leases (health/providers/{local,parakeet}.lease) solstone/think/providers/install_lease.py
Provider runtime health and retry-token records (health/providers/runtime/{local,parakeet}.json, health/providers/runtime/{local,parakeet}.retry-token.json, health/providers/runtime/{local,parakeet}.operation.lock) solstone/think/providers/runtime_health.py
Native speakers-analyze install generation (health/speakers-analyze/install-generation.json, health/speakers-analyze/install-generation.lock) solstone/think/speakers_analyze_installation.py
Provider artifact manifests (cache/providers/**/.solstone-provider-manifest.json, cache/providers/local/mlx/**/*.manifest.json) solstone/think/providers/artifact_proof.py
nvattest appraiser cache (cache/providers/nvattest/**) solstone/think/providers/nvattest_install.py
Media offload ledger (health/offload/<YYYYMMDD>.jsonl) solstone/think/offload_ledger.py
Parakeet server placement record (health/parakeet-cpp.placement) solstone/think/providers/parakeet_server.py
Hosted backup binding (backup/hosted/binding.json) solstone/think/backup/hosted.py
Convey config (config/convey.json) solstone/convey/config.py + solstone/think/facets.py
Chat config (config/chat.json) solstone/apps/chat/config.py
Speaker labels (chronicle/**/talents/speaker_labels.json) solstone/apps/speakers/attribution.py
Speaker corrections (chronicle/**/talents/speaker_corrections.json) solstone/apps/speakers/attribution.py
Stream identity (chronicle/**/<seg>/stream.json marker + streams/<name>.json state) solstone/think/streams.py
Observer ingest manifest (chronicle/**/<seg>/ingest.json) solstone/apps/observer/utils.py
Link service state (link/ca/cert.pem, link/ca/private.pem, link/ca-staging/**, link/nonces.json, link/authorized_clients.json, link/state.json including optional locked_at, link/tokens/account.json, link/totp.json) solstone/think/link/ca.py + solstone/think/link/establish.py + solstone/think/link/nonces.py + solstone/think/link/auth.py + solstone/think/link/paths.py
Chronicle day content (chronicle/YYYYMMDD/**) The capturing module (observer, importer) per its declared outputs
Index (SQLite, indexer/*) solstone/think/indexer/*
Observer registry and sync history (apps/observer/observers/*.json, apps/observer/observers/*/hist/*.jsonl) solstone/apps/observer/utils.py
Import ingest/resolve staging (imports/**) solstone/apps/import/ingest.py + solstone/apps/import/resolve.py + solstone/apps/import/facet_ingest.py + solstone/apps/import/journal_sources.py — HTTP-ingest + resolve staging state, plus the remote-ingest bundle under imports/<id>/. journal_sources.py owns only its create_state_directory imports/ initializers; its source registry is app-storage. Import-bundle and sync-cursor content under imports/<id>/ and imports/<backend>.json is written by solstone/think/importers/{utils,cli,shared,sync}.py (local/CLI import flows + sync cursor), and solstone/think/importers/plaud.py installs streamed imported audio onto imports/<id>/<name> via journal_io's install_file primitive, as importer declared outputs (L7).
Health import local state (imports/health-dedupe.sqlite, imports/_approvals/**, Apple Health imports/<id>/{raw,normalized,content_manifest.jsonl,manifest.json}) solstone/think/importers/{apple_health,health_dedupe,pre_save_gate,shared}.py — Apple Health save-mode reads the approval artifact before any write, installs raw synthetic/approved sources under the import bundle, writes normalized shards under imports/<id>/normalized/, and keeps record dedupe in imports/health-dedupe.sqlite. The pre-save gate is read-only; creation of _approvals/** remains a separately approved setup action.
Operational-log/cache pruning and root task-log compaction — deletion/compaction only, across the dated allowlist (chronicle/<day>/health/*.{log,jsonl}, top-level talents/<talent>/<epoch_ms>.jsonl run logs + talents/<YYYYMMDD>.jsonl day indexes, tokens/, health/local-inference/, awareness/, config/actions/, facets/*/logs/, apps/observer/observers/*/hist/, .cache/cogitate-history/) plus root task_log.txt epoch lines older than the same retention window solstone/think/log_retention.py — prunes derived/operational artifacts by retention age and rewrites only root task_log.txt during compaction, preserving recent and unparseable lines. Routes its own deletes/compaction; does not write through each domain's owner.

If you're about to write to a domain from a module not in this table, stop and route through the owner.

Native sol call <app> <verb> handlers keep the HTTP boundary. Each journal-data command is declared by an app-local native authority and reaches the journal through the generated native HTTP client. The positive native-sol inventory, coverage, architecture, and conformance gates enforce that boundary. The only Python sol call subtree is the finite sol call journal compatibility path documented in docs/PORTING.md.

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-run defaulting to False only 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 journal 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 solstone/think/entities/ that surfaces the write explicitly.

L8 — Hooks have declared outputs #

Post-processing hooks (solstone/think/hooks.py, solstone/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.md for maint commands); do not add a compatibility layer. Cogitate agents default to adding shims; resist this.

  • Trust get_journal() unconditionally. get_journal() from solstone.think.utils is the single source of truth for journal path resolution. For source-checkout installs, the managed bash wrappers at ~/.local/bin/sol and ~/.local/bin/journal set SOLSTONE_JOURNAL before invoking the matching venv binary; packaged installs use uv tool install / pipx install and rely on get_journal() for default-journal resolution; tests use the autouse fixture; Makefile sandboxes set it explicitly. Application code, agent prompts, subprocess environments, and service files must not set SOLSTONE_JOURNAL themselves. To rewrite the wrapper's embedded path use journal config journal <path>. See docs/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 logging module, not print. 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; classes PascalCase; constants UPPER_SNAKE_CASE; private members _leading_underscore. Full table in docs/coding-standards.md.
  • Imports: prefer absolute (from solstone.think.utils import get_journal), grouped stdlib → third-party → local, one per line.
  • Type hints on function signatures.
  • Dependencies: managed by uv. pyproject.toml is authoritative; uv.lock is committed; make install syncs; make update refreshes.
  • Python 3.12+.

10. Commit hygiene #

  • Small, focused commits with descriptive messages.
  • Validate each commit with focused checks appropriate to its diff. The full make ci gate belongs on the final tree before merge or release, not before every intermediate commit.
  • Run git commands directly — not git -C — you're already in the repo.
  • Don't commit runtime artifacts written under tests/fixtures/journal/ by make dev / make sandbox (.gitignore covers them; verify with git status anyway).

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 solstone/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/COGITATE.md The cogitate talent runtime contract — cwd/workspace, the sol-CLI-authoritative journal access, raw-read bound, access tiers, finalization, disallowed assumptions, and the in-context preamble constant. Read before authoring/editing a talent prompt.
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/PORTING.md Python→Rust porting doctrine, Rust workspace layering, and wave-0 lockstep/gate rules
docs/PROMPT_TEMPLATES.md Modifying talent prompt format or frontmatter
docs/PROVIDERS.md Three-lane provider architecture: active-brain resolution, local/BYO/confidential lanes, and honest no-fallback failure semantics
docs/testing.md Test structure, fixtures, debugging test isolation
docs/environment.md Journal path resolution, managed-wrapper behavior, service install details, and SOLSTONE_JOURNAL rules
docs/CHANNEL_ADAPTERS.md Release channel adapter config, scrub-gate expectations, and operator-safe placeholders
docs/release-evidence-contract.md Required before changing the retained release ledger schema registry — why such a change breaks already-cut candidates, what schema_version does and does not tolerate here, and the frozen-fixture rule
docs/journal-format-contract-maintenance.md Changing a committed journal at-rest format (observer ingest envelopes, stream.json, audio.jsonl, screen.jsonl) — schema floor vs producer-local requirements, and which relaxations are safe
docs/coding-standards.md Full naming conventions, ruff 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/VENDOR.md Vendor-level integrations
docs/design/ Per-subsystem design docs
docs/JOURNAL.md Breadcrumb only — redirects to solstone/talent/journal/SKILL.md, the progressive-disclosure journal-layout reference
solstone/talent/journal/SKILL.md Journal layout, vocabulary, and sol call journal CLI (loaded by cogitate talents on demand via skills)
solstone/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 — not coder reading.

12. What this file is NOT #

  • Not a runtime guide for cogitate talents. Runtime CLI restrictions on talents live in solstone/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. solstone/talent/journal/SKILL.md + its references/ 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, see INSTALL.md (owner install), CONTRIBUTING.md (developer install), journal setup, and journal service.