`octop.cli.support.state` keeps a private reader/writer for
`~/.octop/cli_state.json` and both halves are looser than the rest of the tree.
`load()` parses with a bare `json.loads(path.read_text(...))`, so an
unparseable file raises a `json.JSONDecodeError` traceback. This file is read by
nearly every CLI command (`config`, `agent`, `db`, `acting`, `ctx`, the REPL),
so one damaged file aborts all of them at once.
`save()` writes with `path.write_text(...)` — truncate, then write. A crash or a
full disk mid-write leaves exactly the truncated file that `load()` then
refuses, with no warning and no `ensure_root()` in between.
`infra/utils/json_file.py` exists for this; its docstring is explicit that a
torn write "creates the same precondition" as a hand-edit slip and that callers
"must not fall back to an empty dict". Four call sites already route through it
(`cli/commands/run.py`, `agents/plugins/manager.py`, `agents/plugins/seed.py`,
`setup/service.py`).
`load()` now reads via `read_json_object` and translates `JsonFileCorruptError`
into `corrupt_config_error(...)` (`CONFIG_FILE_CORRUPT`) — the same policy as
every other `OCTOP_HOME` JSON reader. `save()` writes via `write_json_atomic`
(mkstemp + `os.replace`) so a reader never observes a partial file. A non-object
payload such as `[1, 2]` also raises the typed error now, instead of a stray
`dict.update` `TypeError`.