Skill index canonical location and staleness guard
The skill index was resolving against the process working directory instead of a fixed location. Different working directories created scattered index copies, and the interface sometimes loaded the wrong one.
I changed the resolution base to a canonical per-user state root. Changing only the default value would not have worked, because the repository config file explicitly sets a relative value. The base change makes the rule hold regardless of configuration, while absolute and tilde-prefixed values still pass through untouched.
Staleness guard
Metadata is written to a sidecar file alongside the index, with the same data mirrored inside the index itself. The sidecar wins on conflict. Write order is index to a temporary name, atomic rename, sidecar last. A missing or older sidecar means unknown provenance rather than a current index.
The staleness check runs on every read with a short cache. I had assumed a full skill tree walk was too expensive for a hot path, but measurement showed it costs about the same as the deserialization the interface already performs on every request.
Cache invalidation
Two consumers share a process but not the index object. One caches a module global for the process lifetime, the other reads from disk on every request. A rebuild from either would leave a fresh index on disk and a stale one in memory in the same process.
The rebuild clears the shared global in a finally block so it runs whether the build succeeded or failed. A cheap file stamp is recorded alongside the cached index so one process notices when a different process rebuilt underneath it.
Rebuild policy
Exactly one component may rebuild without being asked, and only at startup. Absence rebuilds synchronously. Staleness rebuilds in the background while serving the old index, so a rebuild is never added to a protocol handshake a client can time out on.
The interface serves stale data and shows an informational notice with an explicit rebuild button. The guard fails open: an exception inside the check serves the index and warns rather than denying access. Messages never say corrupt or invalid, because the guard fires on ordinary skill updates and alarming language would push users to reinstall.
Verification
I reproduced the original failure as a test. The interface was restarted from the exact directory where the wrong count was first observed, and it now reports the correct count. The stale artifacts elsewhere on disk were confirmed untouched, byte for byte.
A real build with no stubs, real embedding model and real vector store, took about 49 seconds. Nearly all of it was model loading rather than indexing.
The old scattered index copies are orphaned deliberately: not migrated, not deleted. Migrating one would have promoted a stale index to canonical status, which is the exact defect being fixed made permanent.