fix(slug): env-override runs never persist to the cwd cache; cache is GSTACK_HOME-aware

Found while closing the wave's eval gate: a test exporting
GSTACK_PROJECT_SLUG from the repo root persisted the override into the cwd
slug cache, silently rebinding the ENTIRE repo's session state (evals,
decisions, timelines) to the test's slug for every later env-less run. The
escape hatch is per-invocation by contract — it no longer writes the cache.
The cache dir also hardcoded $HOME while lib/bin-context.ts's native port
(#2561) reads it GSTACK_HOME-aware, so temp-home test runs littered the real
~/.gstack (observed: 2,528 stale temp-cwd entries, swept). Writer and reader
now key the same GSTACK_HOME-aware cache; regression tests pin both
behaviors.

Also raises the cso --diff eval budget (240s/25t → 360s/40t):
transcript-verified, the wave's legitimately-grown audit session completes
the report and dies in closing telemetry at ~215s under the old budget; the
full-audit sibling already runs at 300s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 12:25:08 -07:00
co-authored by Claude Fable 5
parent 54ca64b11f
commit 5af4a03b13
3 changed files with 57 additions and 5 deletions
+12 -2
View File
@@ -27,7 +27,11 @@
# injection when consumed via source or eval.
set -euo pipefail
CACHE_DIR="$HOME/.gstack/slug-cache"
# GSTACK_HOME-aware, matching lib/bin-context.ts's native port (#2561): the
# bash writer and the TS reader must key the SAME cache, and a test running
# with GSTACK_HOME=<temp> must write its cache junk there, not into the real
# home (observed: 2,528 stale temp-cwd entries accumulated in ~/.gstack).
CACHE_DIR="${GSTACK_HOME:-$HOME/.gstack}/slug-cache"
PROJECT_DIR="$(pwd)"
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_')
@@ -37,8 +41,14 @@ SLUG=""
# 0. Explicit env override — wins over everything. Escape hatch for vendored
# sub-repos and other genuine "subdir IS its own project" edge cases.
SLUG_FROM_ENV=0
if [[ -n "${GSTACK_PROJECT_SLUG:-}" ]]; then
SLUG=$(printf '%s' "$GSTACK_PROJECT_SLUG" | tr -cd 'a-zA-Z0-9._-')
# Per-invocation escape hatch, never a durable identity: persisting it
# would rebind THIS cwd's slug for every later env-less run (observed: a
# test exporting GSTACK_PROJECT_SLUG from the repo root rebound the whole
# repo's session state to the test's slug).
SLUG_FROM_ENV=1
fi
# 1. Walk up from pwd, tracking the OUTERMOST ancestor with a canonical
@@ -160,7 +170,7 @@ SLUG="${SLUG:-$(basename "$PROJECT_DIR" | tr -cd 'a-zA-Z0-9._-')}"
# injection, but the invariant should not depend on that reasoning).
SLUG=$(printf '%s' "$SLUG" | tr -cd 'a-zA-Z0-9._-')
if [[ -n "$SLUG" ]]; then
if [[ -n "$SLUG" && "$SLUG_FROM_ENV" -eq 0 ]]; then
CURRENT_CACHE=""
if [[ -f "$CACHE_FILE" ]]; then
CURRENT_CACHE=$(cat "$CACHE_FILE" 2>/dev/null || true)
+37
View File
@@ -63,3 +63,40 @@ describe('gstack-slug cache-read sanitization', () => {
}
});
});
// The GSTACK_PROJECT_SLUG escape hatch is per-invocation, never durable: a
// test exporting it from the repo root once rebound the ENTIRE repo's session
// state (evals, decisions, timelines) to the test's slug via the cwd cache.
// The cache is also GSTACK_HOME-aware now, matching lib/bin-context.ts's
// native port — temp-home runs must not litter the real ~/.gstack (observed:
// 2,528 stale temp-cwd entries).
describe('slug cache hygiene', () => {
test('an env-override run never writes the cwd cache', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'slug-env-'));
try {
const r = spawnSync(['bash', SLUG_BIN], {
cwd: os.tmpdir(),
env: { ...process.env, GSTACK_HOME: home, GSTACK_PROJECT_SLUG: 'override-slug' },
});
expect(r.stdout.toString()).toContain('SLUG=override-slug');
expect(fs.existsSync(path.join(home, 'slug-cache'))).toBe(false);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
test('an env-less run caches under GSTACK_HOME, not $HOME', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'slug-home-'));
try {
const r = spawnSync(['bash', SLUG_BIN], {
cwd: os.tmpdir(),
env: { ...process.env, GSTACK_HOME: home },
});
expect(r.exitCode).toBe(0);
const entries = fs.readdirSync(path.join(home, 'slug-cache'));
expect(entries.length).toBe(1);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
});
+8 -3
View File
@@ -161,9 +161,14 @@ IMPORTANT:
- Focus on changes in the current branch vs main.
- The webhook.ts file was added on this branch — it should be analyzed.`,
workingDirectory: csoDiffDir,
maxTurns: 25,
maxTurns: 40,
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent'],
timeout: 240_000,
// 360s/40 turns: the v1.67 wave grew the audit session legitimately —
// transcript-verified, the agent finds the webhook vuln, spawns the
// verification subagent, and writes the report, then gets killed at
// ~215s in its CLOSING telemetry under the old 240s/25-turn budget.
// The full-audit sibling already runs at 300s.
timeout: 360_000,
});
logCost('cso', result);
@@ -176,7 +181,7 @@ IMPORTANT:
).toBe(true);
recordE2E(evalCollector, 'cso-diff-mode', 'e2e-cso', result);
}, 240_000);
}, 400_000);
});
describeIfSelected('CSO v2 — infra scope', ['cso-infra-scope'], () => {