From 87e6ce00d595bd0a9ff42576e60f17050abe5d00 Mon Sep 17 00:00:00 2001 From: Michael Vann <9221873+mvann@users.noreply.github.com> Date: Sun, 31 May 2026 13:34:46 -0700 Subject: [PATCH 1/3] fix(developer-profile): exclude mode:resources entries from SESSION_COUNT, TIER, and NUDGE_ELIGIBLE The /office-hours Phase 6 closing auto-appends one or more mode:resources bookkeeping entries every run (to dedupe which founder-resource links the user has been shown). The do_read path counted these as real sessions: - SESSION_COUNT (and therefore TIER) used sessions.length with no filter - builderSessions used `e.mode !== 'startup'`, which both counted resources entries and excluded legitimate startup-mode sessions Result: normal repeated use silently inflated the tier and could arm the builder->founder nudge from bookkeeping alone (e.g. one real session plus a few closings reporting as TIER: regular with NUDGE_ELIGIBLE: true). Fix reuses the existing `realSessions` filter (already used for LAST_*/ CROSS_PROJECT, see line ~238) for the count, and scopes builderSessions to mode === 'builder' to match the nudge's intent (returning *builders*, not startup diagnostics). Adds 4 Tier-1 regression tests. Closes the count/tier/nudge half of the resources-entry hazard that the LAST_* fix (PR #1676, #1671) addressed for project/assignment fields. --- bin/gstack-developer-profile | 22 +++++--- test/gstack-developer-profile.test.ts | 74 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/bin/gstack-developer-profile b/bin/gstack-developer-profile index a5721a9c5..1b0594630 100755 --- a/bin/gstack-developer-profile +++ b/bin/gstack-developer-profile @@ -225,17 +225,19 @@ do_read() { cat "$PROFILE_FILE" | bun -e " const p = JSON.parse(await Bun.stdin.text()); const sessions = p.sessions || []; - const count = sessions.length; + + // SESSION_COUNT / TIER / CROSS_PROJECT / NUDGE must reflect real sessions, not + // resource-tracking events (the Phase 6 auto-append). Without this filter, a + // session's resources entry written immediately after the real session inflates + // the count (bumping TIER), clobbers LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE, + // and pushes NUDGE_ELIGIBLE over its threshold from bookkeeping alone. + const realSessions = sessions.filter(e => e.mode !== 'resources'); + + const count = realSessions.length; let tier = 'introduction'; if (count >= 8) tier = 'inner_circle'; else if (count >= 4) tier = 'regular'; else if (count >= 1) tier = 'welcome_back'; - - // LAST_* / CROSS_PROJECT must reflect real sessions, not resource-tracking - // events (the Phase 6 auto-append). Without this filter, a session's - // resources entry written immediately after the real session would clobber - // LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE. - const realSessions = sessions.filter(e => e.mode !== 'resources'); const last = realSessions[realSessions.length - 1] || {}; const prev = realSessions[realSessions.length - 2] || {}; const crossProject = prev.project_slug && last.project_slug @@ -252,7 +254,11 @@ do_read() { for (const v of Object.values(signalCounts)) totalSignals += v; const signalStr = Object.entries(signalCounts).map(([k,v]) => k + ':' + v).join(','); - const builderSessions = sessions.filter(e => e.mode !== 'startup').length; + // Builder-mode design sessions only — the builder->founder nudge is about + // someone who keeps returning to *build*, not startup-mode diagnostics and not + // resources bookkeeping. (Was \`e.mode !== 'startup'\`, which counted resources + // entries and excluded real startup sessions.) + const builderSessions = realSessions.filter(e => e.mode === 'builder').length; const nudgeEligible = builderSessions >= 3 && totalSignals >= 5; const resources = p.resources_shown || []; diff --git a/test/gstack-developer-profile.test.ts b/test/gstack-developer-profile.test.ts index ed683bf34..ffd9afa10 100644 --- a/test/gstack-developer-profile.test.ts +++ b/test/gstack-developer-profile.test.ts @@ -556,3 +556,77 @@ describe('gstack-developer-profile --log-session (#1671 fix)', () => { }); }); +// ----------------------------------------------------------------------- +// SESSION_COUNT / TIER / NUDGE_ELIGIBLE must ignore mode:resources entries. +// +// Phase 6 of /office-hours auto-appends one (or more) mode:resources bookkeeping +// entries every run, to dedupe which founder-resource links the user has seen. +// Those are not sessions. Counting them inflated SESSION_COUNT (and therefore +// TIER) and pushed NUDGE_ELIGIBLE over its threshold from bookkeeping alone — +// e.g. a single real session plus three closings reported as tier `regular` +// with the builder->founder nudge armed. +// ----------------------------------------------------------------------- + +describe('gstack-developer-profile resources entries do not inflate count/tier/nudge', () => { + function logStartup(extra: Record = {}) { + return runDev('--log-session', JSON.stringify({ + date: '2026-05-20T00:00:00Z', mode: 'startup', project_slug: 'p', + signal_count: 5, signals: ['a', 'b', 'c', 'd', 'e'], ...extra, + })); + } + function logResources(i: number) { + return runDev('--log-session', JSON.stringify({ + date: '2026-05-20T01:00:00Z', mode: 'resources', project_slug: 'p', + resources_shown: [`url${i}`], + })); + } + + test('SESSION_COUNT counts only real sessions, not resources entries', () => { + logStartup(); + logResources(1); + logResources(2); + logResources(3); + const r = runDev('--read'); + expect(r.stdout).toContain('SESSION_COUNT: 1'); + expect(r.stdout).toContain('TIER: welcome_back'); + }); + + test('TIER is not bumped to regular by resources bookkeeping', () => { + // 3 real sessions = welcome_back; adding resources entries must not reach the + // 4-session `regular` threshold. + logStartup(); + logStartup(); + logStartup(); + for (let i = 0; i < 4; i++) logResources(i); + const r = runDev('--read'); + expect(r.stdout).toContain('SESSION_COUNT: 3'); + expect(r.stdout).toContain('TIER: welcome_back'); + }); + + test('NUDGE_ELIGIBLE stays false when builder-session bar is unmet despite resources noise', () => { + // One startup session carrying 5 signals, plus resources entries. builderSessions + // (mode === "builder") is 0, so the nudge must not arm regardless of signal count. + logStartup(); + logResources(1); + logResources(2); + logResources(3); + const r = runDev('--read'); + expect(r.stdout).toContain('NUDGE_ELIGIBLE: false'); + }); + + test('NUDGE_ELIGIBLE arms on 3 real builder sessions with enough signals', () => { + runDev('--log-session', JSON.stringify({ + date: '2026-05-20T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['a', 'b'], + })); + runDev('--log-session', JSON.stringify({ + date: '2026-05-21T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['c', 'd'], + })); + runDev('--log-session', JSON.stringify({ + date: '2026-05-22T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['e'], + })); + logResources(1); // bookkeeping must not change the verdict either way + const r = runDev('--read'); + expect(r.stdout).toContain('NUDGE_ELIGIBLE: true'); + }); +}); + From dd544d663f0951b32f1b5727f089a761fddb9d74 Mon Sep 17 00:00:00 2001 From: Michael Vann <9221873+mvann@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:49:58 -0700 Subject: [PATCH 2/3] test(developer-profile): add gate boundary cases for nudge and tier --- test/gstack-developer-profile.test.ts | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/gstack-developer-profile.test.ts b/test/gstack-developer-profile.test.ts index ffd9afa10..507706509 100644 --- a/test/gstack-developer-profile.test.ts +++ b/test/gstack-developer-profile.test.ts @@ -628,5 +628,71 @@ describe('gstack-developer-profile resources entries do not inflate count/tier/n const r = runDev('--read'); expect(r.stdout).toContain('NUDGE_ELIGIBLE: true'); }); + + // Boundary cases around the two `>=` gates, so a future >= → > regression + // (or a re-loosening of the builder filter) is caught, not just the happy path. + function logBuilder(signals: string[], day = 20) { + return runDev('--log-session', JSON.stringify({ + date: `2026-05-${day}T00:00:00Z`, mode: 'builder', project_slug: 'p', signals, + })); + } + + test('NUDGE_ELIGIBLE stays false at 2 builder sessions (below the 3-session gate)', () => { + logBuilder(['a', 'b', 'c'], 20); + logBuilder(['d', 'e', 'f'], 21); // 6 signals total — signal gate met, session gate is not + logResources(1); + const r = runDev('--read'); + expect(r.stdout).toContain('NUDGE_ELIGIBLE: false'); + }); + + test('NUDGE_ELIGIBLE stays false at 3 builder sessions with too few signals', () => { + logBuilder(['a'], 20); + logBuilder(['b'], 21); + logBuilder(['c', 'd'], 22); // 4 signals total — session gate met, signal gate (>=5) is not + const r = runDev('--read'); + expect(r.stdout).toContain('NUDGE_ELIGIBLE: false'); + }); + + test('TIER reaches regular at 4 real sessions even when resources entries are present', () => { + logStartup(); + logStartup(); + logStartup(); + logStartup(); + for (let i = 0; i < 5; i++) logResources(i); + const r = runDev('--read'); + expect(r.stdout).toContain('SESSION_COUNT: 4'); + expect(r.stdout).toContain('TIER: regular'); + }); + + test('TIER stays regular at 7 real sessions and crosses to inner_circle at 8 (resources ignored)', () => { + // Upper-tier boundary: the >=8 inner_circle gate must key off real sessions + // only, so a pile of resources bookkeeping can never tip a regular into the + // inner circle, and 8 genuine sessions still reach it. + for (let i = 0; i < 7; i++) logStartup(); + for (let i = 0; i < 6; i++) logResources(i); // 13 raw rows; pre-fix would read inner_circle + let r = runDev('--read'); + expect(r.stdout).toContain('SESSION_COUNT: 7'); + expect(r.stdout).toContain('TIER: regular'); + + logStartup(); // 8th real session + r = runDev('--read'); + expect(r.stdout).toContain('SESSION_COUNT: 8'); + expect(r.stdout).toContain('TIER: inner_circle'); + }); + + test('CROSS_PROJECT ignores a trailing resources entry on a different project', () => { + // The last two REAL sessions are the same project, so CROSS_PROJECT is false. + // A trailing resources row carrying a different project_slug must not become + // the `last` entry and flip CROSS_PROJECT true off bookkeeping. + logStartup({ project_slug: 'samep' }); + logStartup({ project_slug: 'samep' }); + runDev('--log-session', JSON.stringify({ + date: '2026-05-20T02:00:00Z', mode: 'resources', project_slug: 'otherp', + resources_shown: ['url1'], + })); + const r = runDev('--read'); + expect(r.stdout).toContain('CROSS_PROJECT: false'); + expect(r.stdout).toContain('LAST_PROJECT: samep'); + }); }); From 0f04559cd954d3b4fb5e1adb25765ad01c8cd6e3 Mon Sep 17 00:00:00 2001 From: Michael Vann <9221873+mvann@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:49:58 -0700 Subject: [PATCH 3/3] chore: bump version and changelog (v1.60.2.0) Claude-Session: https://claude.ai/code/session_01U5pj5ao1pJbmTfVRz1nQzR --- CHANGELOG.md | 18 ++++++++++++------ VERSION | 2 +- package.json | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351e7cabd..8c2de4352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.60.2.0] - 2026-07-15 + +## **Your builder profile's session count, tier, and the builder→founder nudge now reflect only real design sessions — the resource-tracking bookkeeping that office-hours writes after each session no longer inflates them.** + +After every office-hours session, gstack appends a `mode:resources` entry to your developer profile to track which founder resources it surfaced. `gstack-developer-profile read` was counting those bookkeeping entries as if they were real sessions: they padded `SESSION_COUNT`, prematurely promoted your `TIER` (introduction → welcome_back → regular → inner_circle), and helped arm `NUDGE_ELIGIBLE`. The builder→founder nudge could fire off bookkeeping alone. Now `SESSION_COUNT`, `TIER`, cross-project counts, and `NUDGE_ELIGIBLE` all filter to real sessions, and the nudge counts only `mode:builder` design sessions — so it triggers on someone who keeps coming back to *build*, which is what it was always meant to measure. + +### Fixed + +- `gstack-developer-profile read` no longer counts `mode:resources` entries toward `SESSION_COUNT` or `TIER`. +- `NUDGE_ELIGIBLE` now requires 3+ real `mode:builder` sessions (was: any non-`startup` entry, which counted resources and excluded startup sessions), so it can no longer be armed by bookkeeping alone. +- Added 4 regression tests covering count, tier, and nudge behavior in the presence of resource entries. + ## [1.60.1.0] - 2026-07-09 ## **The /autoplan dual-voice eval is back on the board, catching real regressions.** @@ -74,7 +86,6 @@ If you just installed gstack, your first session points you at something useful - New unit coverage for every detection bucket plus the eval-safe enum contract and the first-run gating (`test/preamble-first-task-scaffold.test.ts`), and a periodic E2E that runs the detector through the real harness (`test/skill-e2e-first-task-scaffold.test.ts`, classified `periodic`). - Browse-content test assertions (gen-skill-docs, audit-compliance, skill-validation, the LLM-judge eval) repointed from the root skill to `browse/SKILL.md` to follow the router split; a regression test pins that the router carries no browse body. - Parity / carve-guard size caps bumped ~1–2KB per skill to account for the shared first-run-guidance preamble section. - ## [1.58.4.0] - 2026-06-18 ## **A community bug-fix wave plus a test-gate that finally sees the questions it was missing.** @@ -123,7 +134,6 @@ If you run gbrain on a Supabase transaction-mode pooler, your writes work again. - The PTY model now pins to `EVALS_MODEL ?? claude-sonnet-4-6` (mirrors `session-runner.ts`), removing operator-model nondeterminism from the smokes. - Stochastic ask-first smokes (plan-eng/plan-design plan-mode + finding-floor) reclassified `periodic` per the non-deterministic-tests rule; the deterministic ones (office-hours, plan-mode-no-op) now run in CI via a new `e2e-pty-plan-smoke` matrix suite with an in-container skill-registry install step. - `redactFindingSpans()` is the machine-egress redaction entry point in `lib/redact-engine.ts`. - ## [1.58.3.0] - 2026-06-18 ## **GBrowser masks the full set of automation tells by default, on every path a page can reach.** @@ -162,7 +172,6 @@ If you drive GBrowser to dogfood, scrape, or QA against anti-bot-protected targe - `GSTACK_STEALTH=extended` layers on top of Layer C; the always-on default does not fake `navigator.plugins` (the opt-in mode still does, as the documented "may break sites" escape hatch). - `--gstack-suppress-prepare-stack-trace` is opt-in via `GSTACK_CDP_STEALTH=on`, so the switch never reaches a Chromium that does not understand it. - `--disable-blink-features=AutomationControlled` comes from one shared `STEALTH_LAUNCH_ARGS` constant across every launch path. - ## [1.58.1.0] - 2026-06-14 ## **Local evals stop lying. Spawned `claude` test children run in a sealed clean room,** @@ -264,7 +273,6 @@ real `~/.claude` in the loop. agent executes instead of a half-document. - ios-qa daemon scenarios use unique pidfiles, fixing `already_running` collisions under `bun test --concurrent`. - ## [1.58.0.0] - 2026-06-12 ## **Your documents grow diagrams. Mermaid and excalidraw fences render as real pictures,** @@ -370,7 +378,6 @@ pasting screenshots of diagrams into documents. Run `/diagram` for the picture, committed bundle is pinned to LF in .gitattributes. - Fixed the `operational-learning` E2E fixture (bin scripts now ship with the lib module they import). - ## [1.57.10.0] - 2026-06-10 ## **Codex review now runs by default everywhere it matters.** @@ -1187,7 +1194,6 @@ Your telemetry choice screen now describes what actually happens, so you can opt #### For contributors - The consent copy and repo-basename handling live in `scripts/resolvers/preamble/`; all `SKILL.md` files and the ship goldens were regenerated from those resolvers. - ## [1.55.0.0] - 2026-05-30 ## **`/sync-gbrain` can no longer be the trigger that lets gbrain delete your repo. The headed browser stops crash-looping, and gbrain installs the current release instead of a pin 23 versions stale.** diff --git a/VERSION b/VERSION index c4190e004..762f17554 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.60.1.0 +1.60.2.0 diff --git a/package.json b/package.json index 846438a60..78b98a86f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.60.1.0", + "version": "1.60.2.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module",