Source-root drift made advisory in memstack index guard
The index guard was forcing a rebuild whenever the source root list changed, even when the index was current. I demoted that comparison to an advisory.
The problem
Trigger 2 in the index guard compared the list of skill source roots stored in the index sidecar against the live root list. A difference forced a rebuild. This created a rebuild loop when two checkouts of the same skills pointed at each other: each startup would see a root mismatch and rebuild, stamping its own root list into the shared sidecar and triggering the next rebuild.
I designed a fix in the previous session and implemented it today.
B-narrow plus advisory
Trigger 2 still runs its source-root comparison, but no longer forces a rebuild. When the roots differ, the guard returns ok=True with status="drift" and a message naming the differing folders. Triggers 1, 3, and 4 are untouched and still fire on their conditions.
I fixed the message defect documented in section 4.2 of the design doc. The old message printed two root counts, which were equal in the common case and named nothing. The new one uses set arithmetic over the two root lists and names each side.
Ordering trap
The advisory is computed where Trigger 2 stood, but I return it last, after the count and slug checks. Returning it in place would let a genuinely stale index escape as ok=True whenever its roots also differed. I pinned this constraint with a test.
Surfacing drift
The first change created a gap: drift was in the API payload but rendered nowhere, and the stderr line from tfidf_search was silenced. I surfaced it in three places.
I rewrote renderIndexNotice in dashboard.html as an explicit five-gate chain that reads index.status. It previously branched on ok and rebuilding only. Gate 3 sits above the ok gate because drift carries ok=true. Gate 3 sits below gate 2 because dashboard.py stamps rebuilding after check_index returns, so the race between status='drift' and rebuilding=true is reachable and rebuild feedback must win.
I added an elif verdict.status == "drift" branch to tfidf_search.py that prints a one-line note at index load. I tested the hot-path objection: the guard runs once per process load and again only when another process rebuilds, not per query. I measured 1 stderr line on first load, then 0 across the next 50 calls.
Tests and deployment
I added tests/test_index_meta_guard.py, the first test coverage this subsystem has ever had. It was 0 of 461. The suite is now 471 passed, 3 skipped.
I committed and deployed to master at a9a581a. All four refs point there. The ~/.memstack/vectors sidecar was never written this session. The built_at timestamp stayed 1785678751.4085395 throughout.
Live case
The secrets-scanner skill exists in two places on this machine. The marketplace copy is 443 lines and 19605 bytes. The checkout copy is 549 lines and 24841 bytes. They differ by 106 lines. Their slugs and counts are identical. The guard now reports this as drift instead of silently rebuilding over it.