Contracts

runtime-state-root.md

Contract: Runtime State Root Resolution

Surface: specify_cli.paths.get_runtime_root()RuntimeRoot Consumers: all global sync/auth/tracker/daemon/state modules

Environment variable contract

ConditionRuntimeRoot.base (all platforms)
SPEC_KITTY_HOME set, non-emptyPath(os.environ["SPEC_KITTY_HOME"])
SPEC_KITTY_HOME unset or empty, POSIXPath.home() / ".spec-kitty"
SPEC_KITTY_HOME unset or empty, Windowsplatformdirs.user_data_dir("spec-kitty", appauthor=False, roaming=False)

Behavioral guarantees

distinct (isolation).

(POSIX byte-identical — NFR-001; Windows = platformdirs base — NFR-003/D4).

per-surface relative suffix (see state-surface-map.md); no consumer recomputes the home independently (FR-010).

  • G1: For any two distinct values of SPEC_KITTY_HOME, the resolved base paths are
  • G2: Resolution is pure — no directory is created, no file is read (NFR-002).
  • G3: With SPEC_KITTY_HOME unset, base equals the pre-fix value on each platform
  • G4: Every global-state location is get_runtime_root().base joined with a fixed,

Test obligations

IDAssertion
T-RR-1SPEC_KITTY_HOME=/tmp/xget_runtime_root().base == Path("/tmp/x") on linux, darwin, win32 (monkeypatched platform).
T-RR-2SPEC_KITTY_HOME="" ⇒ falls through to platform default on each platform.
T-RR-3Unset ⇒ POSIX ~/.spec-kitty; Windows platformdirs base.
T-RR-4Calling get_runtime_root() creates no directories (assert base/auth/etc. absent on a temp HOME).
T-RR-5Architectural guard: no module in sync/auth/tracker/state contains Path.home() / ".spec-kitty" (allowlist keystone + asset-home + migration/fallback).

state-surface-map.md

Contract: State Surface → Path Mapping

Each global-state surface resolves to get_runtime_root().base / <suffix>. The suffix is fixed per surface/platform and MUST NOT change when SPEC_KITTY_HOME is set (only base moves). POSIX suffixes below are the byte-identical contract for NFR-001.

SurfaceResolved path (relative to base)RequirementPer-surface test obligation
Sync config fileconfig.tomlFR-001SyncConfig().config_file under env root; absent from default home
Auth session store (POSIX)auth/FR-002file_fallback.default_store_dir() under env root
Auth session store (Windows)auth/ (normalized)FR-002WindowsFileStorage default under env root (not Path.home())
Token refresh lockauth/refresh.lockFR-003_refresh_lock_path() under env root on POSIX + Windows
Unauthenticated queue DBqueue.dbFR-004default_queue_db_path() (unauth) under env root
Scoped queue DB dirqueues/FR-005default_queue_db_path() (auth) + _scoped_queue_dir() under env root
Active queue scopeactive_queue_scopeFR-005_active_scope_path() under env root
Daemon state/log/lock` (flat, POSIX) / daemon/` (Windows)FR-006_daemon_root(), _sync_root(), lazy SPEC_KITTY_DIR under env root
Lamport clockclock.jsonFR-007LamportClock.load() default + dataclass default under env root
Tracker credentialscredentials (POSIX flat) / tracker/ (Windows)FR-008_tracker_root() under env root
Tracker DBtrackers/FR-008store._trackers_dir() / default_tracker_db_path() under env root
State doctor reportbase + surface patternsFR-009state doctor reported global-sync root == get_runtime_root().base

End-to-end CLI contract (SC-001 / SC-002)

HOME=<tmpA>  SPEC_KITTY_HOME=<tmpB>  SPEC_KITTY_ENABLE_SAAS_SYNC=1
  spec-kitty sync server https://example.invalid
⇒ <tmpB>/config.toml EXISTS
⇒ <tmpA>/.spec-kitty/config.toml ABSENT

This is the literal reproduction from issue #2171, inverted into the passing assertion.