mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
feat: relationship closing — office-hours adapts to repeat users (v0.16.2.0) (#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * chore: bump version and changelog (v0.16.2.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+134
@@ -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:"
|
||||
}
|
||||
Reference in New Issue
Block a user