mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/learning-phase-2.5-clean
# Conflicts: # CHANGELOG.md # VERSION # package.json
This commit is contained in:
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# GStack Browser launcher — starts browse server + headed Chromium with extension
|
||||
#
|
||||
# Works in two modes:
|
||||
# 1. Inside .app bundle: Contents/MacOS/gstack-browser → Resources are at ../Resources/
|
||||
# 2. Dev mode (run directly): uses global gstack install at ~/.claude/skills/gstack/
|
||||
#
|
||||
# Usage:
|
||||
# open "GStack Browser.app" # .app bundle mode
|
||||
# scripts/app/gstack-browser # dev mode (uses global gstack install)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Detect mode: .app bundle or dev
|
||||
if [ -d "$SCRIPT_DIR/../Resources" ]; then
|
||||
# .app bundle mode — resources are alongside in the bundle
|
||||
DIR="$(cd "$SCRIPT_DIR/../Resources" && pwd)"
|
||||
else
|
||||
# Dev mode — use global gstack install
|
||||
DIR="$HOME/.claude/skills/gstack"
|
||||
fi
|
||||
|
||||
# Point Playwright at bundled Chromium (only in .app mode)
|
||||
if [ -d "$DIR/chromium" ]; then
|
||||
CHROMIUM_APP=$(ls -d "$DIR/chromium/"*.app 2>/dev/null | head -1)
|
||||
if [ -n "$CHROMIUM_APP" ]; then
|
||||
export GSTACK_CHROMIUM_PATH="$CHROMIUM_APP/Contents/MacOS/$(ls "$CHROMIUM_APP/Contents/MacOS/" | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Browse server config
|
||||
export BROWSE_PORT=34567
|
||||
export BROWSE_HEADED=1
|
||||
|
||||
# Extension: bundled first, then global install
|
||||
if [ -d "$DIR/extension" ]; then
|
||||
export BROWSE_EXTENSIONS_DIR="$DIR/extension"
|
||||
fi
|
||||
|
||||
# Server script: bundled source first, then global install
|
||||
if [ -f "$DIR/src/server.ts" ]; then
|
||||
export BROWSE_SERVER_SCRIPT="$DIR/src/server.ts"
|
||||
elif [ -f "$HOME/.claude/skills/gstack/browse/src/server.ts" ]; then
|
||||
export BROWSE_SERVER_SCRIPT="$HOME/.claude/skills/gstack/browse/src/server.ts"
|
||||
fi
|
||||
|
||||
# Browse binary: bundled .app first, then global install
|
||||
# Note: -x on a directory is true, so check -f (regular file) too
|
||||
BROWSE_BIN=""
|
||||
for candidate in "$DIR/browse" "$DIR/browse/dist/browse" "$HOME/.claude/skills/gstack/browse/dist/browse"; do
|
||||
if [ -f "$candidate" ] && [ -x "$candidate" ]; then
|
||||
BROWSE_BIN="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$BROWSE_BIN" ]; then
|
||||
echo "ERROR: browse binary not found. Run 'bun run build' in the gstack repo or reinstall GStack Browser."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure profile directory
|
||||
mkdir -p ~/.gstack/chromium-profile
|
||||
|
||||
# Project binding: use last-used project dir, default to home
|
||||
PROJECT_DIR=$(cat ~/.gstack/last-project 2>/dev/null || echo "$HOME")
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
PROJECT_DIR="$HOME"
|
||||
fi
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Launch browse in connect mode
|
||||
exec "$BROWSE_BIN" connect "$@"
|
||||
Binary file not shown.
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
# Build GStack Browser.app — macOS application bundle
|
||||
#
|
||||
# Creates a self-contained .app with:
|
||||
# - Compiled browse binary
|
||||
# - Playwright's bundled Chromium
|
||||
# - Chrome extension (sidebar)
|
||||
# - Info.plist with bundle ID
|
||||
#
|
||||
# Output: dist/GStack Browser.app and dist/GStack-Browser.dmg
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-app.sh # Build .app + DMG
|
||||
# ./scripts/build-app.sh --no-dmg # Build .app only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
APP_NAME="GStack Browser"
|
||||
BUNDLE_ID="com.gstack.browser"
|
||||
VERSION=$(cat "$ROOT/VERSION" 2>/dev/null || echo "0.0.1")
|
||||
BUILD_DIR="$ROOT/dist"
|
||||
APP_DIR="$BUILD_DIR/$APP_NAME.app"
|
||||
|
||||
echo "Building $APP_NAME v$VERSION..."
|
||||
|
||||
# ─── Step 1: Compile browse binary ─────────────────────────────
|
||||
echo " Compiling browse binary..."
|
||||
cd "$ROOT/browse"
|
||||
bun build --compile src/cli.ts --outfile "$BUILD_DIR/browse-app" --target=bun 2>/dev/null
|
||||
cd "$ROOT"
|
||||
|
||||
# ─── Step 2: Find Playwright's Chromium ─────────────────────────
|
||||
echo " Locating Playwright Chromium..."
|
||||
PW_CACHE="$HOME/Library/Caches/ms-playwright"
|
||||
CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort -V | tail -1)
|
||||
|
||||
if [ -z "$CHROMIUM_DIR" ]; then
|
||||
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
||||
echo "Run: bunx playwright install chromium"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHROME_APP=$(ls -d "$CHROMIUM_DIR"/*.app 2>/dev/null | head -1)
|
||||
if [ -z "$CHROME_APP" ]; then
|
||||
echo "ERROR: Chrome .app not found in $CHROMIUM_DIR"
|
||||
exit 1
|
||||
fi
|
||||
echo " Found: $(basename "$CHROME_APP")"
|
||||
|
||||
# ─── Step 3: Create .app structure ──────────────────────────────
|
||||
echo " Building .app bundle..."
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$APP_DIR/Contents/MacOS"
|
||||
mkdir -p "$APP_DIR/Contents/Resources"
|
||||
|
||||
# Launcher script
|
||||
cp "$ROOT/scripts/app/gstack-browser" "$APP_DIR/Contents/MacOS/gstack-browser"
|
||||
chmod +x "$APP_DIR/Contents/MacOS/gstack-browser"
|
||||
|
||||
# Browse binary
|
||||
cp "$BUILD_DIR/browse-app" "$APP_DIR/Contents/Resources/browse"
|
||||
chmod +x "$APP_DIR/Contents/Resources/browse"
|
||||
|
||||
# Extension
|
||||
cp -r "$ROOT/extension" "$APP_DIR/Contents/Resources/extension"
|
||||
# Remove .auth.json if present (auth now via /health endpoint)
|
||||
rm -f "$APP_DIR/Contents/Resources/extension/.auth.json"
|
||||
|
||||
# Server source (needed for `bun run server.ts` subprocess)
|
||||
# The launcher sets BROWSE_SERVER_SCRIPT to point at this.
|
||||
# Copy the full src/ directory since server.ts imports other modules.
|
||||
echo " Copying browse source..."
|
||||
cp -r "$ROOT/browse/src" "$APP_DIR/Contents/Resources/src"
|
||||
# Also need package.json for module resolution
|
||||
cp "$ROOT/browse/package.json" "$APP_DIR/Contents/Resources/" 2>/dev/null || true
|
||||
|
||||
# Chromium
|
||||
mkdir -p "$APP_DIR/Contents/Resources/chromium"
|
||||
echo " Copying Chromium (~330MB)..."
|
||||
cp -a "$CHROME_APP" "$APP_DIR/Contents/Resources/chromium/"
|
||||
|
||||
# ─── Step 3b: Rebrand Chromium ────────────────────────────────────
|
||||
# Patch the bundled Chromium's Info.plist so macOS shows "GStack Browser"
|
||||
# in the menu bar, Dock, and Cmd+Tab instead of "Google Chrome for Testing"
|
||||
CHROMIUM_PLIST="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Info.plist"
|
||||
if [ -f "$CHROMIUM_PLIST" ]; then
|
||||
echo " Rebranding Chromium → $APP_NAME..."
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleName '$APP_NAME'" "$CHROMIUM_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName '$APP_NAME'" "$CHROMIUM_PLIST"
|
||||
# Also update the localized strings if present
|
||||
CHROMIUM_STRINGS="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Resources/en.lproj/InfoPlist.strings"
|
||||
if [ -f "$CHROMIUM_STRINGS" ]; then
|
||||
# InfoPlist.strings may be binary plist, convert to xml first
|
||||
plutil -convert xml1 "$CHROMIUM_STRINGS" 2>/dev/null || true
|
||||
sed -i '' "s/Google Chrome for Testing/$APP_NAME/g" "$CHROMIUM_STRINGS" 2>/dev/null || true
|
||||
fi
|
||||
# Replace Chromium's icon with ours so the Dock shows the GStack icon
|
||||
# (Chromium's process owns the Dock icon, not our launcher)
|
||||
ICON_SRC="$SCRIPT_DIR/app/icon.icns"
|
||||
if [ -f "$ICON_SRC" ]; then
|
||||
CHROMIUM_RESOURCES="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Resources"
|
||||
# Find the original icon filename from Chromium's plist
|
||||
ORIG_ICON=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" "$CHROMIUM_PLIST" 2>/dev/null || echo "app")
|
||||
# Add .icns extension if not present
|
||||
[[ "$ORIG_ICON" != *.icns ]] && ORIG_ICON="${ORIG_ICON}.icns"
|
||||
cp "$ICON_SRC" "$CHROMIUM_RESOURCES/$ORIG_ICON"
|
||||
echo " Replaced Chromium icon → $ORIG_ICON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Step 3c: App icon ────────────────────────────────────────────
|
||||
ICON_SRC="$SCRIPT_DIR/app/icon.icns"
|
||||
if [ -f "$ICON_SRC" ]; then
|
||||
cp "$ICON_SRC" "$APP_DIR/Contents/Resources/icon.icns"
|
||||
echo " App icon installed"
|
||||
else
|
||||
echo " WARNING: No icon.icns found at $ICON_SRC — app will use default icon"
|
||||
fi
|
||||
|
||||
# ─── Step 4: Info.plist ──────────────────────────────────────────
|
||||
cat > "$APP_DIR/Contents/Info.plist" << PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$BUNDLE_ID</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$VERSION</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>gstack-browser</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# ─── Step 5: App size report ────────────────────────────────────
|
||||
APP_SIZE=$(du -sh "$APP_DIR" | cut -f1)
|
||||
echo ""
|
||||
echo " $APP_NAME.app: $APP_SIZE"
|
||||
echo " Contents/MacOS/gstack-browser (launcher)"
|
||||
echo " Contents/Resources/browse ($(du -sh "$APP_DIR/Contents/Resources/browse" | cut -f1))"
|
||||
echo " Contents/Resources/extension/ ($(du -sh "$APP_DIR/Contents/Resources/extension" | cut -f1))"
|
||||
echo " Contents/Resources/chromium/ ($(du -sh "$APP_DIR/Contents/Resources/chromium" | cut -f1))"
|
||||
|
||||
# ─── Step 6: DMG (optional) ─────────────────────────────────────
|
||||
if [ "${1:-}" = "--no-dmg" ]; then
|
||||
echo ""
|
||||
echo "Done. App at: $APP_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DMG_PATH="$BUILD_DIR/GStack-Browser.dmg"
|
||||
echo ""
|
||||
echo " Creating DMG..."
|
||||
rm -f "$DMG_PATH"
|
||||
|
||||
# Create a temporary directory for DMG contents
|
||||
DMG_TMP=$(mktemp -d)
|
||||
cp -a "$APP_DIR" "$DMG_TMP/"
|
||||
ln -s /Applications "$DMG_TMP/Applications"
|
||||
|
||||
hdiutil create -volname "$APP_NAME" \
|
||||
-srcfolder "$DMG_TMP" \
|
||||
-ov -format UDZO \
|
||||
"$DMG_PATH" \
|
||||
> /dev/null 2>&1
|
||||
|
||||
rm -rf "$DMG_TMP"
|
||||
|
||||
DMG_SIZE=$(du -sh "$DMG_PATH" | cut -f1)
|
||||
echo " DMG: $DMG_SIZE → $DMG_PATH"
|
||||
echo ""
|
||||
echo "Done. Install: open $DMG_PATH"
|
||||
@@ -132,6 +132,63 @@ function extractNameAndDescription(content: string): { name: string; description
|
||||
return { name, description };
|
||||
}
|
||||
|
||||
// ─── Voice Trigger Processing ────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract voice-triggers YAML list from frontmatter.
|
||||
* Returns an array of trigger strings, or [] if no voice-triggers field.
|
||||
*/
|
||||
function extractVoiceTriggers(content: string): string[] {
|
||||
const fmStart = content.indexOf('---\n');
|
||||
if (fmStart !== 0) return [];
|
||||
const fmEnd = content.indexOf('\n---', fmStart + 4);
|
||||
if (fmEnd === -1) return [];
|
||||
const frontmatter = content.slice(fmStart + 4, fmEnd);
|
||||
|
||||
const triggers: string[] = [];
|
||||
let inVoice = false;
|
||||
for (const line of frontmatter.split('\n')) {
|
||||
if (/^voice-triggers:/.test(line)) { inVoice = true; continue; }
|
||||
if (inVoice) {
|
||||
const m = line.match(/^\s+-\s+"(.+)"$/);
|
||||
if (m) triggers.push(m[1]);
|
||||
else if (!/^\s/.test(line)) break;
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess voice triggers: fold voice-triggers YAML field into description,
|
||||
* then strip the field from frontmatter. Must run BEFORE transformFrontmatter
|
||||
* and extractNameAndDescription so all hosts see the updated description.
|
||||
*/
|
||||
function processVoiceTriggers(content: string): string {
|
||||
const triggers = extractVoiceTriggers(content);
|
||||
if (triggers.length === 0) return content;
|
||||
|
||||
// Strip voice-triggers block from frontmatter
|
||||
content = content.replace(/^voice-triggers:\n(?:\s+-\s+"[^"]*"\n?)*/m, '');
|
||||
|
||||
// Get current description (after stripping voice-triggers, so it's clean)
|
||||
const { description } = extractNameAndDescription(content);
|
||||
if (!description) return content;
|
||||
|
||||
// Build new description with voice triggers appended
|
||||
const voiceLine = `Voice triggers (speech-to-text aliases): ${triggers.map(t => `"${t}"`).join(', ')}.`;
|
||||
const newDescription = description + '\n' + voiceLine;
|
||||
|
||||
// Replace old indented description with new in frontmatter
|
||||
const oldIndented = description.split('\n').map(l => ` ${l}`).join('\n');
|
||||
const newIndented = newDescription.split('\n').map(l => ` ${l}`).join('\n');
|
||||
content = content.replace(oldIndented, newIndented);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
export { extractVoiceTriggers, processVoiceTriggers };
|
||||
|
||||
const OPENAI_SHORT_DESCRIPTION_LIMIT = 120;
|
||||
|
||||
function condenseOpenAIShortDescription(description: string): string {
|
||||
@@ -163,8 +220,10 @@ policy:
|
||||
*/
|
||||
function transformFrontmatter(content: string, host: Host): string {
|
||||
if (host === 'claude') {
|
||||
// Strip sensitive: field from Claude output (only Factory uses it)
|
||||
return content.replace(/^sensitive:\s*true\n/m, '');
|
||||
// Strip fields not used by Claude: sensitive (Factory-only), voice-triggers (folded into description by preprocessing)
|
||||
content = content.replace(/^sensitive:\s*true\n/m, '');
|
||||
content = content.replace(/^voice-triggers:\n(?:\s+-\s+"[^"]*"\n?)*/m, '');
|
||||
return content;
|
||||
}
|
||||
|
||||
const fmStart = content.indexOf('---\n');
|
||||
@@ -364,13 +423,22 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
throw new Error(`Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}`);
|
||||
}
|
||||
|
||||
// Preprocess voice triggers: fold into description, strip field from frontmatter.
|
||||
// Must run BEFORE transformFrontmatter so all hosts see the updated description,
|
||||
// and BEFORE extractedDescription is used by external host metadata.
|
||||
content = processVoiceTriggers(content);
|
||||
|
||||
// Re-extract description AFTER voice trigger preprocessing so Codex openai.yaml
|
||||
// metadata gets the updated description with voice triggers included.
|
||||
const postProcessDescription = extractNameAndDescription(content).description;
|
||||
|
||||
// For Claude: strip sensitive: field (only Factory uses it)
|
||||
// For external hosts: route output, transform frontmatter, rewrite paths
|
||||
let symlinkLoop = false;
|
||||
if (host === 'claude') {
|
||||
content = transformFrontmatter(content, host);
|
||||
} else {
|
||||
const result = processExternalHost(content, tmplContent, host, skillDir, extractedDescription, ctx, extractedName || undefined);
|
||||
const result = processExternalHost(content, tmplContent, host, skillDir, postProcessDescription, ctx, extractedName || undefined);
|
||||
content = result.content;
|
||||
outputPath = result.outputPath;
|
||||
symlinkLoop = result.symlinkLoop;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* DX Framework resolver
|
||||
*
|
||||
* Shared principles, characteristics, cognitive patterns, and scoring rubric
|
||||
* for /plan-devex-review and /devex-review. Compact (~150 lines).
|
||||
*
|
||||
* Hall of Fame examples are NOT included here. They live in
|
||||
* plan-devex-review/dx-hall-of-fame.md and are loaded on-demand per pass
|
||||
* to avoid prompt bloat.
|
||||
*/
|
||||
import type { TemplateContext } from './types';
|
||||
|
||||
export function generateDxFramework(ctx: TemplateContext): string {
|
||||
const hallOfFamePath = `${ctx.paths.skillRoot}/plan-devex-review/dx-hall-of-fame.md`;
|
||||
|
||||
return `## DX First Principles
|
||||
|
||||
These are the laws. Every recommendation traces back to one of these.
|
||||
|
||||
1. **Zero friction at T0.** First five minutes decide everything. One click to start. Hello world without reading docs. No credit card. No demo call.
|
||||
2. **Incremental steps.** Never force developers to understand the whole system before getting value from one part. Gentle ramp, not cliff.
|
||||
3. **Learn by doing.** Playgrounds, sandboxes, copy-paste code that works in context. Reference docs are necessary but never sufficient.
|
||||
4. **Decide for me, let me override.** Opinionated defaults are features. Escape hatches are requirements. Strong opinions, loosely held.
|
||||
5. **Fight uncertainty.** Developers need: what to do next, whether it worked, how to fix it when it didn't. Every error = problem + cause + fix.
|
||||
6. **Show code in context.** Hello world is a lie. Show real auth, real error handling, real deployment. Solve 100% of the problem.
|
||||
7. **Speed is a feature.** Iteration speed is everything. Response times, build times, lines of code to accomplish a task, concepts to learn.
|
||||
8. **Create magical moments.** What would feel like magic? Stripe's instant API response. Vercel's push-to-deploy. Find yours and make it the first thing developers experience.
|
||||
|
||||
## The Seven DX Characteristics
|
||||
|
||||
| # | Characteristic | What It Means | Gold Standard |
|
||||
|---|---------------|---------------|---------------|
|
||||
| 1 | **Usable** | Simple to install, set up, use. Intuitive APIs. Fast feedback. | Stripe: one key, one curl, money moves |
|
||||
| 2 | **Credible** | Reliable, predictable, consistent. Clear deprecation. Secure. | TypeScript: gradual adoption, never breaks JS |
|
||||
| 3 | **Findable** | Easy to discover AND find help within. Strong community. Good search. | React: every question answered on SO |
|
||||
| 4 | **Useful** | Solves real problems. Features match actual use cases. Scales. | Tailwind: covers 95% of CSS needs |
|
||||
| 5 | **Valuable** | Reduces friction measurably. Saves time. Worth the dependency. | Next.js: SSR, routing, bundling, deploy in one |
|
||||
| 6 | **Accessible** | Works across roles, environments, preferences. CLI + GUI. | VS Code: works for junior to principal |
|
||||
| 7 | **Desirable** | Best-in-class tech. Reasonable pricing. Community momentum. | Vercel: devs WANT to use it, not tolerate it |
|
||||
|
||||
## Cognitive Patterns — How Great DX Leaders Think
|
||||
|
||||
Internalize these; don't enumerate them.
|
||||
|
||||
1. **Chef-for-chefs** — Your users build products for a living. The bar is higher because they notice everything.
|
||||
2. **First five minutes obsession** — New dev arrives. Clock starts. Can they hello-world without docs, sales, or credit card?
|
||||
3. **Error message empathy** — Every error is pain. Does it identify the problem, explain the cause, show the fix, link to docs?
|
||||
4. **Escape hatch awareness** — Every default needs an override. No escape hatch = no trust = no adoption at scale.
|
||||
5. **Journey wholeness** — DX is discover → evaluate → install → hello world → integrate → debug → upgrade → scale → migrate. Every gap = a lost dev.
|
||||
6. **Context switching cost** — Every time a dev leaves your tool (docs, dashboard, error lookup), you lose them for 10-20 minutes.
|
||||
7. **Upgrade fear** — Will this break my production app? Clear changelogs, migration guides, codemods, deprecation warnings. Upgrades should be boring.
|
||||
8. **SDK completeness** — If devs write their own HTTP wrapper, you failed. If the SDK works in 4 of 5 languages, the fifth community hates you.
|
||||
9. **Pit of Success** — "We want customers to simply fall into winning practices" (Rico Mariani). Make the right thing easy, the wrong thing hard.
|
||||
10. **Progressive disclosure** — Simple case is production-ready, not a toy. Complex case uses the same API. SwiftUI: \\\`Button("Save") { save() }\\\` → full customization, same API.
|
||||
|
||||
## DX Scoring Rubric (0-10 calibration)
|
||||
|
||||
| Score | Meaning |
|
||||
|-------|---------|
|
||||
| 9-10 | Best-in-class. Stripe/Vercel tier. Developers rave about it. |
|
||||
| 7-8 | Good. Developers can use it without frustration. Minor gaps. |
|
||||
| 5-6 | Acceptable. Works but with friction. Developers tolerate it. |
|
||||
| 3-4 | Poor. Developers complain. Adoption suffers. |
|
||||
| 1-2 | Broken. Developers abandon after first attempt. |
|
||||
| 0 | Not addressed. No thought given to this dimension. |
|
||||
|
||||
**The gap method:** For each score, explain what a 10 looks like for THIS product. Then fix toward 10.
|
||||
|
||||
## TTHW Benchmarks (Time to Hello World)
|
||||
|
||||
| Tier | Time | Adoption Impact |
|
||||
|------|------|-----------------|
|
||||
| Champion | < 2 min | 3-4x higher adoption |
|
||||
| Competitive | 2-5 min | Baseline |
|
||||
| Needs Work | 5-10 min | Significant drop-off |
|
||||
| Red Flag | > 10 min | 50-70% abandon |
|
||||
|
||||
## Hall of Fame Reference
|
||||
|
||||
During each review pass, load the relevant section from:
|
||||
\\\`${hallOfFamePath}\\\`
|
||||
|
||||
Read ONLY the section for the current pass (e.g., "## Pass 1" for Getting Started).
|
||||
Do NOT read the entire file at once. This keeps context focused.`;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { generateLearningsSearch, generateLearningsLog } from './learnings';
|
||||
import { generateConfidenceCalibration } from './confidence';
|
||||
import { generateInvokeSkill } from './composition';
|
||||
import { generateReviewArmy } from './review-army';
|
||||
import { generateDxFramework } from './dx';
|
||||
|
||||
export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
SLUG_EVAL: generateSlugEval,
|
||||
@@ -59,4 +60,5 @@ export const RESOLVERS: Record<string, ResolverFn> = {
|
||||
INVOKE_SKILL: generateInvokeSkill,
|
||||
CHANGELOG_WORKFLOW: generateChangelogWorkflow,
|
||||
REVIEW_ARMY: generateReviewArmy,
|
||||
DX_FRAMEWORK: generateDxFramework,
|
||||
};
|
||||
|
||||
@@ -508,6 +508,7 @@ Then write a \`## GSTACK REVIEW REPORT\` section to the end of the plan file:
|
||||
| Codex Review | \\\`/codex review\\\` | Independent 2nd opinion | 0 | — | — |
|
||||
| Eng Review | \\\`/plan-eng-review\\\` | Architecture & tests (required) | 0 | — | — |
|
||||
| Design Review | \\\`/plan-design-review\\\` | UI/UX gaps | 0 | — | — |
|
||||
| DX Review | \\\`/plan-devex-review\\\` | Developer experience gaps | 0 | — | — |
|
||||
|
||||
**VERDICT:** NO REVIEWS YET — run \\\`/autoplan\\\` for full review pipeline, or individual reviews above.
|
||||
\\\`\\\`\\\`
|
||||
|
||||
@@ -94,6 +94,10 @@ Parse each JSONL entry. Each skill logs different fields:
|
||||
→ Findings: "{issues_found} issues, {critical_gaps} critical gaps"
|
||||
- **plan-design-review**: \\\`status\\\`, \\\`initial_score\\\`, \\\`overall_score\\\`, \\\`unresolved\\\`, \\\`decisions_made\\\`, \\\`commit\\\`
|
||||
→ Findings: "score: {initial_score}/10 → {overall_score}/10, {decisions_made} decisions"
|
||||
- **plan-devex-review**: \\\`status\\\`, \\\`initial_score\\\`, \\\`overall_score\\\`, \\\`product_type\\\`, \\\`tthw_current\\\`, \\\`tthw_target\\\`, \\\`unresolved\\\`, \\\`commit\\\`
|
||||
→ Findings: "score: {initial_score}/10 → {overall_score}/10, TTHW: {tthw_current} → {tthw_target}"
|
||||
- **devex-review**: \\\`status\\\`, \\\`overall_score\\\`, \\\`product_type\\\`, \\\`tthw_measured\\\`, \\\`dimensions_tested\\\`, \\\`dimensions_inferred\\\`, \\\`boomerang\\\`, \\\`commit\\\`
|
||||
→ Findings: "score: {overall_score}/10, TTHW: {tthw_measured}, {dimensions_tested} tested/{dimensions_inferred} inferred"
|
||||
- **codex-review**: \\\`status\\\`, \\\`gate\\\`, \\\`findings\\\`, \\\`findings_fixed\\\`
|
||||
→ Findings: "{findings} findings, {findings_fixed}/{findings} fixed"
|
||||
|
||||
@@ -112,6 +116,7 @@ Produce this markdown table:
|
||||
| Codex Review | \\\`/codex review\\\` | Independent 2nd opinion | {runs} | {status} | {findings} |
|
||||
| Eng Review | \\\`/plan-eng-review\\\` | Architecture & tests (required) | {runs} | {status} | {findings} |
|
||||
| Design Review | \\\`/plan-design-review\\\` | UI/UX gaps | {runs} | {status} | {findings} |
|
||||
| DX Review | \\\`/plan-devex-review\\\` | Developer experience gaps | {runs} | {status} | {findings} |
|
||||
\\\`\\\`\\\`
|
||||
|
||||
Below the table, add these lines (omit any that are empty/not applicable):
|
||||
|
||||
Reference in New Issue
Block a user