diff --git a/setup b/setup index fabd36e0a..3023d8550 100755 --- a/setup +++ b/setup @@ -3010,10 +3010,10 @@ if [ "$NO_TEAM_MODE" -eq 1 ] && [ -x "$SETTINGS_HOOK" ]; then "$SETTINGS_HOOK" remove-source --source plan-tune-cathedral >/dev/null || true "$SETTINGS_HOOK" remove-source --source auq-error-fallback >/dev/null || true "$SETTINGS_HOOK" remove-source --source gstack-timeline-stop >/dev/null || true - # verify-gate is a user-registered opt-in unrelated to team mode -- turning - # team mode off must not delete it (uninstall still sweeps it, correctly, - # because there the binary itself is being removed). - GSTACK_SWEEP_EXCLUDE_SOURCES="verify-gate" "$SETTINGS_HOOK" prune-stale --all >/dev/null || true + # verify-gate and gstack-memorable are user-registered opt-ins unrelated to + # team mode -- turning team mode off must not delete them (uninstall still + # sweeps both, correctly, because there the binaries themselves are removed). + GSTACK_SWEEP_EXCLUDE_SOURCES="verify-gate,gstack-memorable" "$SETTINGS_HOOK" prune-stale --all >/dev/null || true fi # ─── Redact pre-push guard consent (#1946) ─────────────────────────────────── diff --git a/test/gstack-settings-hook-schema-aware.test.ts b/test/gstack-settings-hook-schema-aware.test.ts index 174892091..87c1170a3 100644 --- a/test/gstack-settings-hook-schema-aware.test.ts +++ b/test/gstack-settings-hook-schema-aware.test.ts @@ -927,6 +927,32 @@ describe('remove-source: identity-aware (tag OR table)', () => { expect(backups()).toEqual([]); }); + test('setup --no-team sweep: GSTACK_SWEEP_EXCLUDE_SOURCES keeps verify-gate AND gstack-memorable (tagged or tag-stripped), sweeps timeline', () => { + fs.writeFileSync(settingsFile, JSON.stringify({ + hooks: { + Stop: [ + { _gstack_source: 'verify-gate', hooks: [{ type: 'command', command: '/x/bin/gstack-verify-gate' }] }, + { _gstack_source: 'gstack-timeline-stop', hooks: [{ type: 'command', command: '/x/hosts/claude/hooks/timeline-stop-hook' }] }, + ], + UserPromptSubmit: [ + { _gstack_source: 'gstack-memorable', hooks: [{ type: 'command', command: memo }] }, + { hooks: [{ type: 'command', command: `bash "${memo}"` }] }, // tag stripped by Claude Code + ], + }, + }, null, 2)); + const r = runIso(['prune-stale', '--all'], { GSTACK_SWEEP_EXCLUDE_SOURCES: 'verify-gate,gstack-memorable' }); + expect(r.exitCode).toBe(0); + expect(r.stdout).toMatch(/removed 1 /); + const s = settings(); + expect(s.hooks.Stop).toHaveLength(1); + expect(s.hooks.Stop[0]._gstack_source).toBe('verify-gate'); + expect(s.hooks.UserPromptSubmit).toHaveLength(2); + // and WITHOUT the exclusion (uninstall) the memorable items go too + const r2 = runIso(['prune-stale', '--all']); + expect(r2.stdout).toMatch(/removed 3 /); + expect(settings().hooks).toBeUndefined(); + }); + test('a tagged legacy stray (single item, no table row) is still removed', () => { fs.writeFileSync(settingsFile, JSON.stringify({ hooks: { UserPromptSubmit: [{ _gstack_source: 'gstack-memorable', hooks: [{ type: 'command', command: '/legacy/anything' }] }] }, diff --git a/test/verify-gate.test.ts b/test/verify-gate.test.ts index 922b7e8bb..7f79cf0d7 100644 --- a/test/verify-gate.test.ts +++ b/test/verify-gate.test.ts @@ -356,7 +356,9 @@ describe('opt-in contract (adapted from the fork: NOT registered by default)', ( expect(mentions.length).toBeGreaterThan(0); // the exclusion itself is pinned for (const line of mentions) { const t = line.trim(); - const allowed = t.startsWith('#') || t.includes('GSTACK_SWEEP_EXCLUDE_SOURCES="verify-gate"'); + // The exclusion list may name other user-registered opt-ins beside + // verify-gate (gstack-memorable); it must still start with verify-gate. + const allowed = t.startsWith('#') || /GSTACK_SWEEP_EXCLUDE_SOURCES="verify-gate(,[a-z-]+)*"/.test(t); expect(allowed).toBe(true); expect(t).not.toContain('add-event'); }