# 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:

```python
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 up` → `journal 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`:
```python
# 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`:

```markdown
# 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).
```

4. **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.

5. **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:

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

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

### The `--consent` flag

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

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

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

```jsonl
{"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

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