Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## [0.16.2.0] - 2026-04-09

### Added
- **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.
- **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.
- **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.
- **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."
- **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.
- **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.

### Fixed
- package.json version now stays in sync with VERSION file.

## [0.16.1.0] - 2026-04-08

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.16.1.0
0.16.2.0
134 changes: 134 additions & 0 deletions bin/gstack-builder-profile
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# gstack-builder-profile — read builder profile and output structured summary
#
# Reads ~/.gstack/builder-profile.jsonl (append-only session log from /office-hours).
# Outputs KEY: VALUE pairs for the template to consume. Computes tier, accumulated
# signals, cross-project detection, nudge eligibility, and resource dedup.
#
# Single source of truth for all closing state. No separate config keys or logs.
#
# Exit 0 with defaults if no profile exists (first-time user = introduction tier).
set -euo pipefail

GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"

# Graceful default: no profile = introduction tier
if [ ! -f "$PROFILE_FILE" ] || [ ! -s "$PROFILE_FILE" ]; then
echo "SESSION_COUNT: 0"
echo "TIER: introduction"
echo "LAST_PROJECT:"
echo "LAST_ASSIGNMENT:"
echo "LAST_DESIGN_TITLE:"
echo "DESIGN_COUNT: 0"
echo "DESIGN_TITLES: []"
echo "ACCUMULATED_SIGNALS:"
echo "TOTAL_SIGNAL_COUNT: 0"
echo "CROSS_PROJECT: false"
echo "NUDGE_ELIGIBLE: false"
echo "RESOURCES_SHOWN:"
echo "RESOURCES_SHOWN_COUNT: 0"
echo "TOPICS:"
exit 0
fi

# Use bun for JSON parsing (same pattern as gstack-learnings-search).
# Fallback to defaults if bun is unavailable.
cat "$PROFILE_FILE" 2>/dev/null | bun -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const entries = [];
for (const line of lines) {
try { entries.push(JSON.parse(line)); } catch {}
}

const count = entries.length;

// Tier computation
let tier = 'introduction';
if (count >= 8) tier = 'inner_circle';
else if (count >= 4) tier = 'regular';
else if (count >= 1) tier = 'welcome_back';

// Last session data
const last = entries[count - 1] || {};
const prev = entries[count - 2] || {};
const crossProject = prev.project_slug && last.project_slug
? prev.project_slug !== last.project_slug
: false;

// Design docs
const designs = entries
.map(e => e.design_doc || '')
.filter(Boolean);
const designTitles = entries
.map(e => {
const doc = e.design_doc || '';
// Extract title from path: ...-design-DATETIME.md -> use the entry's topic or project
return doc ? (e.project_slug || 'unknown') : '';
})
.filter(Boolean);

// Accumulated signals
const signalCounts = {};
let totalSignals = 0;
for (const e of entries) {
for (const s of (e.signals || [])) {
signalCounts[s] = (signalCounts[s] || 0) + 1;
totalSignals++;
}
}
const signalStr = Object.entries(signalCounts)
.map(([k, v]) => k + ':' + v)
.join(',');

// Nudge eligibility: builder-mode + 5+ signals across 3+ sessions
const builderSessions = entries.filter(e => e.mode !== 'startup').length;
const nudgeEligible = builderSessions >= 3 && totalSignals >= 5;

// Resources shown (aggregate all)
const allResources = new Set();
for (const e of entries) {
for (const url of (e.resources_shown || [])) {
allResources.add(url);
}
}

// Topics (aggregate all)
const allTopics = new Set();
for (const e of entries) {
for (const t of (e.topics || [])) {
allTopics.add(t);
}
}

console.log('SESSION_COUNT: ' + count);
console.log('TIER: ' + tier);
console.log('LAST_PROJECT: ' + (last.project_slug || ''));
console.log('LAST_ASSIGNMENT: ' + (last.assignment || ''));
console.log('LAST_DESIGN_TITLE: ' + (last.design_doc || ''));
console.log('DESIGN_COUNT: ' + designs.length);
console.log('DESIGN_TITLES: ' + JSON.stringify(designTitles));
console.log('ACCUMULATED_SIGNALS: ' + signalStr);
console.log('TOTAL_SIGNAL_COUNT: ' + totalSignals);
console.log('CROSS_PROJECT: ' + crossProject);
console.log('NUDGE_ELIGIBLE: ' + nudgeEligible);
console.log('RESOURCES_SHOWN: ' + Array.from(allResources).join(','));
console.log('RESOURCES_SHOWN_COUNT: ' + allResources.size);
console.log('TOPICS: ' + Array.from(allTopics).join(','));
" 2>/dev/null || {
# Fallback if bun is unavailable
echo "SESSION_COUNT: 0"
echo "TIER: introduction"
echo "LAST_PROJECT:"
echo "LAST_ASSIGNMENT:"
echo "LAST_DESIGN_TITLE:"
echo "DESIGN_COUNT: 0"
echo "DESIGN_TITLES: []"
echo "ACCUMULATED_SIGNALS:"
echo "TOTAL_SIGNAL_COUNT: 0"
echo "CROSS_PROJECT: false"
echo "NUDGE_ELIGIBLE: false"
echo "RESOURCES_SHOWN:"
echo "RESOURCES_SHOWN_COUNT: 0"
echo "TOPICS:"
}
54 changes: 54 additions & 0 deletions gstack-upgrade/migrations/v0.16.2.0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Migration: v0.16.2.0 — Merge per-project resource logs into builder profile
#
# What changed: resource dedup moved from per-project resources-shown.jsonl to
# the global builder-profile.jsonl (single source of truth for all closing state).
#
# What this does: finds all per-project resources-shown.jsonl files and merges
# their URLs into a stub builder-profile entry so existing users don't lose
# their dedup history. Idempotent — safe to run multiple times.
#
# Affected: users who ran /office-hours before this version
set -euo pipefail

GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"

# Find all per-project resource logs
RESOURCE_FILES=$(find "$GSTACK_HOME/projects" -name "resources-shown.jsonl" 2>/dev/null || true)

if [ -z "$RESOURCE_FILES" ]; then
# No per-project resource files exist — clean install, nothing to migrate
exit 0
fi

echo " [v0.16.2.0] Migrating per-project resource logs to builder profile..."

# Collect all unique URLs from all per-project files
ALL_URLS=$(echo "$RESOURCE_FILES" | while read -r f; do
[ -f "$f" ] && cat "$f" 2>/dev/null || true
done | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//' | sort -u)

if [ -z "$ALL_URLS" ]; then
exit 0
fi

# Check if builder-profile already has resource data (idempotency)
if [ -f "$PROFILE_FILE" ] && grep -q "resources_shown" "$PROFILE_FILE" 2>/dev/null; then
# Already has resource data, check if it includes the migrated URLs
EXISTING_URLS=$(grep -o '"resources_shown":\[[^]]*\]' "$PROFILE_FILE" 2>/dev/null | grep -o 'https://[^"]*' | sort -u)
NEW_URLS=$(comm -23 <(echo "$ALL_URLS") <(echo "$EXISTING_URLS") 2>/dev/null || echo "$ALL_URLS")
if [ -z "$NEW_URLS" ]; then
# All URLs already present — nothing to do
exit 0
fi
fi

# Build JSON array of URLs
URL_ARRAY=$(echo "$ALL_URLS" | awk 'BEGIN{printf "["} NR>1{printf ","} {printf "\"%s\"", $0} END{printf "]"}')

# Append a migration stub entry to the builder profile
mkdir -p "$GSTACK_HOME"
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"

echo " [v0.16.2.0] Migrated $(echo "$ALL_URLS" | wc -l | tr -d ' ') resource URLs to builder profile."
Loading
Loading