Merge remote-tracking branch 'origin/main' into garrytan/learning-phase-2.5-clean

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
This commit is contained in:
Garry Tan
2026-04-04 13:41:53 -07:00
83 changed files with 6849 additions and 253 deletions
+75
View File
@@ -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.
+195
View File
@@ -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"
+71 -3
View File
@@ -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;
+85
View File
@@ -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.`;
}
+2
View File
@@ -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,
};
+1
View File
@@ -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.
\\\`\\\`\\\`
+5
View File
@@ -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):