config: active-journal matrix for sol config journal + dispatcher exit-code fix
cmd_journal now refuses to silently orphan or destroy a populated journal.
The active-state matrix (current/target × active/not-active) gates four new
flags: --move (atomic os.rename, same-filesystem only), --switch (wrapper-only,
data untouched), --merge (refuses with verbatim instructions pointing at
sol call journal merge), --force (escape hatch). --yes / --dry-run / no-flag
control whether the plan summary executes or is shown for review. All
non-executing paths leave the target directory uncreated.
Decision logic factored into a pure decide() over a JournalChange dataclass,
with a separate execute() for IO. service_is_installed() and
service_is_running() are new public helpers in think.service; _restart,
_status, and _up now share these. cmd_journal branches on running state
upstream so the prior lode's --if-installed restart no longer wakes a
stopped service on --move.
Two collateral fixes:
- think/sol_cli.py:run_command was discarding integer returns from
module.main() (process exited 0 even when the dispatched command
returned non-zero). Now honored. SystemExit semantics unchanged.
Regression test invokes the dispatcher via real subprocess.
- think/install_guard.py:_current_journal_for_alias fallback path
changed from ~/Documents/Solstone to ~/Documents/journal for vocabulary
consistency.
journal_is_active(path) added to think.utils — reads <path>/config/journal.json
directly, never raises, used to detect whether a journal has an owner before
any wrapper rewrite.
Exit-code convention: 0 = success / noop / dry-run, 1 = refusal /
validation, 2 = partial-state failure (rename succeeded but wrapper write
failed; or wrapper rewrote but service start returned non-zero).