feat: first-run activation — project-aware scaffold, router front door, onboarding nudges

Adds the activation system that drives a new install toward a concrete first move:
- bin/gstack-first-task-detect: local-git+filesystem repo classifier emitting one
  validated enum bucket (greenfield/code_<lang>/branch_ahead/dirty_default/clean_default),
  portable timeouts, fail-safe empty output.
- generate-first-run-guidance.ts: unified preamble section — first-run project-aware
  scaffold + returning-session plan->review->ship tip, gated on a persistent .activated
  marker and never run in headless. Detection wired lazily in generate-preamble-bash.ts.
- SKILL.md.tmpl: top-level gstack skill is now a pure router (browse body removed; it
  lives in /browse), routing any request and sending browser/QA work to /browse.
- setup: first-move nudge on first install. office-hours: closing handoff that launches
  the next review via the Skill tool.
- telemetry-ingest: accept onboarding/first_task_scaffold_shown/handoff/route event types.
This commit is contained in:
Garry Tan 2026-06-21 07:16:18 -07:00
parent a861c00cfa
commit 938fa4a035
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
8 changed files with 244 additions and 250 deletions

View File

@ -1,26 +1,40 @@
---
name: gstack
preamble-tier: 1
version: 1.1.0
version: 1.2.0
description: |
Fast headless browser for QA testing and site dogfooding. Navigate pages, interact with
elements, verify state, diff before/after, take annotated screenshots, test responsive
layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or
test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
Router for the gstack skill suite. Sends any gstack request to the right skill
(planning, review, QA, shipping, debugging, docs, security, design). For browser/QA
and dogfooding it points you at /browse. Use when you invoke gstack without a specific
skill, or ask "which gstack skill fits this?". (gstack)
allowed-tools:
- Bash
- Read
- AskUserQuestion
triggers:
- browse this page
- take a screenshot
- navigate to url
- inspect the page
- gstack
- which gstack skill
- route this with gstack
---
{{PREAMBLE}}
## Route first
This is the gstack router. Its one job is to send the request to the right skill.
1. If the request is about a browser, QA, dogfooding, screenshots, or inspecting a page
(open a site, test a deploy, take a screenshot, check a flow visually) → invoke `/browse`.
2. Otherwise, route by the rules below. If nothing matches, answer directly.
Best-effort, record which way you routed (never block on it). Set `ROUTE_OUTCOME` to
`browse` (sent to /browse), `routed` (sent to another skill), or `direct` (answered
directly, no skill matched):
```bash
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type route --skill gstack --outcome ROUTE_OUTCOME --session-id "$_SESSION_ID" 2>/dev/null || true
```
If `PROACTIVE` is `false`: do NOT proactively invoke or suggest other gstack skills during
this session. Only run skills the user explicitly invokes. This preference persists across
sessions via `gstack-config`.
@ -75,236 +89,3 @@ directly as usual.
If the user opts out of suggestions, run `gstack-config set proactive false`.
If they opt back in, run `gstack-config set proactive true`.
# gstack browse: QA Testing & Dogfooding
Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command.
Auto-shuts down after 30 min idle. State persists between calls (cookies, tabs, sessions).
{{BROWSE_SETUP}}
## IMPORTANT
- Use the compiled binary via Bash: `$B <command>`
- NEVER use `mcp__claude-in-chrome__*` tools. They are slow and unreliable.
- Browser persists between calls — cookies, login sessions, and tabs carry over.
- Dialogs (alert/confirm/prompt) are auto-accepted by default — no browser lockup.
- **Show screenshots:** After `$B screenshot`, `$B snapshot -a -o`, or `$B responsive`, always use the Read tool on the output PNG(s) so the user can see them. Without this, screenshots are invisible.
## QA Workflows
> **Credential safety:** Use environment variables for test credentials.
> Set them before running: `export TEST_EMAIL="..." TEST_PASSWORD="..."`
### Test a user flow (login, signup, checkout, etc.)
```bash
# 1. Go to the page
$B goto https://app.example.com/login
# 2. See what's interactive
$B snapshot -i
# 3. Fill the form using refs
$B fill @e3 "$TEST_EMAIL"
$B fill @e4 "$TEST_PASSWORD"
$B click @e5
# 4. Verify it worked
$B snapshot -D # diff shows what changed after clicking
$B is visible ".dashboard" # assert the dashboard appeared
$B screenshot /tmp/after-login.png
```
### Verify a deployment / check prod
```bash
$B goto https://yourapp.com
$B text # read the page — does it load?
$B console # any JS errors?
$B network # any failed requests?
$B js "document.title" # correct title?
$B is visible ".hero-section" # key elements present?
$B screenshot /tmp/prod-check.png
```
### Dogfood a feature end-to-end
```bash
# Navigate to the feature
$B goto https://app.example.com/new-feature
# Take annotated screenshot — shows every interactive element with labels
$B snapshot -i -a -o /tmp/feature-annotated.png
# Find ALL clickable things (including divs with cursor:pointer)
$B snapshot -C
# Walk through the flow
$B snapshot -i # baseline
$B click @e3 # interact
$B snapshot -D # what changed? (unified diff)
# Check element states
$B is visible ".success-toast"
$B is enabled "#next-step-btn"
$B is checked "#agree-checkbox"
# Check console for errors after interactions
$B console
```
### Test responsive layouts
```bash
# Quick: 3 screenshots at mobile/tablet/desktop
$B goto https://yourapp.com
$B responsive /tmp/layout
# Manual: specific viewport
$B viewport 375x812 # iPhone
$B screenshot /tmp/mobile.png
$B viewport 1440x900 # Desktop
$B screenshot /tmp/desktop.png
# Element screenshot (crop to specific element)
$B screenshot "#hero-banner" /tmp/hero.png
$B snapshot -i
$B screenshot @e3 /tmp/button.png
# Region crop
$B screenshot --clip 0,0,800,600 /tmp/above-fold.png
# Viewport only (no scroll)
$B screenshot --viewport /tmp/viewport.png
```
### Test file upload
```bash
$B goto https://app.example.com/upload
$B snapshot -i
$B upload @e3 /path/to/test-file.pdf
$B is visible ".upload-success"
$B screenshot /tmp/upload-result.png
```
### Test forms with validation
```bash
$B goto https://app.example.com/form
$B snapshot -i
# Submit empty — check validation errors appear
$B click @e10 # submit button
$B snapshot -D # diff shows error messages appeared
$B is visible ".error-message"
# Fill and resubmit
$B fill @e3 "valid input"
$B click @e10
$B snapshot -D # diff shows errors gone, success state
```
### Test dialogs (delete confirmations, prompts)
```bash
# Set up dialog handling BEFORE triggering
$B dialog-accept # will auto-accept next alert/confirm
$B click "#delete-button" # triggers confirmation dialog
$B dialog # see what dialog appeared
$B snapshot -D # verify the item was deleted
# For prompts that need input
$B dialog-accept "my answer" # accept with text
$B click "#rename-button" # triggers prompt
```
### Test authenticated pages (import real browser cookies)
```bash
# Import cookies from your real browser (opens interactive picker)
$B cookie-import-browser
# Or import a specific domain directly
$B cookie-import-browser comet --domain .github.com
# Now test authenticated pages
$B goto https://github.com/settings/profile
$B snapshot -i
$B screenshot /tmp/github-profile.png
```
> **Cookie safety:** `cookie-import-browser` transfers real session data.
> Only import cookies from browsers you control.
### Compare two pages / environments
```bash
$B diff https://staging.app.com https://prod.app.com
```
### Multi-step chain (efficient for long flows)
```bash
echo '[
["goto","https://app.example.com"],
["snapshot","-i"],
["fill","@e3","$TEST_EMAIL"],
["fill","@e4","$TEST_PASSWORD"],
["click","@e5"],
["snapshot","-D"],
["screenshot","/tmp/result.png"]
]' | $B chain
```
## Quick Assertion Patterns
```bash
# Element exists and is visible
$B is visible ".modal"
# Button is enabled/disabled
$B is enabled "#submit-btn"
$B is disabled "#submit-btn"
# Checkbox state
$B is checked "#agree"
# Input is editable
$B is editable "#name-field"
# Element has focus
$B is focused "#search-input"
# Page contains text
$B js "document.body.textContent.includes('Success')"
# Element count
$B js "document.querySelectorAll('.list-item').length"
# Specific attribute value
$B attrs "#logo" # returns all attributes as JSON
# CSS property
$B css ".button" "background-color"
```
## Snapshot System
{{SNAPSHOT_FLAGS}}
## Command Reference
{{COMMAND_REFERENCE}}
## Tips
1. **Navigate once, query many times.** `goto` loads the page; then `text`, `js`, `screenshot` all hit the loaded page instantly.
2. **Use `snapshot -i` first.** See all interactive elements, then click/fill by ref. No CSS selector guessing.
3. **Use `snapshot -D` to verify.** Baseline → action → diff. See exactly what changed.
4. **Use `is` for assertions.** `is visible .modal` is faster and more reliable than parsing page text.
5. **Use `snapshot -a` for evidence.** Annotated screenshots are great for bug reports.
6. **Use `snapshot -C` for tricky UIs.** Finds clickable divs that the accessibility tree misses.
7. **Check `console` after actions.** Catch JS errors that don't surface visually.
8. **Use `chain` for long flows.** Single command, no per-step CLI overhead.

105
bin/gstack-first-task-detect Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env bash
# gstack-first-task-detect — classify the current project into ONE first-task
# bucket so the first-run scaffold can suggest a concrete next skill.
#
# Contract (load-bearing — the preamble eval's nothing but a single token):
# - Prints EXACTLY ONE whitelisted enum token to stdout, or nothing.
# - Never hangs: every git call is wrapped in a portable 2s timeout.
# - Never errors out of the caller: best-effort, fail-safe to no output.
# - Local git + filesystem only. NO network (no gh/glab) — this runs in the
# latency-sensitive skill preamble.
#
# Enum tokens (the ONLY strings this ever emits):
# greenfield | code_node | code_python | code_rust | code_go | code_ruby
# | code_ios | branch_ahead | dirty_default | clean_default | nongit
#
# The caller maps the token to human prose; no description text crosses the
# eval boundary. Usage: TOKEN=$(gstack-first-task-detect)
set -uo pipefail
# --- Portable timeout wrapper (gtimeout → timeout → unwrapped), per gstack-codex-probe ---
_ftd_to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "")
_git() {
if [ -n "$_ftd_to" ]; then
"$_ftd_to" 2 git "$@" 2>/dev/null
else
git "$@" 2>/dev/null
fi
}
# Emit only whitelisted tokens — defense in depth even though every emit site
# below is a literal.
_emit() {
case "$1" in
greenfield|code_node|code_python|code_rust|code_go|code_ruby|code_ios|branch_ahead|dirty_default|clean_default|nongit)
printf '%s\n' "$1" ;;
*) : ;; # unknown → emit nothing (caller shows no scaffold)
esac
exit 0
}
# --- 1. Not a git repo → nothing actionable from git, but language may still help ---
if ! _git rev-parse --is-inside-work-tree | grep -q true; then
_emit nongit
fi
# --- 2. Greenfield (no commits) ---
_commits=$(_git rev-list --count HEAD || echo 0)
[ -z "$_commits" ] && _commits=0
if [ "$_commits" -eq 0 ] 2>/dev/null; then
_emit greenfield
fi
# --- 3. Resolve default + current branch (reuse the repo's base-branch fallback) ---
_default=$(_git symbolic-ref refs/remotes/origin/HEAD | sed 's|refs/remotes/origin/||')
if [ -z "$_default" ]; then
if _git rev-parse --verify origin/main >/dev/null; then _default=main
elif _git rev-parse --verify origin/master >/dev/null; then _default=master
else _default=main
fi
fi
_current=$(_git rev-parse --abbrev-ref HEAD || echo "")
# --- 4. On a feature branch ahead of base (local-only) → ready to review/ship ---
if [ -n "$_current" ] && [ "$_current" != "$_default" ] && [ "$_current" != "HEAD" ]; then
# ahead-count vs the REAL base: prefer origin/<default> (the remote truth);
# a stale local <default> would falsely inflate the ahead count.
_base=""
if _git rev-parse --verify "origin/$_default" >/dev/null; then _base="origin/$_default"
elif _git rev-parse --verify "$_default" >/dev/null; then _base="$_default"
fi
if [ -n "$_base" ]; then
_ahead=$(_git rev-list --count "$_base..HEAD" || echo 0)
[ -z "$_ahead" ] && _ahead=0
if [ "$_ahead" -gt 0 ] 2>/dev/null; then
_emit branch_ahead
fi
fi
fi
# --- 5. Uncommitted changes on the default branch → review + commit ---
_dirty=$(_git status --porcelain | head -1)
if [ -n "$_dirty" ] && { [ "$_current" = "$_default" ] || [ "$_current" = "HEAD" ] || [ -z "$_current" ]; }; then
_emit dirty_default
fi
# --- 6. Has code + a recognized language marker → verify tests/build ---
# Resolve to the repo root first so a skill invoked from a subdir doesn't miss
# a root-level package.json / Cargo.toml / etc. Filesystem-only after this.
_TOP=$(_git rev-parse --show-toplevel)
[ -n "$_TOP" ] && cd "$_TOP" 2>/dev/null || true
# Order by specificity/likelihood; stop at first match.
if [ -f package.json ]; then _emit code_node; fi
if [ -f pyproject.toml ] || [ -f setup.py ] || [ -f requirements.txt ]; then _emit code_python; fi
if [ -f Cargo.toml ]; then _emit code_rust; fi
if [ -f go.mod ]; then _emit code_go; fi
if [ -f Gemfile ]; then _emit code_ruby; fi
if ls ./*.xcodeproj >/dev/null 2>&1 || [ -d ios ]; then _emit code_ios; fi
# --- 7. Clean default branch with history, no recognized language → pick something ---
if [ "$_commits" -ge 5 ] 2>/dev/null; then
_emit clean_default
fi
# Nothing confidently actionable → emit nothing (no scaffold).
exit 0

View File

@ -420,13 +420,54 @@ If A: run `open URL1 && open URL2 && open URL3` (opens each in default browser).
If B/C/D: run `open` on the selected URL only.
If E: proceed to next-skill recommendations.
### Next-skill recommendations
### Next-skill recommendations — hand the user into the loop
After the plea, suggest the next step:
Don't just list options. Offer to launch the next review NOW so the design doc flows
straight into a structured review. Map the design-doc mode to the recommended option
(default `/plan-eng-review` when ambiguous — it has the broadest real-world use and the
strongest retention).
- **`/plan-ceo-review`** for ambitious features (EXPANSION mode) — rethink the problem, find the 10-star product
- **`/plan-eng-review`** for well-scoped implementation planning — lock in architecture, tests, edge cases
- **`/plan-design-review`** for visual/UX design review
**If `PROACTIVE` is `false` OR `CONDUCTOR_SESSION: true`:** do NOT auto-launch. Recommend
in one line and stop, letting the user invoke:
- EXPANSION / ambitious → "Next: `/plan-ceo-review` to pressure-test scope and find the 10-star product."
- well-scoped → "Next: `/plan-eng-review` to lock architecture, tests, and edge cases."
- visual/UX-heavy → "Next: `/plan-design-review` for a visual/UX pass."
**Otherwise**, offer via AskUserQuestion (D<N> format from the preamble):
D<N> — Run the next review now?
Project/branch/task: the design doc you just wrote for this feature.
ELI10: You just wrote a design doc. The natural next step is a structured review that
catches scope and architecture problems before you build. I can launch it right now, or
you can run it later yourself.
Stakes if we pick wrong: skipping review means problems surface mid-build, costing rework.
Recommendation: the mode-mapped option (`/plan-eng-review` if unsure) because it locks the
plan before any code is written.
Completeness: A=10/10, B=9/10, C=8/10, D=3/10
Pros / cons:
A) Run /plan-eng-review now (recommended)
✅ Locks architecture, tests, and edge cases before a line of code is written
❌ Adds ~15 min CC now (human: 1-2 hrs of review compressed)
B) Run /plan-ceo-review now
✅ Pressure-tests ambition and scope — finds the 10-star version of the product
❌ Lower value when the scope is already tight and well understood
C) Run /plan-design-review now
✅ Catches visual/UX problems while they are still cheap plan-stage changes
❌ Little value for backend-only or non-visual features
D) Not now — I'll run a review later
✅ Keeps you in flow if you want to start building immediately
❌ Review gaps compound; problems get more expensive after code exists
Net: 15 minutes of structured review now against rework risk later.
On the user's SELECTION of A/B/C (not on invocation success), log the handoff, then invoke
the chosen skill via the **Skill tool** (it auto-discovers the design doc):
```bash
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type handoff --skill office-hours --outcome accepted --session-id "$_SESSION_ID" 2>/dev/null || true
```
On D, log declined and stop:
```bash
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type handoff --skill office-hours --outcome declined --session-id "$_SESSION_ID" 2>/dev/null || true
```
The design doc at `~/.gstack/projects/` is automatically discoverable by downstream skills — they will read it during their pre-review system audit.

View File

@ -32,6 +32,7 @@ import {
import { generateLakeIntro } from './preamble/generate-lake-intro';
import { generateTelemetryPrompt } from './preamble/generate-telemetry-prompt';
import { generateProactivePrompt } from './preamble/generate-proactive-prompt';
import { generateFirstRunGuidance } from './preamble/generate-first-run-guidance';
import { generateRoutingInjection } from './preamble/generate-routing-injection';
import { generateVendoringDeprecation } from './preamble/generate-vendoring-deprecation';
import { generateSpawnedSessionCheck } from './preamble/generate-spawned-session-check';
@ -94,6 +95,7 @@ export function generatePreamble(ctx: TemplateContext): string {
generateLakeIntro(),
generateTelemetryPrompt(ctx),
generateProactivePrompt(ctx),
generateFirstRunGuidance(ctx),
generateRoutingInjection(ctx),
generateVendoringDeprecation(ctx),
generateSpawnedSessionCheck(),

View File

@ -0,0 +1,35 @@
import type { TemplateContext } from '../types';
// First-run guidance (P4 scaffold + P3 loop tip), unified into one section.
// Branches on the persistent `.activated` lifecycle marker — NOT `_SESSIONS`,
// which counts concurrent sessions in the last 120 min, not first-vs-returning.
//
// The FIRST_TASK enum is computed at runtime in generate-preamble-bash.ts (gated
// so the detector only runs on the first run) and printed as `FIRST_TASK: <token>`.
// This section maps the token the model SAW in that output to a one-line nudge
// (no description string ever crosses an eval boundary) and sets markers:
// ~/.gstack/.activated — set at the end of the first-ever skill run
// ~/.gstack/.first-loop-tip-shown — set when the returning-session tip is shown
//
// Note: bash blocks run in separate shells, so the runtime token cannot be read
// from a shell var here — the model substitutes the token it saw for TASK_TOKEN,
// exactly like the Telemetry section substitutes SKILL_NAME/OUTCOME.
export function generateFirstRunGuidance(ctx: TemplateContext): string {
return `## First-run guidance (one-time)
If \`ACTIVATED\` is \`no\` (first skill run on this machine) AND the preamble printed a non-empty \`FIRST_TASK:\` value that is NOT \`nongit\`: show ONE short, project-specific line mapped from the token, as a heads-up, then CONTINUE with whatever the user actually asked — do NOT halt their task. Map the token: \`greenfield\` → "Fresh repo — shape it first with \`/spec\` or \`/office-hours\`." \`code_node\`/\`code_python\`/\`code_rust\`/\`code_go\`/\`code_ruby\`/\`code_ios\` → "There's code here — \`/qa\` to see it work, or \`/investigate\` if something's off." \`branch_ahead\` → "Unshipped work on this branch — \`/review\` then \`/ship\`." \`dirty_default\` → "Uncommitted changes — \`/review\` before committing." \`clean_default\` → "Pick one: \`/spec\`, \`/investigate\`, or \`/qa\`." Then substitute the token you saw for TASK_TOKEN and run (best-effort), and mark activated:
\`\`\`bash
${ctx.paths.binDir}/gstack-telemetry-log --event-type first_task_scaffold_shown --skill "TASK_TOKEN" --outcome shown 2>/dev/null || true
touch ~/.gstack/.activated 2>/dev/null || true
\`\`\`
If \`ACTIVATED\` is \`no\` but \`FIRST_TASK:\` is empty or \`nongit\` (headless, non-git, or nothing actionable): show nothing, just run \`touch ~/.gstack/.activated 2>/dev/null || true\`.
Else if \`ACTIVATED\` is \`yes\` AND \`FIRST_LOOP_SHOWN\` is \`no\`: say once as a heads-up (then continue):
> Tip: gstack pays off when you complete one loop **plan review ship**. A common first loop: \`/office-hours\` or \`/spec\` to shape it, \`/plan-eng-review\` to lock it, then \`/ship\`.
Then run \`touch ~/.gstack/.first-loop-tip-shown 2>/dev/null || true\`.
Skip this section if \`ACTIVATED\` and \`FIRST_LOOP_SHOWN\` are both \`yes\`.`;
}

View File

@ -43,6 +43,17 @@ echo "SESSION_KIND: $_SESSION_KIND"
if [ "$_SESSION_KIND" != "headless" ] && { [ -n "\${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "\${CONDUCTOR_PORT:-}" ]; }; then
echo "CONDUCTOR_SESSION: true"
fi
_ACTIVATED=$([ -f ~/.gstack/.activated ] && echo "yes" || echo "no")
_FIRST_LOOP_SHOWN=$([ -f ~/.gstack/.first-loop-tip-shown ] && echo "yes" || echo "no")
echo "ACTIVATED: $_ACTIVATED"
echo "FIRST_LOOP_SHOWN: $_FIRST_LOOP_SHOWN"
# First-run project detection: run the detector ONLY on the first-ever skill run
# (ACTIVATED=no, interactive) so it stays off the hot path for every run after.
_FIRST_TASK=""
if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]; then
_FIRST_TASK=$(${ctx.paths.binDir}/gstack-first-task-detect 2>/dev/null || true)
fi
echo "FIRST_TASK: $_FIRST_TASK"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(${ctx.paths.binDir}/gstack-config get telemetry 2>/dev/null || true)

11
setup
View File

@ -1229,7 +1229,16 @@ fi
# 9. First-time welcome + legacy cleanup
if [ ! -f "$HOME/.gstack/.welcome-seen" ]; then
log " Welcome! Run /gstack-upgrade anytime to stay current."
log ""
log " gstack is ready. First move:"
log " New idea / empty repo? /office-hours or /spec"
log " Existing code? /qa to see it work, or /investigate"
log " (Run /gstack-upgrade anytime to stay current)"
log ""
# Best-effort onboarding telemetry (respects telemetry!=off; never blocks setup).
if [ -x "$SOURCE_GSTACK_DIR/bin/gstack-telemetry-log" ]; then
"$SOURCE_GSTACK_DIR/bin/gstack-telemetry-log" --event-type onboarding --skill _setup_welcome --outcome shown >/dev/null 2>&1 || true
fi
touch "$HOME/.gstack/.welcome-seen"
fi
rm -f /tmp/gstack-latest-version

View File

@ -67,8 +67,18 @@ Deno.serve(async (req) => {
// Validate schema version
if (event.v !== 1) continue;
// Validate event_type
const validTypes = ["skill_run", "upgrade_prompted", "upgrade_completed"];
// Validate event_type. Activation-funnel events (v1.x) join the originals:
// onboarding (P0 setup nudge), first_task_scaffold_shown (P4 first-run
// scaffold), handoff (P1 office-hours → next skill), route (gstack router).
const validTypes = [
"skill_run",
"upgrade_prompted",
"upgrade_completed",
"onboarding",
"first_task_scaffold_shown",
"handoff",
"route",
];
if (!validTypes.includes(event.event_type)) continue;
rows.push({