personal memory agent
0

Configure Feed

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

Sol CLI Developer Guide#

How the sol CLI is organized, how to add new commands, and what files to maintain.

Architecture#

The CLI has two tiers with distinct purposes:

Tier Pattern Framework Purpose
Top-level sol <cmd> / journal <cmd> Native sol + Python journal dispatcher Native journal access, plus host services and local-only host tools under journal
Call sol call <app> <cmd> Native authority inventory, plus finite journal compatibility Tool-callable functions — what agents and humans invoke for data operations

The boundary#

If an AI agent should tool-call a journal-access data operation → sol call. These commands appear in SKILL.md files and are invoked by talent agents during conversations. Local-only host tools live under journal.

If it's system plumbing or local-only host control → journal <cmd>. Processing pipelines, supervisor, services, capture — things that cron or systemd runs.

Interactive entry points (sol chat, sol help, journal engage) are top-level for discoverability even though they're user-facing. Agents don't invoke these.

Console scripts are split across distributions. The base solstone package ships the public POSIX sol and solstone launchers. The solstone-core package ships the native solstone-core executable those launchers run. The solstone-journal / solstone-journal-cuda distributions ship the host-only journal and mlx-vlm-server console scripts. The dispatcher, including journal_main(), still lives in base solstone.think.sol_cli, but a thin/bare install has no journal executable on PATH.

Top-Level Commands (sol <cmd>)#

How they work#

The public sol / solstone commands are root-owned launchers that exec the sibling native solstone-core binary. Their top-level native commands are declared beside their Rust handlers under solstone/think/native/<command>/authority.toml.

solstone/think/sol_cli.py now contains only the journal host dispatcher. It has a static COMMANDS dict mapping host command names to module paths:

COMMANDS: dict[str, str] = {
    "think": "solstone.think.thinking",
    "importer": "solstone.think.importers.cli",
    ...
}

Each module must export a main() function. The dispatcher does importlib.import_module(path) then calls module.main().

ALIASES provide shortcuts (e.g., journal upjournal service up).

Adding a top-level public sol command#

  1. Create solstone/think/native/<command>/authority.toml with the command path, params, entry type, operation id, and handler name.

  2. Implement solstone/think/native/<command>/command.rs and bind the handler declared by the authority.

  3. Regenerate the inventory with make build-native-sol-inventory.

  4. Update parity fixtures and native-sol gates for the new command.

Use this authority route for top-level commands that read or write journal data through the native HTTP boundary. For local commands that touch no journal data and have no sol call oracle path, use a direct match arm in solstone_core_sol::run alongside root, path, status, and skills.

For host-only commands, use the journal dispatcher instead: create a Python module with main() and register it in solstone/think/sol_cli.py with the appropriate service or universal surface.

journal sandbox-profile is a host-only service command for disposable sandbox journals. It is a deterministic, redacted lifecycle wrapper around existing service enable/disable owners and is gated by the externally owned .solstone-sandbox.json marker. Its closed capability vocabulary is defined in solstone/think/sandbox_profile/manifest.py; do not duplicate that list in generated skill references.

Files to maintain#

File What to do
solstone/think/native/<command>/authority.toml Declare the public native command
solstone/think/native/<command>/command.rs Implement the native handler
solstone/think/sol_cli.py COMMANDS dict Register host-only journal commands

Call Commands (sol call <app> <cmd>)#

How they work#

Native sol call commands are declared by authority.toml files beside Rust handlers under solstone/apps/*/native/ and solstone/think/tools/native/. The production aggregate inventory is generated into core/crates/solstone-core-sol-client/src/generated/inventory.rs.

The only remaining Python Typer mount is the finite private compatibility path for sol call journal:

# think/call.py
from solstone.think.tools.call import app as journal_app

call_app.add_typer(journal_app, name="journal")

Local-only service tools such as journal navigate and journal identity are registered in COMMANDS instead of mounted under sol call.

Adding a new native app command#

This is the happy path for most new commands.

  1. Create or update solstone/apps/<name>/native/authority.toml with the path, params, operation id, HTTP method, route, and handler.

  2. Implement solstone/apps/<name>/native/command.rs and bind the handler declared by the authority.

  3. Regenerate the inventory with make build-native-sol-inventory.

  4. Update the app command fragment (if agents should use these commands). make skills discovers these fragments via scripts/build_skill_references.py from solstone/apps/*/talent/*/SKILL.md:

# solstone/apps/myapp/talent/myapp/SKILL.md
---
name: myapp
description: >
  What this command fragment covers. When to trigger it.
  TRIGGER: keyword1, keyword2, keyword3.
---

# MyApp CLI Fragment

Common pattern:
\`\`\`bash
sol call myapp <command> [args...]
\`\`\`

## list

\`\`\`bash
sol call myapp list [-d DAY] [-f FACET]
\`\`\`

List items for a day.

- `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env).
- `-f, --facet`: facet name (default: `SOL_FACET` env).
  1. Run make skills to regenerate the checked-in router references with scripts/build_skill_references.py and refresh the installed sol + journal router skill symlinks.

  2. Run make check-skill-references before committing, or rely on make ci / make install-checks. The check invokes scripts/build_skill_references.py --check and fails when generated router references are stale.

Local-only think tools#

Use a top-level journal <cmd> entry when the command is meaningful only on the journal host and depends heavily on solstone/think/ internals.

  1. Create solstone/think/tools/<name>.py with app = typer.Typer() and a main() that calls app().
  2. Register in solstone/think/sol_cli.py with surface="service".
  3. Optionally update a router skill reference if the command needs agent-facing guidance.

Files to maintain for a new call command#

File What to do Required?
solstone/apps/<name>/native/authority.toml Native command path, params, and route contract Yes
solstone/apps/<name>/native/command.rs Native handler implementation Yes
solstone/apps/<name>/talent/<name>/SKILL.md App command guidance fragment used by scripts/build_skill_references.py If agents should use it
solstone/talent/sol/references/commands.md Generated sol call <app> inventory Auto-generated by make skills (scripts/build_skill_references.py)
solstone/talent/journal/references/commands.md Generated journal-host command guidance Auto-generated by make skills (scripts/build_skill_references.py)
core/fixtures/native-sol/parity/<name>.jsonl Native parity vectors Yes

Conventions#

Environment defaults#

Commands that take --day or --facet should respect SOL_DAY and SOL_FACET environment variables as defaults. Use the helpers:

from solstone.think.utils import resolve_sol_day, resolve_sol_facet

day = resolve_sol_day(day_arg)    # Falls back to SOL_DAY env
facet = resolve_sol_facet(facet_arg)  # Falls back to SOL_FACET env

Action logging#

All mutating sol call commands should log their actions for audit:

from solstone.think.facets import log_call_action

log_call_action(
    facet=facet,
    action="myapp_create",
    params={"key": "value"},
    day=day,
)

This writes to facets/{facet}/logs/{day}.jsonl (or config/actions/{day}.jsonl if facet is None).

Commands that agents invoke proactively (without the user explicitly asking) should accept a --consent flag:

consent: bool = typer.Option(
    False,
    "--consent",
    help="Assert that explicit user approval was obtained before calling this command (agent audit trail).",
)

This is for audit trail — it records that the agent confirmed user consent before acting.

Output patterns#

  • JSON output: Use --json flag for machine-readable output. Default to human-friendly text.
  • Errors: Write to stderr via typer.echo(..., err=True) and raise typer.Exit(1).
  • Confirmations: Use --yes to skip interactive confirmation for destructive operations.
  • Pagination: Use --limit / --cursor for list commands.

Typer command naming#

@app.command("list")      # Verb as command name
@app.command("show")      # Singular operations
@app.command("create")    # CRUD verbs

Use lowercase, single-word names. Hyphenated names for multi-word (list-nudges-due, set-name).

Doctor Commands#

doctor is a universal command surface: both sol doctor and journal doctor dispatch to solstone.think.doctor, with the battery selected by the active binary.

sol doctor checks universal CLI usability and is designed to run cleanly on a journal-less or repo-less machine. Its default battery has four checks:

  • python_version — blocker; light package-metadata Requires-Python floor, no pyproject.toml required.
  • sol_importable — blocker.
  • local_bin_sol_reachable — advisory.
  • stale_alias_symlink — blocker; checks only the sol wrapper. Stale aliases warn, never block, and journal setup repairs them.

journal doctor diagnoses journal-host health. It is role-aware: on a machine without a local journal directory or installed journal service, folder and service checks emit skip (no local journal / no local journal service) instead of false failures. Its battery is:

  • disk_space — advisory.
  • host_dependencies, config_dir_readable, journal_dir_writable, service_identity, service_running, journal_sync, stale_alias_symlink — blockers. Stale journal aliases warn, never block, and journal setup repairs them.
  • supervisor_conflict — blocker; macOS only; fails when journal.app and the legacy LaunchAgent are both supervising one journal, or when a foreign persistent LaunchAgent relaunches /Applications/solstone.app. Proven-conflict actions are journal service uninstall for the legacy service and one-line remove foreign launchers targeting /Applications/solstone.app commands for foreign launcher plists; other diagnoses remain visible with their actions withheld until the topology is resolved.
  • launchd_stale_plist — advisory on macOS; skipped on Linux. It advises removing the legacy service first and reinstalling the headless service only as a separate step.
  • feature:pdf-import, feature:pdf-export, feature:whisper — advisories with the exact extra-install command when missing.

host_dependencies fix guidance is: Reinstall the journal host stack: pip install --upgrade solstone-journal | uv tool install --upgrade solstone-journal | pipx install --force solstone-journal. On an NVIDIA host use solstone-journal-cuda instead — never install both.

Journal-host blocker failures include invalid service config, service identity mismatch, crash loops, systemd failed state, and journal-sync conflicts. An installed service with no supervisor socket is a warning when the OS unit is not failed. --feature <name> runs a single feature advisory on either surface.

Use sol doctor for “can this CLI run?”, journal doctor for “why is this journal host unhealthy?”, make preflight for the stdlib-only fresh-clone check before .venv/uv exist, and journal health for the live supervisor status view.

Structured output: journal setup --jsonl and doctor --jsonl#

Use --jsonl when another process needs progress events as they happen. The contract is one JSON object per stdout line, flushed immediately; doctor --jsonl is mutually exclusive with doctor --json, and the existing doctor --json payload keeps its short statuses (ok, warn, fail, skip).

Event Emitted by When
setup.started journal setup --jsonl Setup arguments are resolved and the run starts.
setup.completed journal setup --jsonl Setup reaches a terminal ok or failed state.
step.started journal setup --jsonl A setup step starts.
step.completed journal setup --jsonl A setup step finishes with outcome: "ok" or outcome: "skipped".
step.failed journal setup --jsonl A setup step fails or reaches a dead end.
step.warning journal setup --jsonl Setup translates advisory diagnostics, dropped doctor lines, or non-fatal wrapper-provisioning failures.
doctor.started doctor --jsonl Doctor diagnostics begin.
check.completed doctor --jsonl One diagnostic check finishes. Status is long form: ok, warning, failed, or skipped.
doctor.completed doctor --jsonl Doctor diagnostics finish with status: "ok", "warning", or "failed".
Code When
doctor_failed Doctor reports a blocking failure or cannot start.
doctor_jsonl_incomplete Doctor exits without a doctor.completed event.
doctor_timeout Doctor exceeds its timeout.
journal_dir_invalid The requested journal path is a regular file.
journal_existing_blocked Non-interactive setup refuses to auto-claim an existing journal.
service_up_failed Service installation succeeded but service startup failed.
setup_unhandled_exception A setup step raised an unexpected exception.
step_subprocess_failed A setup subprocess exited non-zero.
step_subprocess_timeout A setup subprocess exceeded its timeout.

Step names are fixed and ordered: doctor, journal, install_models, skills_user, skills_journal, wrapper, service, brain.

Skipped, warning, or resumed reasons are fixed: --skip-models, --skip-brain, --skip-models implies --skip-brain, --skip-skills, --skip-service, --skip-wrapper, a provider is already configured, provider config is not in the expected shape, local provider unavailable on this host, local bootstrap did not start, prior_run_ok, resumed_after_restart.

The wrapper setup step provisions both managed wrappers in-process for source and packaged installs. It backs up a non-owned alias under /tmp before overwriting it. When --skip-wrapper is passed, the step is skipped entirely and no alias is inspected or replaced. Provisioning failures emit step.warning and setup still exits successfully so the next run can repair the wrappers.

Doctor pass-through#

journal setup --jsonl runs journal doctor --readiness --jsonl for the doctor step and forwards doctor.started, check.completed, and doctor.completed lines verbatim. The readiness battery is the client readiness checks (python_version, sol_importable, local_bin_sol_reachable, stale_alias_symlink, disk_space, journal_dir_writable) plus host_dependencies, default_stt_ready, feature:pdf-import, feature:pdf-export, and feature:whisper; it does not run runtime service, sync, config-dir, or launchd checks. Advisory doctor checks are also translated into setup-level step.warning events so consumers can handle setup warnings uniformly.

Example stream excerpt for setup readiness:

{"event":"setup.started","ts":"2026-05-11T20:00:00Z","version":"0.0.0+source","mode":"non_interactive"}
{"event":"step.started","ts":"2026-05-11T20:00:00Z","step":"doctor","index":1,"total":8}
{"event":"doctor.started","ts":"2026-05-11T20:00:00Z","version":"0.0.0+source","port":5015,"feature":""}
{"event":"check.completed","ts":"2026-05-11T20:00:01Z","name":"python_version","severity":"blocker","status":"ok","detail":"Python version ok","fix":""}
{"event":"doctor.completed","ts":"2026-05-11T20:00:01Z","status":"ok","duration_ms":120,"summary":{"total":10,"failed":0,"warnings":0,"skipped":0}}
{"event":"step.completed","ts":"2026-05-11T20:00:01Z","step":"doctor","outcome":"ok","duration_ms":121}
{"event":"step.completed","ts":"2026-05-11T20:00:04Z","step":"service","outcome":"ok","duration_ms":900}
{"event":"setup.completed","ts":"2026-05-11T20:00:04Z","status":"ok","duration_ms":4000}

Consumer snippet#

import json
import subprocess

proc = subprocess.Popen(
    ["journal", "setup", "--jsonl", "--yes"],
    stdout=subprocess.PIPE,
    text=True,
    bufsize=1,
)
for line in proc.stdout:
    event = json.loads(line)
    print(event["event"], event)
proc.wait()

Directory Structure#

solstone/
├── think/
│   ├── sol_cli.py                  # Entry point + COMMANDS registry
│   ├── call.py                     # sol call journal compatibility mount
│   ├── tools/
│   │   ├── call.py                 # sol call journal (built-in)
│   │   ├── navigate.py             # journal navigate (built-in)
│   │   └── sol.py                  # journal identity (built-in)
│   └── *.py                        # Top-level command modules
├── solstone/apps/
│   ├── activities/
│   │   ├── native/                 # sol call activities native authority/handler
│   │   └── talent/activities/SKILL.md # builder source for generated router refs
│   ├── entities/native/
│   ├── speakers/native/
│   ├── support/native/
│   ├── transcripts/native/
│   ├── awareness/native/
│   └── ...
├── talent/
│   ├── sol/SKILL.md                # installed sol router skill
│   ├── journal/SKILL.md            # installed journal router skill
│   ├── sol/references/commands.md  # generated app command inventory
│   ├── journal/references/commands.md # generated journal-host command guidance
│   └── *.md                        # Agent prompt files
├── journal/.agents/skills/          # sol + journal router symlinks
└── AGENTS.md                        # Developer guide

The solstone/apps/ dual role#

solstone/apps/ contains convey web routes and, when an app exposes agent-facing CLI commands, a native native/authority.toml plus native/command.rs.

Current Command Inventory#

Top-level (sol <cmd> / journal <cmd>)#

Group Commands
Think (processing) import, think, planner, indexer, supervisor, schedule, maintenance, top, health, notify (sol notify, native), heartbeat
Service service (+ aliases up, down, start), navigate, identity, settings, install-provider
Observe (capture) transcribe, describe, sense, transfer, observer
Talent (AI agents) agents, cortex, talent, call, engage, providers
Convey (web UI) convey, restart-convey, maint
Specialized config, skills, streams, journal-stats, reprocess, formatter, detect-created
Installation doctor
Help help, chat

reprocess is the on-demand single-day reprocess command: process-now by default; --from-scratch re-runs already-complete units.

journal maintenance list|sync|run <app:name> [-- args] manages app-owned recurring routines discovered from apps/*/maintenance.py.

Call (sol call <app> <cmd>)#

App Source Commands
activities solstone/apps/activities/native/authority.toml list, get, create, update, mute, unmute
entities solstone/apps/entities/native/authority.toml list, move, detect, attach, update, aka, record-merge-candidate, merge-candidates, accept-merge-candidate, dismiss-merge-candidate, merge, undo-merge, ambiguities, resolve-ambiguity, entity-history, restore-version, network, history, overview, observations, observe, search
speakers solstone/apps/speakers/native/authority.toml list, show, detect-owner, confirm-owner, clusters, suggest
transcripts solstone/apps/transcripts/native/authority.toml list, read, segments
support solstone/apps/support/native/authority.toml register, search, article, create, list, show, reply, attach, feedback, announcements, diagnose
sol solstone/apps/sol/native/authority.toml set-name, reset, set-owner, sol-init
settings solstone/apps/settings/native/authority.toml personal service keys (show/set/delete). Thinking provider selection lives in the Thinking app; local provider install lives at journal install-provider local.
awareness solstone/apps/awareness/native/authority.toml status, imports, log, log-read
journal solstone/think/tools/call.py search, events, facets, facet (show/create/update/rename/mute/unmute/delete/merge), news, agents, read, imports, import, retention purge, storage-summary

sol skills manages coding-agent skill installation with install, uninstall, and list. The former build verb is gone; make skills runs scripts/build_skill_references.py directly before invoking sol skills install.

Skill System#

Project skill installation installs exactly two router skills into both journal/.claude/skills/ and journal/.agents/skills/: sol and journal. sol skills install --project does not install per-app fragments or every SKILL.md as a top-level skill.

Skill locations:

  • Installed router skills: solstone/talent/sol/SKILL.md, solstone/talent/journal/SKILL.md
  • App command fragments: solstone/apps/<name>/talent/<name>/SKILL.md
  • Generated references: solstone/talent/sol/references/commands.md, solstone/talent/journal/references/commands.md

App command fragments are builder source. make skills folds their guidance into deterministic, checked-in generated references via scripts/build_skill_references.py. The generated references aggregate per-app command guidance from native authority files, including health's sol call health commands and health's journal-host journal health / journal talent guidance. There is no in-repo vit skill.

Fragments document CLI commands and add behavioral guidance beyond what --help shows (e.g., "check entity context before attaching a new relationship to avoid duplicates"). Agents consume that guidance through the sol and journal router skill references.

Keeping skills in sync#

When you add or change a sol call command, update both the native authority/handler and the corresponding app command fragment. The generated router references are what agents actually read — they don't parse --help output. Include:

  • Full command syntax with all flags
  • Behavior notes (edge cases, defaults, validation)
  • Examples showing common usage patterns

Then run make skills. make check-skill-references invokes scripts/build_skill_references.py --check and is wired into make ci / make install-checks to fail when generated references are stale.