Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dbd7aee

Browse files
garrytanclaude
andauthored
feat: relationship closing — office-hours adapts to repeat users (v0.16.2.0) (garrytan#937)
* fix: sync package.json version with VERSION file package.json was 0.15.15.0 while VERSION was 0.15.16.0, causing gen-skill-docs freshness check test failures. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add builder profile helper for office-hours relationship closing New bin/gstack-builder-profile reads ~/.gstack/builder-profile.jsonl and outputs structured summary (tier, signals, resources, topics). Single source of truth for all closing state — no separate config keys or logs. Uses bun-based JSONL parsing pattern from gstack-learnings-search. Graceful fallback to introduction tier if bun unavailable or file missing. 26 unit tests covering tier computation, signal accumulation, cross-project detection, nudge eligibility, resource dedup, and malformed JSONL handling. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: relationship closing — office-hours adapts to repeat users The office-hours closing now deepens over time instead of repeating the same YC plea every session. Four tiers based on session count: - Introduction (session 1): full YC plea + founder resources - Welcome Back (sessions 2-3): lead with recognition, skip plea - Regular (sessions 4-7): arc-level callbacks, signal visibility, builder-to-founder nudge, auto-generated journey summary - Inner Circle (sessions 8+): the data speaks Key design decisions (from CEO + Eng + Codex + DX reviews): - Single source of truth: one builder-profile.jsonl, no split-brain state - Lead with recognition on repeat visits (DX: magical moment hits immediately) - Narrative arc journey summary, not data tables - Tone examples per tier to prevent generic AI voice - Global resource dedup (low-sensitivity video watch history) - Migration merges per-project resource logs into builder profile Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: bump version and changelog (v0.16.2.0) Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent a7593d7 commit dbd7aee

8 files changed

Lines changed: 853 additions & 98 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [0.16.2.0] - 2026-04-09
4+
5+
### Added
6+
- **Office hours now remembers you.** The closing experience adapts based on how many sessions you've done. First time: full YC plea and founder resources. Sessions 2-3: "Welcome back. Last time you were working on [your project]. How's it going?" Sessions 4-7: arc-level callbacks across your whole journey, accumulated signal visibility, and an auto-generated Builder Journey narrative. Sessions 8+: the data speaks for itself.
7+
- **Builder profile** tracks your office hours journey in a single append-only session log. Signals, design docs, assignments, topics, and resources shown, all in one file. No split-brain state, no separate config keys.
8+
- **Builder-to-founder nudge** for repeat builder-mode users who accumulate founder signals. Evidence-gated: only triggers when you've shown 5+ signals across 3+ builder sessions. Not a pitch. An observation.
9+
- **Journey-matched resources.** Instead of category-matching from the static pool, resources now match your accumulated session context. "You've been iterating on a fintech idea for 3 sessions... Tom Blomfield built Monzo from exactly this kind of persistence."
10+
- **Builder Journey Summary** auto-generates at session 5+ and opens in your browser. A narrative arc of your journey, not a data table. Written in second person, referencing specific things you said across sessions.
11+
- **Global resource dedup.** Resource links now dedup globally (not per-project), so switching repos doesn't reset your watch history. Each link shows only once, ever.
12+
13+
### Fixed
14+
- package.json version now stays in sync with VERSION file.
15+
316
## [0.16.1.0] - 2026-04-08
417

518
### Fixed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.16.1.0
1+
0.16.2.0

bin/gstack-builder-profile

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env bash
2+
# gstack-builder-profile — read builder profile and output structured summary
3+
#
4+
# Reads ~/.gstack/builder-profile.jsonl (append-only session log from /office-hours).
5+
# Outputs KEY: VALUE pairs for the template to consume. Computes tier, accumulated
6+
# signals, cross-project detection, nudge eligibility, and resource dedup.
7+
#
8+
# Single source of truth for all closing state. No separate config keys or logs.
9+
#
10+
# Exit 0 with defaults if no profile exists (first-time user = introduction tier).
11+
set -euo pipefail
12+
13+
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
14+
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"
15+
16+
# Graceful default: no profile = introduction tier
17+
if [ ! -f "$PROFILE_FILE" ] || [ ! -s "$PROFILE_FILE" ]; then
18+
echo "SESSION_COUNT: 0"
19+
echo "TIER: introduction"
20+
echo "LAST_PROJECT:"
21+
echo "LAST_ASSIGNMENT:"
22+
echo "LAST_DESIGN_TITLE:"
23+
echo "DESIGN_COUNT: 0"
24+
echo "DESIGN_TITLES: []"
25+
echo "ACCUMULATED_SIGNALS:"
26+
echo "TOTAL_SIGNAL_COUNT: 0"
27+
echo "CROSS_PROJECT: false"
28+
echo "NUDGE_ELIGIBLE: false"
29+
echo "RESOURCES_SHOWN:"
30+
echo "RESOURCES_SHOWN_COUNT: 0"
31+
echo "TOPICS:"
32+
exit 0
33+
fi
34+
35+
# Use bun for JSON parsing (same pattern as gstack-learnings-search).
36+
# Fallback to defaults if bun is unavailable.
37+
cat "$PROFILE_FILE" 2>/dev/null | bun -e "
38+
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
39+
const entries = [];
40+
for (const line of lines) {
41+
try { entries.push(JSON.parse(line)); } catch {}
42+
}
43+
44+
const count = entries.length;
45+
46+
// Tier computation
47+
let tier = 'introduction';
48+
if (count >= 8) tier = 'inner_circle';
49+
else if (count >= 4) tier = 'regular';
50+
else if (count >= 1) tier = 'welcome_back';
51+
52+
// Last session data
53+
const last = entries[count - 1] || {};
54+
const prev = entries[count - 2] || {};
55+
const crossProject = prev.project_slug && last.project_slug
56+
? prev.project_slug !== last.project_slug
57+
: false;
58+
59+
// Design docs
60+
const designs = entries
61+
.map(e => e.design_doc || '')
62+
.filter(Boolean);
63+
const designTitles = entries
64+
.map(e => {
65+
const doc = e.design_doc || '';
66+
// Extract title from path: ...-design-DATETIME.md -> use the entry's topic or project
67+
return doc ? (e.project_slug || 'unknown') : '';
68+
})
69+
.filter(Boolean);
70+
71+
// Accumulated signals
72+
const signalCounts = {};
73+
let totalSignals = 0;
74+
for (const e of entries) {
75+
for (const s of (e.signals || [])) {
76+
signalCounts[s] = (signalCounts[s] || 0) + 1;
77+
totalSignals++;
78+
}
79+
}
80+
const signalStr = Object.entries(signalCounts)
81+
.map(([k, v]) => k + ':' + v)
82+
.join(',');
83+
84+
// Nudge eligibility: builder-mode + 5+ signals across 3+ sessions
85+
const builderSessions = entries.filter(e => e.mode !== 'startup').length;
86+
const nudgeEligible = builderSessions >= 3 && totalSignals >= 5;
87+
88+
// Resources shown (aggregate all)
89+
const allResources = new Set();
90+
for (const e of entries) {
91+
for (const url of (e.resources_shown || [])) {
92+
allResources.add(url);
93+
}
94+
}
95+
96+
// Topics (aggregate all)
97+
const allTopics = new Set();
98+
for (const e of entries) {
99+
for (const t of (e.topics || [])) {
100+
allTopics.add(t);
101+
}
102+
}
103+
104+
console.log('SESSION_COUNT: ' + count);
105+
console.log('TIER: ' + tier);
106+
console.log('LAST_PROJECT: ' + (last.project_slug || ''));
107+
console.log('LAST_ASSIGNMENT: ' + (last.assignment || ''));
108+
console.log('LAST_DESIGN_TITLE: ' + (last.design_doc || ''));
109+
console.log('DESIGN_COUNT: ' + designs.length);
110+
console.log('DESIGN_TITLES: ' + JSON.stringify(designTitles));
111+
console.log('ACCUMULATED_SIGNALS: ' + signalStr);
112+
console.log('TOTAL_SIGNAL_COUNT: ' + totalSignals);
113+
console.log('CROSS_PROJECT: ' + crossProject);
114+
console.log('NUDGE_ELIGIBLE: ' + nudgeEligible);
115+
console.log('RESOURCES_SHOWN: ' + Array.from(allResources).join(','));
116+
console.log('RESOURCES_SHOWN_COUNT: ' + allResources.size);
117+
console.log('TOPICS: ' + Array.from(allTopics).join(','));
118+
" 2>/dev/null || {
119+
# Fallback if bun is unavailable
120+
echo "SESSION_COUNT: 0"
121+
echo "TIER: introduction"
122+
echo "LAST_PROJECT:"
123+
echo "LAST_ASSIGNMENT:"
124+
echo "LAST_DESIGN_TITLE:"
125+
echo "DESIGN_COUNT: 0"
126+
echo "DESIGN_TITLES: []"
127+
echo "ACCUMULATED_SIGNALS:"
128+
echo "TOTAL_SIGNAL_COUNT: 0"
129+
echo "CROSS_PROJECT: false"
130+
echo "NUDGE_ELIGIBLE: false"
131+
echo "RESOURCES_SHOWN:"
132+
echo "RESOURCES_SHOWN_COUNT: 0"
133+
echo "TOPICS:"
134+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
# Migration: v0.16.2.0 — Merge per-project resource logs into builder profile
3+
#
4+
# What changed: resource dedup moved from per-project resources-shown.jsonl to
5+
# the global builder-profile.jsonl (single source of truth for all closing state).
6+
#
7+
# What this does: finds all per-project resources-shown.jsonl files and merges
8+
# their URLs into a stub builder-profile entry so existing users don't lose
9+
# their dedup history. Idempotent — safe to run multiple times.
10+
#
11+
# Affected: users who ran /office-hours before this version
12+
set -euo pipefail
13+
14+
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
15+
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"
16+
17+
# Find all per-project resource logs
18+
RESOURCE_FILES=$(find "$GSTACK_HOME/projects" -name "resources-shown.jsonl" 2>/dev/null || true)
19+
20+
if [ -z "$RESOURCE_FILES" ]; then
21+
# No per-project resource files exist — clean install, nothing to migrate
22+
exit 0
23+
fi
24+
25+
echo " [v0.16.2.0] Migrating per-project resource logs to builder profile..."
26+
27+
# Collect all unique URLs from all per-project files
28+
ALL_URLS=$(echo "$RESOURCE_FILES" | while read -r f; do
29+
[ -f "$f" ] && cat "$f" 2>/dev/null || true
30+
done | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//' | sort -u)
31+
32+
if [ -z "$ALL_URLS" ]; then
33+
exit 0
34+
fi
35+
36+
# Check if builder-profile already has resource data (idempotency)
37+
if [ -f "$PROFILE_FILE" ] && grep -q "resources_shown" "$PROFILE_FILE" 2>/dev/null; then
38+
# Already has resource data, check if it includes the migrated URLs
39+
EXISTING_URLS=$(grep -o '"resources_shown":\[[^]]*\]' "$PROFILE_FILE" 2>/dev/null | grep -o 'https://[^"]*' | sort -u)
40+
NEW_URLS=$(comm -23 <(echo "$ALL_URLS") <(echo "$EXISTING_URLS") 2>/dev/null || echo "$ALL_URLS")
41+
if [ -z "$NEW_URLS" ]; then
42+
# All URLs already present — nothing to do
43+
exit 0
44+
fi
45+
fi
46+
47+
# Build JSON array of URLs
48+
URL_ARRAY=$(echo "$ALL_URLS" | awk 'BEGIN{printf "["} NR>1{printf ","} {printf "\"%s\"", $0} END{printf "]"}')
49+
50+
# Append a migration stub entry to the builder profile
51+
mkdir -p "$GSTACK_HOME"
52+
echo "{\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"mode\":\"migration\",\"project_slug\":\"_migrated\",\"signal_count\":0,\"signals\":[],\"design_doc\":\"\",\"assignment\":\"\",\"resources_shown\":$URL_ARRAY,\"topics\":[]}" >> "$PROFILE_FILE"
53+
54+
echo " [v0.16.2.0] Migrated $(echo "$ALL_URLS" | wc -l | tr -d ' ') resource URLs to builder profile."

0 commit comments

Comments
 (0)