D9 cross-model finding from codex outside voice: salience-sourced digests
can include emotionally-weighted personal pages (family, therapy,
reflection). Pulling those into a coding-review prompt leaks sensitive
context into work-flow reasoning.
fetchSalience now strips entries whose slugs don't match an allowlist
prefix BEFORE writing to the cache file. Default allowlist is
SALIENCE_DEFAULT_ALLOWLIST = ['projects/', 'concepts/', 'gstack/'].
User can extend via:
gstack-config set salience_allowlist 'projects/,gstack/,concepts/,custom/'
or override with GSTACK_SALIENCE_ALLOWLIST env var.
Digest still records the strip count for transparency. Empty result
emits 'all N entries stripped' note rather than silent absence.
test/salience-allowlist.test.ts: 9 tests covering default permits,
default blocks, empty allowlist, env override, whitespace trimming,
and the invariant that defaults contain nothing sensitive (personal,
family, therapy, reflection, private, medical, health).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When autoplan dispatches 4 planning skills back-to-back and they all hit
a cold-miss on the same digest, only ONE actually fetches from the brain.
The rest dedup via the project-scoped lockfile at
~/.gstack/projects/<slug>/brain-cache/.refresh.lock.
Reuses the 5-min stale-takeover convention from /sync-gbrain. Lock is
taken over when:
- File is older than CACHE_REFRESH_LOCK_TIMEOUT_MS
- PID is on the same host and dead (process.kill(pid, 0) fails)
- Lock file is corrupt (defensive)
withRefreshLock(projectSlug, fn) returns either the callback's value or
the literal 'dedup'. The CLI emits exit code 3 + diagnostic stderr on
dedup, so callers can choose to wait + retry (resolver does this) or
fall through to stale-but-usable behavior.
test/cache-concurrent-refresh.test.ts: 7 tests covering acquire/release,
stale-takeover, dead-PID takeover, corrupt-lock recovery, error-path
release, and cross-project lock location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>