From 5af4a03b13c49d343ad9ed8b3b2ed0315643261d Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 16 Aug 2026 12:25:08 -0700 Subject: [PATCH] fix(slug): env-override runs never persist to the cwd cache; cache is GSTACK_HOME-aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- bin/gstack-slug | 14 ++++++++++-- test/gstack-slug-sanitize.test.ts | 37 +++++++++++++++++++++++++++++++ test/skill-e2e-cso.test.ts | 11 ++++++--- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/bin/gstack-slug b/bin/gstack-slug index e9b2aaf02..924454722 100755 --- a/bin/gstack-slug +++ b/bin/gstack-slug @@ -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= 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) diff --git a/test/gstack-slug-sanitize.test.ts b/test/gstack-slug-sanitize.test.ts index 7c5dc8f7d..9933ca01e 100644 --- a/test/gstack-slug-sanitize.test.ts +++ b/test/gstack-slug-sanitize.test.ts @@ -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 }); + } + }); +}); diff --git a/test/skill-e2e-cso.test.ts b/test/skill-e2e-cso.test.ts index 64aa18bde..9bd0ed380 100644 --- a/test/skill-e2e-cso.test.ts +++ b/test/skill-e2e-cso.test.ts @@ -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'], () => {