gstack-developer-profile --read double-counts mode:"resources" bookkeeping rows into SESSION_COUNT, TIER, and NUDGE_ELIGIBLE
bin/gstack-developer-profile derives a builder's SESSION_COUNT, TIER, and NUDGE_ELIGIBLE from the sessions[] array in ~/.gstack/developer-profile.json. But /office-hours appends two rows per run: the real session row (mode: "startup" or "builder"), and a separate mode: "resources" bookkeeping row recorded after resources are surfaced (office-hours/SKILL.md:1620, written via --log-session). The read path counts both.
This was masked while #1677 was live (the writer wrote to the legacy builder-profile.jsonl the reader never re-read, so SESSION_COUNT was stuck at 0). Now that the profile is unified onto developer-profile.json and sessions actually persist, the resources rows are read — so the bug is live: counts read roughly 2x real sessions.
Root cause (bin/gstack-developer-profile, current main)
The file already knows resources rows aren't sessions. Lines 236-238 filter them for LAST_*/CROSS_PROJECT, with a comment naming them "the Phase 6 auto-append":
const realSessions = sessions.filter(e => e.mode !== 'resources');
But that filter is applied to only 2 of the 4 consumers. The other two still read raw sessions:
- Line 228:
const count = sessions.length; → SESSION_COUNT and TIER (thresholds >=8 inner_circle, >=4 regular, >=1 welcome_back) are inflated.
- Line 255:
const builderSessions = sessions.filter(e => e.mode !== 'startup').length; → doubly wrong: it counts resources rows (they're !== 'startup') and excludes real startup sessions. Feeds NUDGE_ELIGIBLE (line 256: builderSessions >= 3 && totalSignals >= 5), so the builder→founder nudge can arm off bookkeeping alone.
Repro
Fresh $HOME. Run /office-hours to completion 4 times as real design sessions.
- Each run appends 1 session row + 1
mode:"resources" row → sessions.length == 8.
gstack-developer-profile --read reports SESSION_COUNT: 8 / TIER: inner_circle.
- Truth: 4 real sessions, which should be
TIER: regular.
Two real sessions already report SESSION_COUNT: 4 / regular when the user is actually at welcome_back. The founder nudge can fire after as few as 3 runs on bookkeeping rows rather than 3 real builder sessions.
Impact
Low-severity (greeting tier and the builder→founder nudge are personalization/tone, not crash or data loss), but it fires on the default happy path for every /office-hours user now that sessions persist: wrong-tier greeting and a premature "have you thought about whether this could be a company?" nudge.
Fix
Hoist the existing realSessions filter above count, and make the nudge count real mode:"builder" sessions:
count = realSessions.length → SESSION_COUNT / TIER / CROSS_PROJECT / designs all key off real sessions.
builderSessions = realSessions.filter(e => e.mode === 'builder').length → nudge measures repeat builders, which is what it was meant to measure.
Write path untouched; this is a read-only aggregation change. A PR with this fix plus regression tests (count, tier both-sides-of-gate boundaries, cross-project, and nudge in the presence of resources rows) is up at #1991.
Relationship to #1677
Distinct bug, sequential. #1677 was a write-path split-brain (SESSION_COUNT always 0 because writes went to a file the reader never read). Unifying the profile fixed that and exposed this one: now that rows persist and get read, the mode:"resources" rows get over-counted. #1677 = sessions don't persist; this = persisted sessions are over-counted.
gstack-developer-profile --readdouble-countsmode:"resources"bookkeeping rows into SESSION_COUNT, TIER, and NUDGE_ELIGIBLEbin/gstack-developer-profilederives a builder'sSESSION_COUNT,TIER, andNUDGE_ELIGIBLEfrom thesessions[]array in~/.gstack/developer-profile.json. But/office-hoursappends two rows per run: the real session row (mode: "startup"or"builder"), and a separatemode: "resources"bookkeeping row recorded after resources are surfaced (office-hours/SKILL.md:1620, written via--log-session). The read path counts both.This was masked while #1677 was live (the writer wrote to the legacy
builder-profile.jsonlthe reader never re-read, soSESSION_COUNTwas stuck at 0). Now that the profile is unified ontodeveloper-profile.jsonand sessions actually persist, the resources rows are read — so the bug is live: counts read roughly 2x real sessions.Root cause (
bin/gstack-developer-profile, currentmain)The file already knows resources rows aren't sessions. Lines 236-238 filter them for
LAST_*/CROSS_PROJECT, with a comment naming them "the Phase 6 auto-append":But that filter is applied to only 2 of the 4 consumers. The other two still read raw
sessions:const count = sessions.length;→SESSION_COUNTandTIER(thresholds>=8inner_circle,>=4regular,>=1welcome_back) are inflated.const builderSessions = sessions.filter(e => e.mode !== 'startup').length;→ doubly wrong: it countsresourcesrows (they're!== 'startup') and excludes realstartupsessions. FeedsNUDGE_ELIGIBLE(line 256:builderSessions >= 3 && totalSignals >= 5), so the builder→founder nudge can arm off bookkeeping alone.Repro
Fresh
$HOME. Run/office-hoursto completion 4 times as real design sessions.mode:"resources"row →sessions.length == 8.gstack-developer-profile --readreportsSESSION_COUNT: 8/TIER: inner_circle.TIER: regular.Two real sessions already report
SESSION_COUNT: 4/regularwhen the user is actually atwelcome_back. The founder nudge can fire after as few as 3 runs on bookkeeping rows rather than 3 realbuildersessions.Impact
Low-severity (greeting tier and the builder→founder nudge are personalization/tone, not crash or data loss), but it fires on the default happy path for every
/office-hoursuser now that sessions persist: wrong-tier greeting and a premature "have you thought about whether this could be a company?" nudge.Fix
Hoist the existing
realSessionsfilter abovecount, and make the nudge count realmode:"builder"sessions:count = realSessions.length→ SESSION_COUNT / TIER / CROSS_PROJECT / designs all key off real sessions.builderSessions = realSessions.filter(e => e.mode === 'builder').length→ nudge measures repeat builders, which is what it was meant to measure.Write path untouched; this is a read-only aggregation change. A PR with this fix plus regression tests (count, tier both-sides-of-gate boundaries, cross-project, and nudge in the presence of resources rows) is up at #1991.
Relationship to #1677
Distinct bug, sequential. #1677 was a write-path split-brain (
SESSION_COUNTalways 0 because writes went to a file the reader never read). Unifying the profile fixed that and exposed this one: now that rows persist and get read, themode:"resources"rows get over-counted. #1677 = sessions don't persist; this = persisted sessions are over-counted.