# 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 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 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 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` | Full unit suite — `tests/` + every `solstone/apps/*/tests/`, one parallel run. Format-check runs first; failures block tests. |
| `make test-cov` | Same suite with full-repo terminal coverage; used by `make verify`. |
| `make test-integration` | Opt-in real local build/process and persisted-index contracts. Never part of `make ci`. |
| `make test-app APP=<name>` | Run a single app's tests (focus helper). |
| `make test-only TEST=<path-or-pattern>` | Run a specific test file or pytest node id (`TEST="-k test_name"` also works). |
| `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` | Install checks plus the full unit suite. Canonical final-tree gate before merge or release. |
| `make verify` | Install checks plus the coverage suite. Use when coverage is specifically required. |
| `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. |

During development, use the narrowest checks that prove the changed behavior:
`make test-only`, `make test-app`, and specific `make check-*` targets. Run
`make ci` once on the settled final tree before merge or release. Run it
earlier only for changes to CI/test infrastructure, shared fixtures, packaging
or dependencies, broad cross-cutting contracts, or when a clean baseline is
necessary to diagnose the task. 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

DESTRUCTIVE: `bash scripts/release.sh --candidate` is fresh construction; before
policy or build work it 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`.

`bash scripts/release.sh --recover <version> <source-commit>` is
retained-byte-only, read-only validation. It preserves retained payload, ledger,
and proofs, revalidates them, and reports `retained-candidate-valid`. It never
rebuilds, refreshes advisories, contacts hosts, installs wheels, reads
authentication, or uses the network.

`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, and per-target
install/smoke proofs are locally consistent for the retained bytes. Proofs do
not authorize publication and do not prove external distribution.

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. `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.

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. `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>/`,
which holds `ledger.json`, the three proof receipts under `proofs/`, and, after
the prerequisite is published, the single post-finalization tombstone verification
record.

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 are immutable create-only objects.
`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`](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

- **Framework:** pytest. Files `test_*.py`, functions `test_*`. Shared fixtures in `tests/conftest.py`.
- **Fixture journal:** `tests/fixtures/journal/` — immutable mock input with facets, entities, segments, and index state. The autouse `set_test_journal_path` fixture in `tests/conftest.py` points unit tests at it. Tests that write, scan, or rebuild journal/index state must use `journal_copy` or a smaller `tmp_path` journal (see §8).
- **Run one test:** `make test-only TEST=tests/test_utils.py::test_foo` or `TEST="-k test_foo"`. **One app:** `make test-app APP=<name>`.
- **`make test` runs everything** — `tests/` and every `solstone/apps/*/tests/` in one parallel run. App tests are not a separate step.
- **`make test` / `make ci` are strict unit/component rails** — mock process, thread, clock, network, and repository boundaries; no real browser, live network, API keys, heavyweight builds, or writes to shared fixture state. Real local build/process and persisted-index contracts use `@pytest.mark.integration` and run only through `make test-integration`. Live product verification uses `make sandbox`.
- **After editing `solstone/convey/` or `solstone/apps/`:** `journal restart-convey` to reload code in a running stack.
- **`make dev` + `make sandbox`** both write runtime artifacts into the fixtures journal; `tests/fixtures/journal/.gitignore` covers those — never commit 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 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` |
| Sandbox profile intent (`health/sandbox-profile/intent.json`) | `solstone/think/sandbox_profile/intent.py` — disposable sandbox lifecycle intent only; marker file remains externally owned and read-only |
| 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/hpke/upload_private.pem`, `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/upload_key.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:

```python
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:

  ```python
  # 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](https://docs.astral.sh/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/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](INSTALL.md) (owner install), [CONTRIBUTING.md](CONTRIBUTING.md) (developer install), `journal setup`, and `journal service`.

## 13. Owner-facing copy: the system-anatomy canon

- **Composition by register: owner-facing two parts, sol the keeper.** In owner-facing copy, name the two parts the owner has — `solstone = observers + journal` — and name sol as the keeper who lives in and tends the journal, not a third enumerated part. Never write a three-part owner-facing enumeration in owner-visible copy. The engineering/architecture register is retained and explicit: in architecture statements, technical docs, system/diagram-internal labels, code-side prose, and this repo's architecture sections, the system is `solstone = observers + sol agent + journal` — the sol agent is the running software that tends the journal. The split is by register, not contradiction: owner-facing → two parts, sol the keeper in the journal; engineering/architecture → the sol agent is the running software that tends the journal.
- **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".
- **`capture` is code-only.** Keep it in module names such as `solstone/observe/`, function names, OS subsystem identifiers such as `com.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.
- **`sol` is one thing.** `sol` is the running software; there is no homunculus behind it. Use two registers for one entity: `sol` in conversation, `sol agent` in technical contexts.
- **`keeper` is a surface-specific edge case.** `voice-terminology.md` makes `keeper` the role noun for `sol` in product copy generally. The `solstone-swift` surface bans `keeper` because the mobile UX uses the owner's chosen identity, default `sol`. 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: `solstone/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, `capture` is 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". |
