6.3 KiB
Fix #1671: /office-hours always reports SESSION_COUNT: 0
Status: SHIPPED
Branch: fix-1671-profile-migration
Date: 2026-05-23
Issue: https://github.com/garrytan/gstack/issues/1671
Original PR that introduced the bug: garrytan/gstack#1039 / commit 0a803f9 / v1.0.0.0 / 2026-04-18
The problem
/office-hours reports SESSION_COUNT: 0 and TIER: introduction on every invocation, even for users who have run the skill many times. The welcome_back tier (bin/gstack-developer-profile:165-169) that exists to skip the closing pitch for returning users is unreachable. Live ~5 weeks on every fresh-$HOME user since v1.0.0.0.
Root cause
The v1.0.0.0 migration moved the read path to ~/.gstack/developer-profile.json but left the writer in office-hours/SKILL.md.tmpl writing to the legacy ~/.gstack/builder-profile.jsonl. The ensure_profile stub created on first read has sessions: []; subsequent writes go to a file the reader never re-reads. Reader and writer disagree on storage.
Full root-cause analysis (including RC2/RC3 follow-ups): https://github.com/garrytan/gstack/issues/1671
The fix
Make the writer use the same file the reader does.
Changes
-
bin/gstack-developer-profile— add--log-session '<json>'subcommand:- Validates required fields (
date,mode), silent-skip on invalid input (matchesbin/gstack-timeline-log:22-26). - Reads existing
developer-profile.jsonviabun -e. - Appends entry to
sessions[]. Updatessignals_accumulated(per-signal-string increment, same asdo_migrate:67-69), unionsresources_shownandtopics. - Atomic mktemp+mv write (matches existing pattern at line 54).
- Calls
gstack-brain-enqueue "developer-profile.json"after write, mirroringbin/gstack-timeline-log:40.
- Validates required fields (
-
bin/gstack-developer-profile:do_read— filtermode:"resources"entries when picking LAST_PROJECT / LAST_ASSIGNMENT / LAST_DESIGN_TITLE / CROSS_PROJECT / DESIGN_*. The Phase 6 resources auto-append happens after the real session in the same /office-hours invocation; without the filter, that resources entry clobbers real-session state for the user's next session. Latent bug that was masked by the broken writer; activated by the fix. -
office-hours/SKILL.md.tmpl— swap writers at lines 490 and 893:- From:
echo '{...}' >> "$GSTACK_STATE_ROOT/builder-profile.jsonl" - To:
~/.claude/skills/gstack/bin/gstack-developer-profile --log-session '{...}' 2>/dev/null || true - Run
bun run gen:skill-docsto regenerateoffice-hours/SKILL.md.
- From:
What's NOT in the fix (intentionally)
- No new binary. The owner binary for
developer-profile.jsonisgstack-developer-profile; the writer belongs there as a subcommand.--log-sessionjoins the binary's existing--migrate/--derivewrite-side subcommand boundary, not thegstack-*-logevent-writer family. Verb name still matchesgstack-*-log. - No mkdir-locks. Concurrent /office-hours calls have a read-modify-write race on
developer-profile.json. The codebase accepts the same race ingstack-config(r-m-w on YAML, no lock). Not introduced by this fix; out of scope. - No schema bump. Schema stays at
schema_version: 1. The fix doesn't change the schema, just makes the writer use it. - No auto-reconcile for affected users. Existing users with stranded
builder-profile.jsonlentries don't get their past history auto-merged intodeveloper-profile.json. On their next /office-hours run, the first new session lands inwelcome_back; past data stays in the legacy file (still readable by other tools during deprecation). Most affected users have only a handful of stranded sessions so the loss is mostly aesthetic. Dropped the one-release-only reconcile pathway as net noise — Garry's "right-sized diff" voice. - No autoplan timeline rollup (RC2). Separate concern, separate PR.
- No project-scope opt-in (RC3). Separate concern, separate PR.
- No gbrain glob change. The office-hours manifest still globs
~/.gstack/builder-profile.jsonlfor context; once new writes stop landing there, the snapshot goes cold. Update in a follow-up if it becomes a UX issue.
Tests (all gate-tier, free, deterministic)
-
Regression test in
test/gstack-developer-profile.test.ts:- Fresh
$HOME. - Run /office-hours preamble: gstack-developer-profile creates empty stub.
- Call
--log-sessionwith a startup-mode JSON. - Run
--readagain. AssertSESSION_COUNT: 1,TIER: welcome_back. - Fails on current main (subcommand doesn't exist). Passes with fix.
- Fresh
-
do_readmode filter test: after recording a startup session followed by a resources entry,--readreturns LAST_PROJECT / LAST_ASSIGNMENT / LAST_DESIGN_TITLE from the real session, not from the resources entry. RESOURCES_SHOWN still aggregates correctly. -
Validation + aggregation tests:
--log-sessionsilently skips invalid JSON / missing required fields, injectstsif missing, preserves user-setts, correctly aggregates signals/resources/topics across multiple sessions. -
Static-grep invariant in
test/static-no-legacy-writes.test.ts(new): walks every skill dir, asserts no production code path writes tobuilder-profile.jsonlexcept allowlisted readers (gstack-developer-profile,gstack-memory-ingest.ts,gstack-artifacts-init, doc files). Prevents future writers from regressing onto the legacy file.
Acceptance criteria
- Second
/office-hoursinvocation on a fresh$HOMEreturnsTIER: welcome_back. bun testpasses on the touched files in isolation.bun run gen:skill-docsproduces clean diff matching the.tmpledits.
Rollout
- One commit. PATCH version bump per CHANGELOG style guide.
- CHANGELOG entry written by
/ship. User-facing voice: lead with what users experience now that they didn't before (welcome_back tier kicks in on second visit).
Follow-up TODOs
- Deprecate
builder-profile.jsonlentirely (writer + shim + memory-ingest type) after one release. - Fix RC2 (autoplan inlines sub-skills, bypassing their timeline-log preambles).
- Add
GSTACK_PROFILE_SCOPEopt-in for power users with multiple agent identities (RC3). - /plan-tune doesn't currently call
--derive, soinferred/gapcan drift (pre-existing, unrelated to #1671). mode:"resources"entries inflate SESSION_COUNT under the existing tier aggregator (pre-existing, unrelated to #1671 root cause).