From 1d29225a9240b54a5f1480ae32394fc0cabf29c9 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 13:08:20 -0700 Subject: [PATCH] feat(office-hours): 'never show me these again' for the founder-resources pitch (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 6 resources offer (34 PG essays + Garry/YC videos) had no permanent decline — the reporter showed memory instructions kept being overridden on every update, so people who said no got re-pitched forever. The offer now closes with a standing choice; opting out runs gstack-config set founder_resources false (new key, default true, true|false validated), the write is VERIFIED before any promise (a failed write says so and skips this session only), and every future session skips the entire section silently — no resources, no 'skipped as requested' mention. Config outlives session context, so never means never. Re-enable anytime: gstack-config set founder_resources true. The pitch stays default-ON for everyone who never opted out. Tests pin the key's default/persistence/validation through the real config bin and the generated section's gate-before-content + write-verify contract. Approved as a promo-surface change (CEO review D3.4, 2026-08-14). Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian Co-Authored-By: Claude Fable 5 --- bin/gstack-config | 5 ++ office-hours/sections/design-and-handoff.md | 23 ++++++ .../sections/design-and-handoff.md.tmpl | 23 ++++++ test/founder-resources-optout.test.ts | 77 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 test/founder-resources-optout.test.ts diff --git a/bin/gstack-config b/bin/gstack-config index 1248788e5..970c961bb 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -134,6 +134,7 @@ lookup_default() { redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection redact_prepush_hook) echo "false" ;; pair_agent) echo "off" ;; # remote tunnel consent — fail-closed until /pair-agent asks + founder_resources) echo "true" ;; # office-hours resource pitch — #538 permanent opt-out sets false # Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline: # brain_trust_policy@ — unset on fresh install; setup-gbrain # writes 'personal' for local engines, @@ -315,6 +316,10 @@ case "${1:-}" in echo "Warning: pair_agent '$VALUE' not recognized. Valid values: on, off. Using off." >&2 VALUE="off" fi + if [ "$KEY" = "founder_resources" ] && [ "$VALUE" != "true" ] && [ "$VALUE" != "false" ]; then + echo "Warning: founder_resources '$VALUE' not recognized. Valid values: true, false. Using true." >&2 + VALUE="true" + fi if [ "$KEY" = "plan_tune_hooks" ] && [ "$VALUE" != "prompt" ] && [ "$VALUE" != "yes" ] && [ "$VALUE" != "no" ]; then echo "Warning: plan_tune_hooks '$VALUE' not recognized. Valid values: prompt, yes, no. Using prompt." >&2 VALUE="prompt" diff --git a/office-hours/sections/design-and-handoff.md b/office-hours/sections/design-and-handoff.md index b7f88fa17..3bf507c9e 100644 --- a/office-hours/sections/design-and-handoff.md +++ b/office-hours/sections/design-and-handoff.md @@ -451,9 +451,32 @@ Then proceed to Founder Resources below. ### Founder Resources (all tiers) +**Standing opt-out check (#538) — run FIRST:** + +```bash +~/.claude/skills/gstack/bin/gstack-config get founder_resources 2>/dev/null || echo "true" +``` + +If the value is `false`, **skip this entire section silently** — no resources, +no "skipped as requested" mention. The user said never; config outlives +session context and memory instructions, so never means never. (Re-enable: +`gstack-config set founder_resources true`.) + Share 2-3 resources from the pool below. For repeat users, resources compound by matching to accumulated session context, not just this session's category. +**After sharing, close with the standing choice** (one line, not a ceremony): + +> Want these? I can open any of them — or say "never show me these again" and +> this section disappears for good. + +If the user opts out (any clear phrasing of never/stop showing these): run +`~/.claude/skills/gstack/bin/gstack-config set founder_resources false`, then +VERIFY the write (`gstack-config get founder_resources` must read back +`false`) before promising anything — if the write failed, say so and skip for +this session only. On success confirm in one line with the re-enable command +and continue the handoff. + **Dedup check:** Read `RESOURCES_SHOWN` from the builder profile output above. If `RESOURCES_SHOWN_COUNT` is 34 or more, skip this section entirely (all resources exhausted). Otherwise, avoid selecting any URL that appears in the RESOURCES_SHOWN list. diff --git a/office-hours/sections/design-and-handoff.md.tmpl b/office-hours/sections/design-and-handoff.md.tmpl index cd98c000c..5924dd706 100644 --- a/office-hours/sections/design-and-handoff.md.tmpl +++ b/office-hours/sections/design-and-handoff.md.tmpl @@ -340,9 +340,32 @@ Then proceed to Founder Resources below. ### Founder Resources (all tiers) +**Standing opt-out check (#538) — run FIRST:** + +```bash +~/.claude/skills/gstack/bin/gstack-config get founder_resources 2>/dev/null || echo "true" +``` + +If the value is `false`, **skip this entire section silently** — no resources, +no "skipped as requested" mention. The user said never; config outlives +session context and memory instructions, so never means never. (Re-enable: +`gstack-config set founder_resources true`.) + Share 2-3 resources from the pool below. For repeat users, resources compound by matching to accumulated session context, not just this session's category. +**After sharing, close with the standing choice** (one line, not a ceremony): + +> Want these? I can open any of them — or say "never show me these again" and +> this section disappears for good. + +If the user opts out (any clear phrasing of never/stop showing these): run +`~/.claude/skills/gstack/bin/gstack-config set founder_resources false`, then +VERIFY the write (`gstack-config get founder_resources` must read back +`false`) before promising anything — if the write failed, say so and skip for +this session only. On success confirm in one line with the re-enable command +and continue the handoff. + **Dedup check:** Read `RESOURCES_SHOWN` from the builder profile output above. If `RESOURCES_SHOWN_COUNT` is 34 or more, skip this section entirely (all resources exhausted). Otherwise, avoid selecting any URL that appears in the RESOURCES_SHOWN list. diff --git a/test/founder-resources-optout.test.ts b/test/founder-resources-optout.test.ts new file mode 100644 index 000000000..46993f0bb --- /dev/null +++ b/test/founder-resources-optout.test.ts @@ -0,0 +1,77 @@ +/** + * #538: the office-hours founder-resources pitch takes no for an answer. + * + * The reporter found that memory instructions telling the agent to stop + * showing the 34-resource pool kept being overridden on every update. The + * fix is a config key — `founder_resources` — that session context cannot + * override: `false` skips the entire section silently, forever, until the + * user re-enables it. + * + * Pins: (1) the config key's default, persistence, and validation through + * the real bin/gstack-config subprocess; (2) the generated section gates on + * the key BEFORE any resource content; (3) the opt-out write is verified + * before the skill may promise "never again" (R6 — a failed write must not + * produce a false promise). + */ + +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import { execFileSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +const ROOT = path.resolve(import.meta.dir, ".."); +const CONFIG_BIN = path.join(ROOT, "bin", "gstack-config"); +const SECTION = path.join(ROOT, "office-hours", "sections", "design-and-handoff.md"); + +let tmpHome: string; +beforeEach(() => { tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "gstack-538-")); }); +afterEach(() => { fs.rmSync(tmpHome, { recursive: true, force: true }); }); + +function cfg(args: string[]): string { + return execFileSync(CONFIG_BIN, args, { + encoding: "utf-8", + env: { ...process.env, GSTACK_HOME: tmpHome }, + }).trim(); +} + +describe("founder_resources config key (#538)", () => { + test("defaults to true (pitch stays on for everyone who never opted out)", () => { + expect(cfg(["get", "founder_resources"])).toBe("true"); + }); + + test("set false persists and reads back — the write-verify contract", () => { + cfg(["set", "founder_resources", "false"]); + expect(cfg(["get", "founder_resources"])).toBe("false"); + }); + + test("invalid values are rejected to the default, never persisted as-is", () => { + execFileSync(CONFIG_BIN, ["set", "founder_resources", "banana"], { + encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, GSTACK_HOME: tmpHome }, + }); + expect(cfg(["get", "founder_resources"])).toBe("true"); + }); +}); + +describe("office-hours section gates on the key (#538)", () => { + const src = fs.readFileSync(SECTION, "utf-8"); + + test("the opt-out check precedes any resource content", () => { + const gate = src.indexOf("gstack-config get founder_resources"); + const pool = src.indexOf("Resource Pool"); + expect(gate).toBeGreaterThan(-1); + expect(pool).toBeGreaterThan(-1); + expect(gate).toBeLessThan(pool); + }); + + test("skip is silent and permanent — never means never", () => { + expect(src).toContain("skip this entire section silently"); + expect(src).toContain("gstack-config set founder_resources true"); + }); + + test("the opt-out write is verified before any promise (R6)", () => { + expect(src).toContain("VERIFY the write"); + expect(src).toMatch(/read back\s*\n?`false`/); + }); +});