diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index 2b19817e9..402bd0d7b 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -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 ` -- 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. diff --git a/bin/gstack-first-task-detect b/bin/gstack-first-task-detect new file mode 100755 index 000000000..97a1a7a63 --- /dev/null +++ b/bin/gstack-first-task-detect @@ -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/ (the remote truth); + # a stale local 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 diff --git a/office-hours/sections/design-and-handoff.md.tmpl b/office-hours/sections/design-and-handoff.md.tmpl index 8acfe1f32..a8f8c5aac 100644 --- a/office-hours/sections/design-and-handoff.md.tmpl +++ b/office-hours/sections/design-and-handoff.md.tmpl @@ -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 format from the preamble): + +D — 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. diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index cc3045174..1cdfc2e6d 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -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(), diff --git a/scripts/resolvers/preamble/generate-first-run-guidance.ts b/scripts/resolvers/preamble/generate-first-run-guidance.ts new file mode 100644 index 000000000..9818a04c3 --- /dev/null +++ b/scripts/resolvers/preamble/generate-first-run-guidance.ts @@ -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: `. +// 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\`.`; +} diff --git a/scripts/resolvers/preamble/generate-preamble-bash.ts b/scripts/resolvers/preamble/generate-preamble-bash.ts index f82318f0c..f0bc57b4b 100644 --- a/scripts/resolvers/preamble/generate-preamble-bash.ts +++ b/scripts/resolvers/preamble/generate-preamble-bash.ts @@ -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) diff --git a/setup b/setup index e4d31f881..0b6098832 100755 --- a/setup +++ b/setup @@ -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 diff --git a/supabase/functions/telemetry-ingest/index.ts b/supabase/functions/telemetry-ingest/index.ts index 125f69f65..acf994eab 100644 --- a/supabase/functions/telemetry-ingest/index.ts +++ b/supabase/functions/telemetry-ingest/index.ts @@ -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({