mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-10 07:07:34 +02:00
merge: incorporate origin/main into community-mode branch
Resolved 10 conflicted files:
- VERSION/package.json: kept 0.12.0.0 (feature branch version)
- CHANGELOG.md: preserved both branch entry and main's new entries
- supabase/config.sh: kept GSTACK_WEB_URL, accepted TELEMETRY_ENDPOINT removal
- bin/gstack-{community-dashboard,telemetry-log,telemetry-sync,update-check}:
took main's improved versions (edge function approach, safe cursor, UUID gen)
- supabase/functions/community-pulse: took main's count-based approach
- test/telemetry.test.ts: took main's structure with fingerprint field name
Post-merge fixes:
- Removed shadowed local RESOLVERS/functions in gen-skill-docs.ts (main's
resolver imports now take precedence for tier-based preamble, coverage gates)
- Added 3 missing E2E_TIERS entries (ship-plan-*, review-plan-completion)
- Updated telemetry test to match current prompt text
- Regenerated all SKILL.md files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
name: Periodic Evals
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Monday 6 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: evals-periodic
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}/ci
|
||||
EVALS_TIER: periodic
|
||||
EVALS_ALL: 1 # Ignore diff — run all periodic tests
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: meta
|
||||
run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json') }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if image exists
|
||||
id: check
|
||||
run: |
|
||||
if docker manifest inspect ${{ steps.meta.outputs.tag }} > /dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- if: steps.check.outputs.exists == 'false'
|
||||
run: cp package.json .github/docker/
|
||||
|
||||
- if: steps.check.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .github/docker
|
||||
file: .github/docker/Dockerfile.ci
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tag }}
|
||||
${{ env.IMAGE }}:latest
|
||||
|
||||
evals:
|
||||
runs-on: ubicloud-standard-2
|
||||
needs: build-image
|
||||
container:
|
||||
image: ${{ needs.build-image.outputs.image-tag }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
options: --user runner
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
suite:
|
||||
- name: e2e-plan
|
||||
file: test/skill-e2e-plan.test.ts
|
||||
- name: e2e-design
|
||||
file: test/skill-e2e-design.test.ts
|
||||
- name: e2e-qa-bugs
|
||||
file: test/skill-e2e-qa-bugs.test.ts
|
||||
- name: e2e-qa-workflow
|
||||
file: test/skill-e2e-qa-workflow.test.ts
|
||||
- name: e2e-review
|
||||
file: test/skill-e2e-review.test.ts
|
||||
- name: e2e-workflow
|
||||
file: test/skill-e2e-workflow.test.ts
|
||||
- name: e2e-routing
|
||||
file: test/skill-routing-e2e.test.ts
|
||||
- name: e2e-codex
|
||||
file: test/codex-e2e.test.ts
|
||||
- name: e2e-gemini
|
||||
file: test/gemini-e2e.test.ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fix bun temp
|
||||
run: |
|
||||
mkdir -p /home/runner/.cache/bun
|
||||
{
|
||||
echo "BUN_INSTALL_CACHE_DIR=/home/runner/.cache/bun"
|
||||
echo "BUN_TMPDIR=/home/runner/.cache/bun"
|
||||
echo "TMPDIR=/home/runner/.cache"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore deps
|
||||
run: |
|
||||
if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then
|
||||
ln -s /opt/node_modules_cache node_modules
|
||||
else
|
||||
bun install
|
||||
fi
|
||||
|
||||
- run: bun run build
|
||||
|
||||
- name: Run ${{ matrix.suite.name }}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
EVALS_CONCURRENCY: "40"
|
||||
PLAYWRIGHT_BROWSERS_PATH: /opt/playwright-browsers
|
||||
run: EVALS=1 bun test --retry 2 --concurrent --max-concurrency 40 ${{ matrix.suite.file }}
|
||||
|
||||
- name: Upload eval results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eval-periodic-${{ matrix.suite.name }}
|
||||
path: ~/.gstack-dev/evals/*.json
|
||||
retention-days: 90
|
||||
@@ -10,6 +10,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}/ci
|
||||
EVALS_TIER: gate
|
||||
|
||||
jobs:
|
||||
# Build Docker image with pre-baked toolchain (cached — only rebuilds on Dockerfile/lockfile change)
|
||||
@@ -87,10 +88,8 @@ jobs:
|
||||
file: test/skill-e2e-review.test.ts
|
||||
- name: e2e-workflow
|
||||
file: test/skill-e2e-workflow.test.ts
|
||||
allow_failure: true # /ship + /setup-browser-cookies are env-dependent
|
||||
- name: e2e-routing
|
||||
file: test/skill-routing-e2e.test.ts
|
||||
allow_failure: true # LLM routing is non-deterministic
|
||||
- name: e2e-codex
|
||||
file: test/codex-e2e.test.ts
|
||||
- name: e2e-gemini
|
||||
@@ -131,7 +130,6 @@ jobs:
|
||||
bun -e "import {chromium} from 'playwright';const b=await chromium.launch({args:['--no-sandbox']});console.log('Chromium OK');await b.close()"
|
||||
|
||||
- name: Run ${{ matrix.suite.name }}
|
||||
continue-on-error: ${{ matrix.suite.allow_failure || false }}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
@@ -16,3 +16,4 @@ bun.lock
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.example
|
||||
supabase/.temp/
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ The server writes `.gstack/browse.json` (atomic write via tmp + rename, mode 0o6
|
||||
{ "pid": 12345, "port": 34567, "token": "uuid-v4", "startedAt": "...", "binaryVersion": "abc123" }
|
||||
```
|
||||
|
||||
The CLI reads this file to find the server. If the file is missing, stale, or the PID is dead, the CLI spawns a new server.
|
||||
The CLI reads this file to find the server. If the file is missing or the server fails an HTTP health check, the CLI spawns a new server. On Windows, PID-based process detection is unreliable in Bun binaries, so the health check (GET /health) is the primary liveness signal on all platforms.
|
||||
|
||||
### Port selection
|
||||
|
||||
|
||||
@@ -33,6 +33,97 @@
|
||||
- Homebrew tap deferred to TODOS.md (P2)
|
||||
- 8 new telemetry tests (source field, duration caps, fingerprint persistence)
|
||||
|
||||
## [0.11.18.1] - 2026-03-24
|
||||
|
||||
### Changed
|
||||
|
||||
- **One decision per question — everywhere.** Every skill now presents decisions one at a time, each with its own focused question, recommendation, and options. No more wall-of-text questions that bundle unrelated choices together. This was already enforced in the three plan-review skills; now it's a universal rule across all 23+ skills.
|
||||
|
||||
## [0.11.18.0] - 2026-03-24 — Ship With Teeth
|
||||
|
||||
`/ship` and `/review` now actually enforce the quality gates they've been talking about. Coverage audit becomes a real gate (not just a diagram), plan completion gets verified against the diff, and verification steps from your plan run automatically.
|
||||
|
||||
### Added
|
||||
|
||||
- **Test coverage gate in /ship.** AI-assessed coverage below 60% is a hard stop. 60-79% gets a prompt. 80%+ passes. Thresholds are configurable per-project via `## Test Coverage` in CLAUDE.md.
|
||||
- **Coverage warning in /review.** Low coverage is now flagged prominently before you reach the /ship gate, so you can write tests early.
|
||||
- **Plan completion audit.** /ship reads your plan file, extracts every actionable item, cross-references against the diff, and shows you a DONE/NOT DONE/PARTIAL/CHANGED checklist. Missing items are a shipping blocker (with override).
|
||||
- **Plan-aware scope drift detection.** /review's scope drift check now reads the plan file too — not just TODOS.md and PR description.
|
||||
- **Auto-verification via /qa-only.** /ship reads your plan's verification section and runs /qa-only inline to test it — if a dev server is running on localhost. No server, no problem — it skips gracefully.
|
||||
- **Shared plan file discovery.** Conversation context first, content-based grep fallback second. Used by plan completion, plan review reports, and verification.
|
||||
- **Ship metrics logging.** Coverage %, plan completion ratio, and verification results are logged to review JSONL for /retro to track trends.
|
||||
- **Plan completion in /retro.** Weekly retros now show plan completion rates across shipped branches.
|
||||
|
||||
## [0.11.17.0] - 2026-03-24 — Cleaner Skill Descriptions + Proactive Opt-Out
|
||||
|
||||
### Changed
|
||||
|
||||
- **Skill descriptions are now clean and readable.** Removed the ugly "MANUAL TRIGGER ONLY" prefix from every skill description that was wasting 58 characters and causing build errors for Codex integration.
|
||||
- **You can now opt out of proactive skill suggestions.** The first time you run any gstack skill, you'll be asked whether you want gstack to suggest skills during your workflow. If you prefer to invoke skills manually, just say no — it's saved as a global setting. You can change your mind anytime with `gstack-config set proactive true/false`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Telemetry source tagging no longer crashes.** Fixed duration guards and source field validation in the telemetry logger so it handles edge cases cleanly instead of erroring.
|
||||
|
||||
## [0.11.16.1] - 2026-03-24 — Installation ID Privacy Fix
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Installation IDs are now random UUIDs instead of hostname hashes.** The old `SHA-256(hostname+username)` approach meant anyone who knew your machine identity could compute your installation ID. Now uses a random UUID stored in `~/.gstack/installation-id` — not derivable from any public input, rotatable by deleting the file.
|
||||
- **RLS verification script handles edge cases.** `verify-rls.sh` now correctly treats INSERT success as expected (kept for old client compat), handles 409 conflicts and 204 no-ops.
|
||||
|
||||
## [0.11.16.0] - 2026-03-24 — Smarter CI + Telemetry Security
|
||||
|
||||
### Changed
|
||||
|
||||
- **CI runs only gate tests by default — periodic tests run weekly.** Every E2E test is now classified as `gate` (blocks PRs) or `periodic` (weekly cron + on-demand). Gate tests cover functional correctness and safety guardrails. Periodic tests cover expensive Opus quality benchmarks, non-deterministic routing tests, and tests requiring external services (Codex, Gemini). CI feedback is faster and cheaper while quality benchmarks still run weekly.
|
||||
- **Global touchfiles are now granular.** Previously, changing `gen-skill-docs.ts` triggered all 56 E2E tests. Now only the ~27 tests that actually depend on it run. Same for `llm-judge.ts`, `test-server.ts`, `worktree.ts`, and the Codex/Gemini session runners. The truly global list is down to 3 files (session-runner, eval-store, touchfiles.ts itself).
|
||||
- **New `test:gate` and `test:periodic` scripts** replace `test:e2e:fast`. Use `EVALS_TIER=gate` or `EVALS_TIER=periodic` to filter tests by tier.
|
||||
- **Telemetry sync uses `GSTACK_SUPABASE_URL` instead of `GSTACK_TELEMETRY_ENDPOINT`.** Edge functions need the base URL, not the REST API path. The old variable is removed from `config.sh`.
|
||||
- **Cursor advancement is now safe.** The sync script checks the edge function's `inserted` count before advancing — if zero events were inserted, the cursor holds and retries next run.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Telemetry RLS policies tightened.** Row-level security policies on all telemetry tables now deny direct access via the anon key. All reads and writes go through validated edge functions with schema checks, event type allowlists, and field length limits.
|
||||
- **Community dashboard is faster and server-cached.** Dashboard stats are now served from a single edge function with 1-hour server-side caching, replacing multiple direct queries.
|
||||
|
||||
### For contributors
|
||||
|
||||
- `E2E_TIERS` map in `test/helpers/touchfiles.ts` classifies every test — a free validation test ensures it stays in sync with `E2E_TOUCHFILES`
|
||||
- `EVALS_FAST` / `FAST_EXCLUDED_TESTS` removed in favor of `EVALS_TIER`
|
||||
- `allow_failure` removed from CI matrix (gate tests should be reliable)
|
||||
- New `.github/workflows/evals-periodic.yml` runs periodic tests Monday 6 AM UTC
|
||||
- New migration: `supabase/migrations/002_tighten_rls.sql`
|
||||
- New smoke test: `supabase/verify-rls.sh` (9 checks: 5 reads + 4 writes)
|
||||
- Extended `test/telemetry.test.ts` with field name verification
|
||||
- Untracked `browse/dist/` binaries from git (arm64-only, rebuilt by `./setup`)
|
||||
|
||||
## [0.11.15.0] - 2026-03-24 — E2E Test Coverage for Plan Reviews & Codex
|
||||
|
||||
### Added
|
||||
|
||||
- **E2E tests verify plan review reports appear at the bottom of plans.** The `/plan-eng-review` review report is now tested end-to-end — if it stops writing `## GSTACK REVIEW REPORT` to the plan file, the test catches it.
|
||||
- **E2E tests verify Codex is offered in every plan skill.** Four new lightweight tests confirm that `/office-hours`, `/plan-ceo-review`, `/plan-design-review`, and `/plan-eng-review` all check for Codex availability, prompt the user, and handle the fallback when Codex is unavailable.
|
||||
|
||||
### For contributors
|
||||
|
||||
- New E2E tests in `test/skill-e2e-plan.test.ts`: `plan-review-report`, `codex-offered-eng-review`, `codex-offered-ceo-review`, `codex-offered-office-hours`, `codex-offered-design-review`
|
||||
- Updated touchfile mappings and selection count assertions
|
||||
- Added `touchfiles` to the documented global touchfile list in CLAUDE.md
|
||||
|
||||
## [0.11.14.0] - 2026-03-24 — Windows Browse Fix
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Browse engine now works on Windows.** Three compounding bugs blocked all Windows `/browse` users: the server process died when the CLI exited (Bun's `unref()` doesn't truly detach on Windows), the health check never ran because `process.kill(pid, 0)` is broken in Bun binaries on Windows, and Chromium's sandbox failed when spawned through the Bun→Node process chain. All three are now fixed. Credits to @fqueiro (PR #191) for identifying the `detached: true` approach.
|
||||
- **Health check runs first on all platforms.** `ensureServer()` now tries an HTTP health check before falling back to PID-based detection — more reliable on every OS, not just Windows.
|
||||
- **Startup errors are logged to disk.** When the server fails to start, errors are written to `~/.gstack/browse-startup-error.log` so Windows users (who lose stderr due to process detachment) can debug.
|
||||
- **Chromium sandbox disabled on Windows.** Chromium's sandbox requires elevated privileges when spawned through the Bun→Node chain — now disabled on Windows only.
|
||||
|
||||
### For contributors
|
||||
|
||||
- New tests for `isServerHealthy()` and startup error logging in `browse/test/config.test.ts`
|
||||
|
||||
## [0.11.13.0] - 2026-03-24 — Worktree Isolation + Infrastructure Elegance
|
||||
|
||||
### Added
|
||||
|
||||
@@ -7,6 +7,8 @@ bun install # install dependencies
|
||||
bun test # run free tests (browse + snapshot + skill validation)
|
||||
bun run test:evals # run paid evals: LLM judge + E2E (diff-based, ~$4/run max)
|
||||
bun run test:evals:all # run ALL paid evals regardless of diff
|
||||
bun run test:gate # run gate-tier tests only (CI default, blocks merge)
|
||||
bun run test:periodic # run periodic-tier tests only (weekly cron / manual)
|
||||
bun run test:e2e # run E2E tests only (diff-based, ~$3.85/run max)
|
||||
bun run test:e2e:all # run ALL E2E tests regardless of diff
|
||||
bun run eval:select # show which tests would run based on current diff
|
||||
@@ -29,9 +31,17 @@ against the previous run.
|
||||
**Diff-based test selection:** `test:evals` and `test:e2e` auto-select tests based
|
||||
on `git diff` against the base branch. Each test declares its file dependencies in
|
||||
`test/helpers/touchfiles.ts`. Changes to global touchfiles (session-runner, eval-store,
|
||||
llm-judge, gen-skill-docs) trigger all tests. Use `EVALS_ALL=1` or the `:all` script
|
||||
touchfiles.ts itself) trigger all tests. Use `EVALS_ALL=1` or the `:all` script
|
||||
variants to force all tests. Run `eval:select` to preview which tests would run.
|
||||
|
||||
**Two-tier system:** Tests are classified as `gate` or `periodic` in `E2E_TIERS`
|
||||
(in `test/helpers/touchfiles.ts`). CI runs only gate tests (`EVALS_TIER=gate`);
|
||||
periodic tests run weekly via cron or manually. Use `EVALS_TIER=gate` or
|
||||
`EVALS_TIER=periodic` to filter. When adding new E2E tests, classify them:
|
||||
1. Safety guardrail or deterministic functional test? -> `gate`
|
||||
2. Quality benchmark, Opus model test, or non-deterministic? -> `periodic`
|
||||
3. Requires external service (Codex, Gemini)? -> `periodic`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
@@ -165,6 +175,19 @@ symlink or a real copy. If it's a symlink to your working directory, be aware th
|
||||
gen-skill-docs pipeline, consider whether the changes should be tested in isolation
|
||||
before going live (especially if the user is actively using gstack in other windows).
|
||||
|
||||
## Compiled binaries — NEVER commit browse/dist/
|
||||
|
||||
The `browse/dist/` directory contains compiled Bun binaries (`browse`, `find-browse`,
|
||||
~58MB each). These are Mach-O arm64 only — they do NOT work on Linux, Windows, or
|
||||
Intel Macs. The `./setup` script already builds from source for every platform, so
|
||||
the checked-in binaries are redundant. They are tracked by git due to a historical
|
||||
mistake and should eventually be removed with `git rm --cached`.
|
||||
|
||||
**NEVER stage or commit these files.** They show up as modified in `git status`
|
||||
because they're tracked despite `.gitignore` — ignore them. When staging files,
|
||||
always use specific filenames (`git add file1 file2`) — never `git add .` or
|
||||
`git add -A`, which will accidentally include the binaries.
|
||||
|
||||
## Commit style
|
||||
|
||||
**Always bisect commits.** Every commit should be a single logical change. When
|
||||
|
||||
@@ -216,7 +216,7 @@ gstack includes **opt-in** usage telemetry to help improve the project. Here's e
|
||||
- **What's never sent:** code, file paths, repo names, branch names, prompts, or any user-generated content.
|
||||
- **Change anytime:** `gstack-config set telemetry off` disables everything instantly.
|
||||
|
||||
Data is stored in [Supabase](https://supabase.com) (open source Firebase alternative). The schema is in [`supabase/migrations/001_telemetry.sql`](supabase/migrations/001_telemetry.sql) — you can verify exactly what's collected. The Supabase publishable key in the repo is a public key (like a Firebase API key) — row-level security policies restrict it to insert-only access.
|
||||
Data is stored in [Supabase](https://supabase.com) (open source Firebase alternative). The schema is in [`supabase/migrations/`](supabase/migrations/) — you can verify exactly what's collected. The Supabase publishable key in the repo is a public key (like a Firebase API key) — row-level security policies deny all direct access. Telemetry flows through validated edge functions that enforce schema checks, event type allowlists, and field length limits.
|
||||
|
||||
**Local analytics are always available.** Run `gstack-analytics` to see your personal usage dashboard from the local JSONL file — no remote data needed.
|
||||
|
||||
|
||||
@@ -3,19 +3,10 @@ name: gstack
|
||||
preamble-tier: 1
|
||||
version: 1.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /gstack.
|
||||
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.
|
||||
Also suggest adjacent gstack skills by stage: brainstorm /office-hours; strategy
|
||||
/plan-ceo-review; architecture /plan-eng-review; design /plan-design-review or
|
||||
/design-consultation; auto-review /autoplan; debugging /investigate; QA /qa; code review
|
||||
/review; visual audit /design-review; shipping /ship; docs /document-release; retro
|
||||
/retro; second opinion /codex; prod safety /careful or /guard; scoped edits /freeze or
|
||||
/unfreeze; gstack upgrades /gstack-upgrade. If the user opts out of suggestions, stop
|
||||
and run gstack-config set proactive false; if they opt back in, run gstack-config set
|
||||
proactive true.
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -36,9 +27,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -56,8 +49,11 @@ echo '{"skill":"gstack","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basen
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -106,6 +102,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## Contributor Mode
|
||||
|
||||
If `_CONTRIB` is `true`: you are in **contributor mode**. At the end of each major workflow step, rate your gstack experience 0-10. If not a 10 and there's an actionable bug or improvement — file a field report.
|
||||
|
||||
@@ -7,14 +7,6 @@ description: |
|
||||
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.
|
||||
Also suggest adjacent gstack skills by stage: brainstorm /office-hours; strategy
|
||||
/plan-ceo-review; architecture /plan-eng-review; design /plan-design-review or
|
||||
/design-consultation; auto-review /autoplan; debugging /investigate; QA /qa; code review
|
||||
/review; visual audit /design-review; shipping /ship; docs /document-release; retro
|
||||
/retro; second opinion /codex; prod safety /careful or /guard; scoped edits /freeze or
|
||||
/unfreeze; gstack upgrades /gstack-upgrade. If the user opts out of suggestions, stop
|
||||
and run gstack-config set proactive false; if they opt back in, run gstack-config set
|
||||
proactive true.
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: autoplan
|
||||
preamble-tier: 3
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /autoplan.
|
||||
Auto-review pipeline — reads the full CEO, design, and eng review skills from disk
|
||||
and runs them sequentially with auto-decisions using 6 decision principles. Surfaces
|
||||
taste decisions (close approaches, borderline scope, codex disagreements) at a final
|
||||
@@ -37,9 +36,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -57,8 +58,11 @@ echo '{"skill":"autoplan","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(bas
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -107,6 +111,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: benchmark
|
||||
preamble-tier: 1
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /benchmark.
|
||||
Performance regression detection using the browse daemon. Establishes
|
||||
baselines for page load times, Core Web Vitals, and resource sizes.
|
||||
Compares before/after on every PR. Tracks performance trends over time.
|
||||
@@ -30,9 +29,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -50,8 +51,11 @@ echo '{"skill":"benchmark","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(ba
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -100,6 +104,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## Contributor Mode
|
||||
|
||||
If `_CONTRIB` is `true`: you are in **contributor mode**. At the end of each major workflow step, rate your gstack experience 0-10. If not a 10 and there's an actionable bug or improvement — file a field report.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-community-dashboard — community usage stats from Supabase
|
||||
#
|
||||
# Queries the Supabase REST API to show community-wide gstack usage:
|
||||
# Calls the community-pulse edge function for aggregated stats:
|
||||
# skill popularity, crash clusters, version distribution, retention.
|
||||
#
|
||||
# Env overrides (for testing):
|
||||
@@ -30,46 +30,40 @@ if [ -z "$SUPABASE_URL" ] || [ -z "$ANON_KEY" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── Helper: query Supabase REST API ─────────────────────────
|
||||
query() {
|
||||
local table="$1"
|
||||
local params="${2:-}"
|
||||
curl -sf --max-time 10 \
|
||||
"${SUPABASE_URL}/rest/v1/${table}?${params}" \
|
||||
-H "apikey: ${ANON_KEY}" \
|
||||
-H "Authorization: Bearer ${ANON_KEY}" \
|
||||
2>/dev/null || echo "[]"
|
||||
}
|
||||
# ─── Fetch aggregated stats from edge function ────────────────
|
||||
DATA="$(curl -sf --max-time 15 \
|
||||
"${SUPABASE_URL}/functions/v1/community-pulse" \
|
||||
-H "apikey: ${ANON_KEY}" \
|
||||
2>/dev/null || echo "{}")"
|
||||
|
||||
echo "gstack community dashboard"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# ─── Weekly active installs ──────────────────────────────────
|
||||
WEEK_AGO="$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")"
|
||||
if [ -n "$WEEK_AGO" ]; then
|
||||
# Direct REST query (replaces unreliable community-pulse edge function)
|
||||
WEEKLY="$(curl -sf --max-time 10 \
|
||||
"${SUPABASE_URL}/rest/v1/update_checks?select=install_fingerprint&checked_at=gte.${WEEK_AGO}&source=eq.live" \
|
||||
-H "apikey: ${ANON_KEY}" \
|
||||
-H "Authorization: Bearer ${ANON_KEY}" \
|
||||
2>/dev/null | grep -o '"install_fingerprint":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
|
||||
WEEKLY="${WEEKLY:-0}"
|
||||
WEEKLY="$(echo "$DATA" | grep -o '"weekly_active":[0-9]*' | grep -o '[0-9]*' || echo "0")"
|
||||
CHANGE="$(echo "$DATA" | grep -o '"change_pct":[0-9-]*' | grep -o '[0-9-]*' || echo "0")"
|
||||
|
||||
echo "Weekly active installs: ${WEEKLY} unique"
|
||||
echo ""
|
||||
echo "Weekly active installs: ${WEEKLY}"
|
||||
if [ "$CHANGE" -gt 0 ] 2>/dev/null; then
|
||||
echo " Change: +${CHANGE}%"
|
||||
elif [ "$CHANGE" -lt 0 ] 2>/dev/null; then
|
||||
echo " Change: ${CHANGE}%"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ─── Skill popularity (top 10) ───────────────────────────────
|
||||
echo "Top skills (last 7 days)"
|
||||
echo "────────────────────────"
|
||||
|
||||
# Query telemetry_events, group by skill
|
||||
EVENTS="$(query "telemetry_events" "select=skill,gstack_version,session_id&event_type=eq.skill_run&event_timestamp=gte.${WEEK_AGO}&source=eq.live&limit=1000" 2>/dev/null || echo "[]")"
|
||||
|
||||
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
|
||||
echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -10 | while read -r COUNT SKILL; do
|
||||
printf " /%-20s %d runs\n" "$SKILL" "$COUNT"
|
||||
# Parse top_skills array from JSON
|
||||
SKILLS="$(echo "$DATA" | grep -o '"top_skills":\[[^]]*\]' || echo "")"
|
||||
if [ -n "$SKILLS" ] && [ "$SKILLS" != '"top_skills":[]' ]; then
|
||||
# Parse each object — handle any key order (JSONB doesn't preserve order)
|
||||
echo "$SKILLS" | grep -o '{[^}]*}' | while read -r OBJ; do
|
||||
SKILL="$(echo "$OBJ" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')"
|
||||
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
|
||||
[ -n "$SKILL" ] && [ -n "$COUNT" ] && printf " /%-20s %s runs\n" "$SKILL" "$COUNT"
|
||||
done
|
||||
else
|
||||
echo " No data yet"
|
||||
@@ -80,31 +74,13 @@ echo ""
|
||||
echo "Top errors (last 7 days)"
|
||||
echo "────────────────────────"
|
||||
|
||||
ERRORS="$(query "telemetry_events" "select=skill,error_class,error_message,failed_step,duration_s,session_id&outcome=eq.error&event_timestamp=gte.${WEEK_AGO}&source=eq.live&order=event_timestamp.desc&limit=200" 2>/dev/null || echo "[]")"
|
||||
|
||||
if [ "$ERRORS" != "[]" ] && [ -n "$ERRORS" ]; then
|
||||
# Group by skill + error_class, show count and example message
|
||||
echo "$ERRORS" | grep -o '"skill":"[^"]*"[^}]*"error_class":"[^"]*"' | \
|
||||
sed 's/.*"skill":"//;s/".*"error_class":"/\t/' | sed 's/"$//' | \
|
||||
sort | uniq -c | sort -rn | head -8 | while read -r COUNT COMBO; do
|
||||
SKILL="$(echo "$COMBO" | cut -f1)"
|
||||
ERR="$(echo "$COMBO" | cut -f2)"
|
||||
# Find an example error_message for this combo
|
||||
MSG="$(echo "$ERRORS" | grep -o "\"skill\":\"${SKILL}\"[^}]*\"error_message\":\"[^\"]*\"" | \
|
||||
grep -o '"error_message":"[^"]*"' | head -1 | sed 's/"error_message":"//;s/"$//' || true)"
|
||||
# Find an example failed_step
|
||||
STEP="$(echo "$ERRORS" | grep -o "\"skill\":\"${SKILL}\"[^}]*\"failed_step\":\"[^\"]*\"" | \
|
||||
grep -o '"failed_step":"[^"]*"' | head -1 | sed 's/"failed_step":"//;s/"$//' || true)"
|
||||
|
||||
printf " /%-12s %-18s %3d errors\n" "$SKILL" "${ERR:-unknown}" "$COUNT"
|
||||
[ -n "$STEP" ] && printf " step: %s\n" "$STEP"
|
||||
[ -n "$MSG" ] && printf " e.g.: %s\n" "$(echo "$MSG" | head -c 80)"
|
||||
done
|
||||
|
||||
# Show how many unique sessions have errors
|
||||
ERR_SESSIONS="$(echo "$ERRORS" | grep -o '"session_id":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
|
||||
echo ""
|
||||
echo " ${ERR_SESSIONS} unique session(s) with errors"
|
||||
CRASHES="$(echo "$DATA" | grep -o '"crashes":\[[^]]*\]' || echo "")"
|
||||
if [ -n "$CRASHES" ] && [ "$CRASHES" != '"crashes":[]' ]; then
|
||||
echo "$CRASHES" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
|
||||
ERR="$(echo "$OBJ" | grep -o '"error_class":"[^"]*"' | awk -F'"' '{print $4}')"
|
||||
C="$(echo "$OBJ" | grep -o '"total_occurrences":[0-9]*' | grep -o '[0-9]*')"
|
||||
[ -n "$ERR" ] && printf " %-30s %s occurrences\n" "$ERR" "${C:-?}"
|
||||
done
|
||||
else
|
||||
echo " No errors reported"
|
||||
fi
|
||||
@@ -114,49 +90,17 @@ echo ""
|
||||
echo "Version distribution (last 7 days)"
|
||||
echo "───────────────────────────────────"
|
||||
|
||||
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
|
||||
echo "$EVENTS" | grep -o '"gstack_version":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -5 | while read -r COUNT VER; do
|
||||
printf " v%-15s %d events\n" "$VER" "$COUNT"
|
||||
VERSIONS="$(echo "$DATA" | grep -o '"versions":\[[^]]*\]' || echo "")"
|
||||
if [ -n "$VERSIONS" ] && [ "$VERSIONS" != '"versions":[]' ]; then
|
||||
echo "$VERSIONS" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
|
||||
VER="$(echo "$OBJ" | grep -o '"version":"[^"]*"' | awk -F'"' '{print $4}')"
|
||||
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
|
||||
[ -n "$VER" ] && [ -n "$COUNT" ] && printf " v%-15s %s events\n" "$VER" "$COUNT"
|
||||
done
|
||||
else
|
||||
echo " No data yet"
|
||||
fi
|
||||
|
||||
# ─── Sessions (distinct session_id, works for all tiers) ────
|
||||
echo "Sessions (last 7 days)"
|
||||
echo "──────────────────────"
|
||||
|
||||
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
|
||||
SESSION_COUNT="$(echo "$EVENTS" | grep -o '"session_id":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
|
||||
echo " ${SESSION_COUNT} unique sessions"
|
||||
else
|
||||
echo " No session data"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ─── Skill recommendations ─────────────────────────────────
|
||||
# Fetch top skills for recommendations
|
||||
TOP_SKILLS="$(echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -3 | awk '{print $2}' | tr '\n' ',' | sed 's/,$//')"
|
||||
|
||||
if [ -n "$TOP_SKILLS" ]; then
|
||||
RECS="$(curl -sf --max-time 10 \
|
||||
"${SUPABASE_URL}/functions/v1/community-recommendations?skills=${TOP_SKILLS}" \
|
||||
-H "Authorization: Bearer ${ANON_KEY}" \
|
||||
2>/dev/null || echo '{"recommendations":[]}')"
|
||||
|
||||
REC_LIST="$(echo "$RECS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')"
|
||||
REC_REASONS="$(echo "$RECS" | grep -o '"reason":"[^"]*"' | awk -F'"' '{print $4}')"
|
||||
|
||||
if [ -n "$REC_LIST" ]; then
|
||||
echo "Skills you might like"
|
||||
echo "─────────────────────"
|
||||
paste <(echo "$REC_LIST") <(echo "$REC_REASONS") 2>/dev/null | while IFS=$'\t' read -r SKILL REASON; do
|
||||
[ -z "$SKILL" ] && continue
|
||||
printf " /%-20s %s\n" "$SKILL" "${REASON:-}"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "For local analytics: gstack-analytics"
|
||||
echo "For benchmarks: gstack-community-benchmarks"
|
||||
|
||||
@@ -115,17 +115,26 @@ if [ -d "$STATE_DIR/sessions" ]; then
|
||||
fi
|
||||
|
||||
# Generate/read persistent UUID fingerprint (all tiers, not just community)
|
||||
# Uses a random UUID stored locally — not derived from hostname/user so it
|
||||
# can't be guessed or correlated by someone who knows your machine identity.
|
||||
INSTALL_FP=""
|
||||
FP_FILE="$STATE_DIR/.install-id"
|
||||
if [ -f "$FP_FILE" ]; then
|
||||
INSTALL_FP="$(cat "$FP_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||
fi
|
||||
if [ -z "$INSTALL_FP" ]; then
|
||||
INSTALL_FP="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null || echo "")"
|
||||
INSTALL_FP="$(echo "$INSTALL_FP" | tr '[:upper:]' '[:lower:]')" # normalize case
|
||||
# Generate a random UUID v4
|
||||
if command -v uuidgen >/dev/null 2>&1; then
|
||||
INSTALL_FP="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
||||
elif [ -r /proc/sys/kernel/random/uuid ]; then
|
||||
INSTALL_FP="$(cat /proc/sys/kernel/random/uuid)"
|
||||
else
|
||||
# Fallback: random hex from /dev/urandom
|
||||
INSTALL_FP="$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n')"
|
||||
fi
|
||||
if [ -n "$INSTALL_FP" ]; then
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$INSTALL_FP" > "$FP_FILE"
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||
printf '%s' "$INSTALL_FP" > "$FP_FILE" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+25
-36
@@ -3,11 +3,12 @@
|
||||
#
|
||||
# Fire-and-forget, backgrounded, rate-limited to once per 5 minutes.
|
||||
# Strips local-only fields before sending. Respects privacy tiers.
|
||||
# Posts to the telemetry-ingest edge function (not PostgREST directly).
|
||||
#
|
||||
# Env overrides (for testing):
|
||||
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
||||
# GSTACK_DIR — override auto-detected gstack root
|
||||
# GSTACK_TELEMETRY_ENDPOINT — override Supabase endpoint URL
|
||||
# GSTACK_SUPABASE_URL — override Supabase project URL
|
||||
set -uo pipefail
|
||||
|
||||
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
@@ -19,15 +20,15 @@ RATE_FILE="$ANALYTICS_DIR/.last-sync-time"
|
||||
CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
|
||||
|
||||
# Source Supabase config if not overridden by env
|
||||
if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
||||
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
||||
. "$GSTACK_DIR/supabase/config.sh"
|
||||
fi
|
||||
ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
|
||||
SUPABASE_URL="${GSTACK_SUPABASE_URL:-}"
|
||||
ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
||||
|
||||
# ─── Pre-checks ──────────────────────────────────────────────
|
||||
# No endpoint configured yet → exit silently
|
||||
[ -z "$ENDPOINT" ] && exit 0
|
||||
# No Supabase URL configured yet → exit silently
|
||||
[ -z "$SUPABASE_URL" ] && exit 0
|
||||
|
||||
# No JSONL file → nothing to sync
|
||||
[ -f "$JSONL_FILE" ] || exit 0
|
||||
@@ -66,6 +67,8 @@ UNSENT="$(tail -n "+$SKIP" "$JSONL_FILE" 2>/dev/null || true)"
|
||||
[ -z "$UNSENT" ] && exit 0
|
||||
|
||||
# ─── Strip local-only fields and build batch ─────────────────
|
||||
# Edge function expects raw JSONL field names (v, ts, sessions) —
|
||||
# no column renaming needed (the function maps them internally).
|
||||
BATCH="["
|
||||
FIRST=true
|
||||
COUNT=0
|
||||
@@ -75,15 +78,10 @@ while IFS= read -r LINE; do
|
||||
[ -z "$LINE" ] && continue
|
||||
echo "$LINE" | grep -q '^{' || continue
|
||||
|
||||
# Strip local-only fields + map JSONL field names to Postgres column names
|
||||
# Backward compat: map old installation_id → install_fingerprint for unsent entries
|
||||
# Strip local-only fields (keep v, ts, sessions as-is for edge function)
|
||||
CLEAN="$(echo "$LINE" | sed \
|
||||
-e 's/,"_repo_slug":"[^"]*"//g' \
|
||||
-e 's/,"_branch":"[^"]*"//g' \
|
||||
-e 's/"v":/"schema_version":/g' \
|
||||
-e 's/"ts":/"event_timestamp":/g' \
|
||||
-e 's/"sessions":/"concurrent_sessions":/g' \
|
||||
-e 's/"installation_id":/"install_fingerprint":/g' \
|
||||
-e 's/,"repo":"[^"]*"//g')"
|
||||
|
||||
if [ "$FIRST" = "true" ]; then
|
||||
@@ -103,40 +101,31 @@ BATCH="$BATCH]"
|
||||
# Nothing to send after filtering
|
||||
[ "$COUNT" -eq 0 ] && exit 0
|
||||
|
||||
# ─── POST to Supabase ────────────────────────────────────────
|
||||
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
|
||||
-X POST "${ENDPOINT}/telemetry_events" \
|
||||
# ─── POST to edge function ───────────────────────────────────
|
||||
RESP_FILE="$(mktemp /tmp/gstack-sync-XXXXXX 2>/dev/null || echo "/tmp/gstack-sync-$$")"
|
||||
HTTP_CODE="$(curl -s -w '%{http_code}' --max-time 10 \
|
||||
-X POST "${SUPABASE_URL}/functions/v1/telemetry-ingest" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "apikey: ${ANON_KEY}" \
|
||||
-H "Authorization: Bearer ${ANON_KEY}" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-o "$RESP_FILE" \
|
||||
-d "$BATCH" 2>/dev/null || echo "000")"
|
||||
|
||||
# ─── Update cursor on success (2xx) ─────────────────────────
|
||||
case "$HTTP_CODE" in
|
||||
2*) NEW_CURSOR=$(( CURSOR + COUNT ))
|
||||
2*)
|
||||
# Parse inserted count from response — only advance if events were actually inserted.
|
||||
# Advance by SENT count (not inserted count) because we can't map inserted back to
|
||||
# source lines. If inserted==0, something is systemically wrong — don't advance.
|
||||
INSERTED="$(grep -o '"inserted":[0-9]*' "$RESP_FILE" 2>/dev/null | grep -o '[0-9]*' || echo "0")"
|
||||
if [ "${INSERTED:-0}" -gt 0 ] 2>/dev/null; then
|
||||
NEW_CURSOR=$(( CURSOR + COUNT ))
|
||||
echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true
|
||||
|
||||
# Ping update_checks (install base proxy)
|
||||
GSTACK_VERSION="$(cat "$GSTACK_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown")"
|
||||
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
curl -sf --max-time 5 \
|
||||
-X POST "${ENDPOINT}/update_checks" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "apikey: ${ANON_KEY}" \
|
||||
-H "Authorization: Bearer ${ANON_KEY}" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d "{\"gstack_version\":\"$GSTACK_VERSION\",\"os\":\"$_OS\"}" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
# Trigger community backup if community tier
|
||||
BACKUP_CMD="$GSTACK_DIR/bin/gstack-community-backup"
|
||||
if [ "$TIER" = "community" ] && [ -x "$BACKUP_CMD" ]; then
|
||||
"$BACKUP_CMD" 2>/dev/null &
|
||||
fi
|
||||
;;
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -f "$RESP_FILE" 2>/dev/null || true
|
||||
|
||||
# Update rate limit marker
|
||||
touch "$RATE_FILE" 2>/dev/null || true
|
||||
|
||||
|
||||
+9
-27
@@ -160,40 +160,22 @@ fi
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Fire Supabase install ping in background (parallel, non-blocking)
|
||||
# This logs an update check event for community health metrics.
|
||||
# If the endpoint isn't configured or Supabase is down, this is a no-op.
|
||||
# Source Supabase config for install ping
|
||||
if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
||||
# This logs an update check event for community health metrics via edge function.
|
||||
# If Supabase is not configured or telemetry is off, this is a no-op.
|
||||
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
||||
. "$GSTACK_DIR/supabase/config.sh"
|
||||
fi
|
||||
_SUPA_ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
|
||||
_SUPA_URL="${GSTACK_SUPABASE_URL:-}"
|
||||
_SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
||||
# Generate/read install fingerprint (runs for ALL tiers including off)
|
||||
_FP=""
|
||||
_FP_FILE="$STATE_DIR/.install-id"
|
||||
if [ -f "$_FP_FILE" ]; then
|
||||
_FP="$(cat "$_FP_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||
fi
|
||||
if [ -z "$_FP" ]; then
|
||||
_FP="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null || echo "")"
|
||||
_FP="$(echo "$_FP" | tr '[:upper:]' '[:lower:]')"
|
||||
if [ -n "$_FP" ]; then
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$_FP" > "$_FP_FILE"
|
||||
fi
|
||||
fi
|
||||
# Update-check pings always fire (ungated from telemetry tier).
|
||||
# This sends only: version, OS, and a random UUID. No usage data.
|
||||
# Equivalent to what GitHub sees in HTTP access logs for VERSION.
|
||||
if [ -n "$_SUPA_ENDPOINT" ] && [ -n "$_SUPA_KEY" ]; then
|
||||
# Respect telemetry opt-out — don't ping Supabase if user set telemetry: off
|
||||
_TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
|
||||
if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
|
||||
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
curl -sf --max-time 5 \
|
||||
-X POST "${_SUPA_ENDPOINT}/update_checks" \
|
||||
-X POST "${_SUPA_URL}/functions/v1/update-check" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "apikey: ${_SUPA_KEY}" \
|
||||
-H "Authorization: Bearer ${_SUPA_KEY}" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d "{\"gstack_version\":\"$LOCAL\",\"os\":\"$_OS\",\"install_fingerprint\":\"${_FP}\"}" \
|
||||
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
|
||||
>/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: browse
|
||||
preamble-tier: 1
|
||||
version: 1.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /browse.
|
||||
Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with
|
||||
elements, verify page state, diff before/after actions, take annotated screenshots, check
|
||||
responsive layouts, test forms and uploads, handle dialogs, and assert element states.
|
||||
@@ -30,9 +29,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -50,8 +51,11 @@ echo '{"skill":"browse","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basen
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -100,6 +104,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## Contributor Mode
|
||||
|
||||
If `_CONTRIB` is `true`: you are in **contributor mode**. At the end of each major workflow step, rate your gstack experience 0-10. If not a 10 and there's an actionable bug or improvement — file a field report.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -89,6 +89,10 @@ export class BrowserManager {
|
||||
|
||||
this.browser = await chromium.launch({
|
||||
headless: useHeadless,
|
||||
// On Windows, Chromium's sandbox fails when the server is spawned through
|
||||
// the Bun→Node process chain (GitHub #276). Disable it — local daemon
|
||||
// browsing user-specified URLs has marginal sandbox benefit.
|
||||
chromiumSandbox: process.platform !== 'win32',
|
||||
...(launchArgs.length > 0 ? { args: launchArgs } : {}),
|
||||
});
|
||||
|
||||
@@ -492,7 +496,11 @@ export class BrowserManager {
|
||||
// 2. Launch new headed browser (try-catch — if this fails, headless stays running)
|
||||
let newBrowser: Browser;
|
||||
try {
|
||||
newBrowser = await chromium.launch({ headless: false, timeout: 15000 });
|
||||
newBrowser = await chromium.launch({
|
||||
headless: false,
|
||||
timeout: 15000,
|
||||
chromiumSandbox: process.platform !== 'win32',
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return `ERROR: Cannot open headed browser — ${msg}. Headless browser still running.`;
|
||||
|
||||
+105
-39
@@ -76,6 +76,13 @@ export function resolveNodeServerScript(
|
||||
|
||||
const NODE_SERVER_SCRIPT = IS_WINDOWS ? resolveNodeServerScript() : null;
|
||||
|
||||
// On Windows, hard-fail if server-node.mjs is missing — the Bun path is known broken.
|
||||
if (IS_WINDOWS && !NODE_SERVER_SCRIPT) {
|
||||
throw new Error(
|
||||
'server-node.mjs not found. Run `bun run build` to generate the Windows server bundle.'
|
||||
);
|
||||
}
|
||||
|
||||
interface ServerState {
|
||||
pid: number;
|
||||
port: number;
|
||||
@@ -96,6 +103,19 @@ function readState(): ServerState | null {
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
if (IS_WINDOWS) {
|
||||
// Bun's compiled binary can't signal Windows PIDs (always throws ESRCH).
|
||||
// Use tasklist as a fallback. Only for one-shot calls — too slow for polling loops.
|
||||
try {
|
||||
const result = Bun.spawnSync(
|
||||
['tasklist', '/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
|
||||
{ stdout: 'pipe', stderr: 'pipe', timeout: 3000 }
|
||||
);
|
||||
return result.stdout.toString().includes(`"${pid}"`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
@@ -104,10 +124,42 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP health check — definitive proof the server is alive and responsive.
|
||||
* Used in all polling loops instead of isProcessAlive() (which is slow on Windows).
|
||||
*/
|
||||
export async function isServerHealthy(port: number): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
const health = await resp.json() as any;
|
||||
return health.status === 'healthy';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Process Management ─────────────────────────────────────────
|
||||
async function killServer(pid: number): Promise<void> {
|
||||
if (!isProcessAlive(pid)) return;
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
// taskkill /T /F kills the process tree (Node + Chromium)
|
||||
try {
|
||||
Bun.spawnSync(
|
||||
['taskkill', '/PID', String(pid), '/T', '/F'],
|
||||
{ stdout: 'pipe', stderr: 'pipe', timeout: 5000 }
|
||||
);
|
||||
} catch {}
|
||||
const deadline = Date.now() + 2000;
|
||||
while (Date.now() < deadline && isProcessAlive(pid)) {
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try { process.kill(pid, 'SIGTERM'); } catch { return; }
|
||||
|
||||
// Wait up to 2s for graceful shutdown
|
||||
@@ -127,6 +179,10 @@ async function killServer(pid: number): Promise<void> {
|
||||
* Verifies PID ownership before sending signals.
|
||||
*/
|
||||
function cleanupLegacyState(): void {
|
||||
// No legacy state on Windows — /tmp and `ps` don't exist, and gstack
|
||||
// never ran on Windows before the Node.js fallback was added.
|
||||
if (IS_WINDOWS) return;
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync('/tmp').filter(f => f.startsWith('browse-server') && f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
@@ -164,44 +220,65 @@ function cleanupLegacyState(): void {
|
||||
async function startServer(): Promise<ServerState> {
|
||||
ensureStateDir(config);
|
||||
|
||||
// Clean up stale state file
|
||||
// Clean up stale state file and error log
|
||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
||||
try { fs.unlinkSync(path.join(config.stateDir, 'browse-startup-error.log')); } catch {}
|
||||
|
||||
// Start server as detached background process.
|
||||
// On Windows, Bun can't launch/connect to Playwright's Chromium (oven-sh/bun#4253, #9911).
|
||||
// Fall back to running the server under Node.js with Bun API polyfills.
|
||||
const useNode = IS_WINDOWS && NODE_SERVER_SCRIPT;
|
||||
const serverCmd = useNode
|
||||
? ['node', NODE_SERVER_SCRIPT]
|
||||
: ['bun', 'run', SERVER_SCRIPT];
|
||||
const proc = Bun.spawn(serverCmd, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile },
|
||||
});
|
||||
let proc: any = null;
|
||||
|
||||
// Don't hold the CLI open
|
||||
proc.unref();
|
||||
if (IS_WINDOWS && NODE_SERVER_SCRIPT) {
|
||||
// Windows: Bun.spawn() + proc.unref() doesn't truly detach on Windows —
|
||||
// when the CLI exits, the server dies with it. Use Node's child_process.spawn
|
||||
// with { detached: true } instead, which is the gold standard for Windows
|
||||
// process independence. Credit: PR #191 by @fqueiro.
|
||||
const launcherCode =
|
||||
`const{spawn}=require('child_process');` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
`{detached:true,stdio:'ignore',env:Object.assign({},process.env,` +
|
||||
`{BROWSE_STATE_FILE:${JSON.stringify(config.stateFile)}})}).unref()`;
|
||||
Bun.spawnSync(['node', '-e', launcherCode], { stdio: 'ignore' });
|
||||
} else {
|
||||
// macOS/Linux: Bun.spawn + unref works correctly
|
||||
proc = Bun.spawn(['bun', 'run', SERVER_SCRIPT], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile },
|
||||
});
|
||||
proc.unref();
|
||||
}
|
||||
|
||||
// Wait for state file to appear
|
||||
// Wait for server to become healthy.
|
||||
// Use HTTP health check (not isProcessAlive) — it's fast (~instant ECONNREFUSED)
|
||||
// and works reliably on all platforms including Windows.
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < MAX_START_WAIT) {
|
||||
const state = readState();
|
||||
if (state && isProcessAlive(state.pid)) {
|
||||
if (state && await isServerHealthy(state.port)) {
|
||||
return state;
|
||||
}
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
|
||||
// If we get here, server didn't start in time
|
||||
// Try to read stderr for error message
|
||||
const stderr = proc.stderr;
|
||||
if (stderr) {
|
||||
const reader = stderr.getReader();
|
||||
// Server didn't start in time — try to get error details
|
||||
if (proc?.stderr) {
|
||||
// macOS/Linux: read stderr from the spawned process
|
||||
const reader = proc.stderr.getReader();
|
||||
const { value } = await reader.read();
|
||||
if (value) {
|
||||
const errText = new TextDecoder().decode(value);
|
||||
throw new Error(`Server failed to start:\n${errText}`);
|
||||
}
|
||||
} else {
|
||||
// Windows: check startup error log (server writes errors to disk since
|
||||
// stderr is unavailable due to stdio: 'ignore' for detachment)
|
||||
const errorLogPath = path.join(config.stateDir, 'browse-startup-error.log');
|
||||
try {
|
||||
const errorLog = fs.readFileSync(errorLogPath, 'utf-8').trim();
|
||||
if (errorLog) {
|
||||
throw new Error(`Server failed to start:\n${errorLog}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
}
|
||||
}
|
||||
throw new Error(`Server failed to start within ${MAX_START_WAIT / 1000}s`);
|
||||
}
|
||||
@@ -237,7 +314,10 @@ function acquireServerLock(): (() => void) | null {
|
||||
async function ensureServer(): Promise<ServerState> {
|
||||
const state = readState();
|
||||
|
||||
if (state && isProcessAlive(state.pid)) {
|
||||
// Health-check-first: HTTP is definitive proof the server is alive and responsive.
|
||||
// This replaces the PID-gated approach which breaks on Windows (Bun's process.kill
|
||||
// always throws ESRCH for Windows PIDs in compiled binaries).
|
||||
if (state && await isServerHealthy(state.port)) {
|
||||
// Check for binary version mismatch (auto-restart on update)
|
||||
const currentVersion = readVersionHash();
|
||||
if (currentVersion && state.binaryVersion && currentVersion !== state.binaryVersion) {
|
||||
@@ -245,21 +325,7 @@ async function ensureServer(): Promise<ServerState> {
|
||||
await killServer(state.pid);
|
||||
return startServer();
|
||||
}
|
||||
|
||||
// Server appears alive — do a health check
|
||||
try {
|
||||
const resp = await fetch(`http://127.0.0.1:${state.port}/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const health = await resp.json() as any;
|
||||
if (health.status === 'healthy') {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Health check failed — server is dead or unhealthy
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// Ensure state directory exists before lock acquisition (lock file lives there)
|
||||
@@ -273,7 +339,7 @@ async function ensureServer(): Promise<ServerState> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < MAX_START_WAIT) {
|
||||
const freshState = readState();
|
||||
if (freshState && isProcessAlive(freshState.pid)) return freshState;
|
||||
if (freshState && await isServerHealthy(freshState.port)) return freshState;
|
||||
await Bun.sleep(200);
|
||||
}
|
||||
throw new Error('Timed out waiting for another instance to start the server');
|
||||
@@ -282,7 +348,7 @@ async function ensureServer(): Promise<ServerState> {
|
||||
try {
|
||||
// Re-read state under lock in case another process just started the server
|
||||
const freshState = readState();
|
||||
if (freshState && isProcessAlive(freshState.pid)) {
|
||||
if (freshState && await isServerHealthy(freshState.port)) {
|
||||
return freshState;
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,13 @@ async function shutdown() {
|
||||
// Handle signals
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
// Windows: taskkill /F bypasses SIGTERM, but 'exit' fires for some shutdown paths.
|
||||
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
|
||||
if (process.platform === 'win32') {
|
||||
process.on('exit', () => {
|
||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Start ─────────────────────────────────────────────────────
|
||||
async function start() {
|
||||
@@ -365,5 +372,14 @@ async function start() {
|
||||
|
||||
start().catch((err) => {
|
||||
console.error(`[browse] Failed to start: ${err.message}`);
|
||||
// Write error to disk for the CLI to read — on Windows, the CLI can't capture
|
||||
// stderr because the server is launched with detached: true, stdio: 'ignore'.
|
||||
try {
|
||||
const errorLogPath = path.join(config.stateDir, 'browse-startup-error.log');
|
||||
fs.mkdirSync(config.stateDir, { recursive: true });
|
||||
fs.writeFileSync(errorLogPath, `${new Date().toISOString()} ${err.message}\n${err.stack || ''}\n`);
|
||||
} catch {
|
||||
// stateDir may not exist — nothing more we can do
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -248,3 +248,69 @@ describe('version mismatch detection', () => {
|
||||
expect(shouldRestart).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isServerHealthy', () => {
|
||||
const { isServerHealthy } = require('../src/cli');
|
||||
const http = require('http');
|
||||
|
||||
test('returns true for a healthy server', async () => {
|
||||
const server = http.createServer((_req: any, res: any) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'healthy' }));
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, resolve));
|
||||
const port = server.address().port;
|
||||
try {
|
||||
expect(await isServerHealthy(port)).toBe(true);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('returns false for an unhealthy server', async () => {
|
||||
const server = http.createServer((_req: any, res: any) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'unhealthy' }));
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, resolve));
|
||||
const port = server.address().port;
|
||||
try {
|
||||
expect(await isServerHealthy(port)).toBe(false);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('returns false when server is not running', async () => {
|
||||
// Use a port that's almost certainly not in use
|
||||
expect(await isServerHealthy(59999)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false on non-200 response', async () => {
|
||||
const server = http.createServer((_req: any, res: any) => {
|
||||
res.writeHead(500);
|
||||
res.end('Internal Server Error');
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, resolve));
|
||||
const port = server.address().port;
|
||||
try {
|
||||
expect(await isServerHealthy(port)).toBe(false);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('startup error log', () => {
|
||||
test('write and read error log', () => {
|
||||
const tmpDir = path.join(os.tmpdir(), `browse-error-log-test-${Date.now()}`);
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
const errorLogPath = path.join(tmpDir, 'browse-startup-error.log');
|
||||
const errorMsg = 'Cannot find module playwright';
|
||||
fs.writeFileSync(errorLogPath, `2026-03-23T00:00:00.000Z ${errorMsg}\n`);
|
||||
const content = fs.readFileSync(errorLogPath, 'utf-8').trim();
|
||||
expect(content).toContain(errorMsg);
|
||||
expect(content).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO timestamp prefix
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: canary
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /canary.
|
||||
Post-deploy canary monitoring. Watches the live app for console errors,
|
||||
performance regressions, and page failures using the browse daemon. Takes
|
||||
periodic screenshots, compares against pre-deploy baselines, and alerts
|
||||
@@ -30,9 +29,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -50,8 +51,11 @@ echo '{"skill":"canary","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basen
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -100,6 +104,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: careful
|
||||
version: 0.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /careful.
|
||||
Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE,
|
||||
force-push, git reset --hard, kubectl delete, and similar destructive operations.
|
||||
User can override each warning. Use when touching prod, debugging live systems,
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: codex
|
||||
preamble-tier: 3
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /codex.
|
||||
OpenAI Codex CLI wrapper — three modes. Code review: independent diff review via
|
||||
codex review with pass/fail gate. Challenge: adversarial mode that tries to break
|
||||
your code. Consult: ask codex anything with session continuity for follow-ups.
|
||||
@@ -31,9 +30,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -51,8 +52,11 @@ echo '{"skill":"codex","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basena
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -101,6 +105,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: cso
|
||||
preamble-tier: 2
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /cso.
|
||||
Chief Security Officer mode. Infrastructure-first security audit: secrets archaeology,
|
||||
dependency supply chain, CI/CD pipeline security, LLM/AI security, skill supply chain
|
||||
scanning, plus OWASP Top 10, STRIDE threat modeling, and active verification.
|
||||
@@ -34,9 +33,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -54,8 +55,11 @@ echo '{"skill":"cso","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -104,6 +108,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -3,7 +3,6 @@ name: design-consultation
|
||||
preamble-tier: 3
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /design-consultation.
|
||||
Design consultation: understands your product, researches the landscape, proposes a
|
||||
complete design system (aesthetic, typography, color, layout, spacing, motion), and
|
||||
generates font+color preview pages. Creates DESIGN.md as your project's design source
|
||||
@@ -35,9 +34,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -55,8 +56,11 @@ echo '{"skill":"design-consultation","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","re
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -105,6 +109,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: design-review
|
||||
preamble-tier: 4
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /design-review.
|
||||
Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems,
|
||||
AI slop patterns, and slow interactions — then fixes them. Iteratively fixes issues
|
||||
in source code, committing each fix atomically and re-verifying with before/after
|
||||
@@ -35,9 +34,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -55,8 +56,11 @@ echo '{"skill":"design-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -105,6 +109,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -3,7 +3,6 @@ name: document-release
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /document-release.
|
||||
Post-ship documentation update. Reads all project docs, cross-references the
|
||||
diff, updates README/ARCHITECTURE/CONTRIBUTING/CLAUDE.md to match what shipped,
|
||||
polishes CHANGELOG voice, cleans up TODOS, and optionally bumps VERSION. Use when
|
||||
@@ -32,9 +31,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -52,8 +53,11 @@ echo '{"skill":"document-release","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo"
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -102,6 +106,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: freeze
|
||||
version: 0.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /freeze.
|
||||
Restrict file edits to a specific directory for the session. Blocks Edit and
|
||||
Write outside the allowed path. Use when debugging to prevent accidentally
|
||||
"fixing" unrelated code, or when you want to scope changes to one module.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: gstack-upgrade
|
||||
version: 1.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /gstack-upgrade.
|
||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
|
||||
"update gstack", or "get latest version".
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: guard
|
||||
version: 0.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /guard.
|
||||
Full safety mode: destructive command warnings + directory-scoped edits.
|
||||
Combines /careful (warns before rm -rf, DROP TABLE, force-push, etc.) with
|
||||
/freeze (blocks edits outside a specified directory). Use for maximum safety
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: investigate
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /investigate.
|
||||
Systematic debugging with root cause investigation. Four phases: investigate,
|
||||
analyze, hypothesize, implement. Iron Law: no fixes without root cause.
|
||||
Use when asked to "debug this", "fix this bug", "why is this broken",
|
||||
@@ -46,9 +45,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -66,8 +67,11 @@ echo '{"skill":"investigate","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -116,6 +120,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -3,7 +3,6 @@ name: land-and-deploy
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /land-and-deploy.
|
||||
Land and deploy workflow. Merges the PR, waits for CI and deploy,
|
||||
verifies production health via canary checks. Takes over after /ship
|
||||
creates the PR. Use when: "merge", "land", "deploy", "merge and verify",
|
||||
@@ -29,9 +28,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -49,8 +50,11 @@ echo '{"skill":"land-and-deploy","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -99,6 +103,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: office-hours
|
||||
preamble-tier: 3
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /office-hours.
|
||||
YC Office Hours — two modes. Startup mode: six forcing questions that expose
|
||||
demand reality, status quo, desperate specificity, narrowest wedge, observation,
|
||||
and future-fit. Builder mode: design thinking brainstorming for side projects,
|
||||
@@ -37,9 +36,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -57,8 +58,11 @@ echo '{"skill":"office-hours","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -107,6 +111,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"test:evals:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts",
|
||||
"test:e2e": "EVALS=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts",
|
||||
"test:e2e:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts",
|
||||
"test:e2e:fast": "EVALS=1 EVALS_FAST=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts",
|
||||
"test:gate": "EVALS=1 EVALS_TIER=gate bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts",
|
||||
"test:periodic": "EVALS=1 EVALS_TIER=periodic EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts",
|
||||
"test:codex": "EVALS=1 bun test test/codex-e2e.test.ts",
|
||||
"test:codex:all": "EVALS=1 EVALS_ALL=1 bun test test/codex-e2e.test.ts",
|
||||
"test:gemini": "EVALS=1 bun test test/gemini-e2e.test.ts",
|
||||
|
||||
@@ -3,7 +3,6 @@ name: plan-ceo-review
|
||||
preamble-tier: 3
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /plan-ceo-review.
|
||||
CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
|
||||
challenge premises, expand scope when it creates a better product. Four modes:
|
||||
SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick
|
||||
@@ -35,9 +34,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -55,8 +56,11 @@ echo '{"skill":"plan-ceo-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -105,6 +109,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -3,7 +3,6 @@ name: plan-design-review
|
||||
preamble-tier: 3
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /plan-design-review.
|
||||
Designer's eye plan review — interactive, like CEO and Eng review.
|
||||
Rates each design dimension 0-10, explains what would make it a 10,
|
||||
then fixes the plan to get there. Works in plan mode. For live site
|
||||
@@ -33,9 +32,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -53,8 +54,11 @@ echo '{"skill":"plan-design-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","rep
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -103,6 +107,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
@@ -3,7 +3,6 @@ name: plan-eng-review
|
||||
preamble-tier: 3
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /plan-eng-review.
|
||||
Eng manager-mode plan review. Lock in the execution plan — architecture,
|
||||
data flow, diagrams, edge cases, test coverage, performance. Walks through
|
||||
issues interactively with opinionated recommendations. Use when asked to
|
||||
@@ -34,9 +33,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -54,8 +55,11 @@ echo '{"skill":"plan-eng-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -104,6 +108,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: qa-only
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /qa-only.
|
||||
Report-only QA testing. Systematically tests a web application and produces a
|
||||
structured report with health score, screenshots, and repro steps — but never
|
||||
fixes anything. Use when asked to "just report bugs", "qa report only", or
|
||||
@@ -30,9 +29,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -50,8 +51,11 @@ echo '{"skill":"qa-only","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(base
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -100,6 +104,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: qa
|
||||
preamble-tier: 4
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /qa.
|
||||
Systematically QA test a web application and fix bugs found. Runs QA testing,
|
||||
then iteratively fixes bugs in source code, committing each fix atomically and
|
||||
re-verifying. Use when asked to "qa", "QA", "test this site", "find bugs",
|
||||
@@ -36,9 +35,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -56,8 +57,11 @@ echo '{"skill":"qa","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -106,6 +110,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+50
-3
@@ -3,7 +3,6 @@ name: retro
|
||||
preamble-tier: 2
|
||||
version: 2.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /retro.
|
||||
Weekly engineering retrospective. Analyzes commit history, work patterns,
|
||||
and code quality metrics with persistent history and trend tracking.
|
||||
Team-aware: breaks down per-person contributions with praise and growth areas.
|
||||
@@ -30,9 +29,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -50,8 +51,11 @@ echo '{"skill":"retro","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basena
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -100,6 +104,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
@@ -676,6 +701,28 @@ Narrative covering:
|
||||
- If prior retro exists and has `test_health`: show delta "Test count: {last} → {now} (+{delta})"
|
||||
- If test ratio < 20%: flag as growth area — "100% test coverage is the goal. Tests make vibe coding safe."
|
||||
|
||||
### Plan Completion
|
||||
Check review JSONL logs for plan completion data from /ship runs this period:
|
||||
|
||||
```bash
|
||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||
cat ~/.gstack/projects/$SLUG/*-reviews.jsonl 2>/dev/null | grep '"skill":"ship"' | grep '"plan_items_total"' || echo "NO_PLAN_DATA"
|
||||
```
|
||||
|
||||
If plan completion data exists within the retro time window:
|
||||
- Count branches shipped with plans (entries that have `plan_items_total` > 0)
|
||||
- Compute average completion: sum of `plan_items_done` / sum of `plan_items_total`
|
||||
- Identify most-skipped item category if data supports it
|
||||
|
||||
Output:
|
||||
```
|
||||
Plan Completion This Period:
|
||||
{N} branches shipped with plans
|
||||
Average completion: {X}% ({done}/{total} items)
|
||||
```
|
||||
|
||||
If no plan data exists, skip this section silently.
|
||||
|
||||
### Focus & Highlights
|
||||
(from Step 8)
|
||||
- Focus score with interpretation
|
||||
|
||||
@@ -460,6 +460,28 @@ Narrative covering:
|
||||
- If prior retro exists and has `test_health`: show delta "Test count: {last} → {now} (+{delta})"
|
||||
- If test ratio < 20%: flag as growth area — "100% test coverage is the goal. Tests make vibe coding safe."
|
||||
|
||||
### Plan Completion
|
||||
Check review JSONL logs for plan completion data from /ship runs this period:
|
||||
|
||||
```bash
|
||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||
cat ~/.gstack/projects/$SLUG/*-reviews.jsonl 2>/dev/null | grep '"skill":"ship"' | grep '"plan_items_total"' || echo "NO_PLAN_DATA"
|
||||
```
|
||||
|
||||
If plan completion data exists within the retro time window:
|
||||
- Count branches shipped with plans (entries that have `plan_items_total` > 0)
|
||||
- Compute average completion: sum of `plan_items_done` / sum of `plan_items_total`
|
||||
- Identify most-skipped item category if data supports it
|
||||
|
||||
Output:
|
||||
```
|
||||
Plan Completion This Period:
|
||||
{N} branches shipped with plans
|
||||
Average completion: {X}% ({done}/{total} items)
|
||||
```
|
||||
|
||||
If no plan data exists, skip this section silently.
|
||||
|
||||
### Focus & Highlights
|
||||
(from Step 8)
|
||||
- Focus score with interpretation
|
||||
|
||||
+157
-4
@@ -3,7 +3,6 @@ name: review
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /review.
|
||||
Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust
|
||||
boundary violations, conditional side effects, and other structural issues. Use when
|
||||
asked to "review this PR", "code review", "pre-landing review", or "check my diff".
|
||||
@@ -33,9 +32,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -53,8 +54,11 @@ echo '{"skill":"review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basen
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -103,6 +107,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
@@ -297,7 +322,120 @@ Before reviewing code quality, check: **did they build what was requested — no
|
||||
**If no PR exists:** rely on commit messages and TODOS.md for stated intent — this is the common case since /review runs before /ship creates the PR.
|
||||
2. Identify the **stated intent** — what was this branch supposed to accomplish?
|
||||
3. Run `git diff origin/<base>...HEAD --stat` and compare the files changed against the stated intent.
|
||||
4. Evaluate with skepticism:
|
||||
|
||||
### Plan File Discovery
|
||||
|
||||
1. **Conversation context (primary):** Check if there is an active plan file in this conversation — Claude Code system messages include plan file paths when in plan mode. Look for references like `~/.claude/plans/*.md` in system messages. If found, use it directly — this is the most reliable signal.
|
||||
|
||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||
|
||||
```bash
|
||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||
# Try branch name match first (most specific)
|
||||
PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1)
|
||||
# Fall back to repo name match
|
||||
[ -z "$PLAN" ] && PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1)
|
||||
# Last resort: most recent plan modified in the last 24 hours
|
||||
[ -z "$PLAN" ] && PLAN=$(find ~/.claude/plans -name '*.md' -mmin -1440 -maxdepth 1 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
|
||||
[ -n "$PLAN" ] && echo "PLAN_FILE: $PLAN" || echo "NO_PLAN_FILE"
|
||||
```
|
||||
|
||||
3. **Validation:** If a plan file was found via content-based search (not conversation context), read the first 20 lines and verify it is relevant to the current branch's work. If it appears to be from a different project or feature, treat as "no plan file found."
|
||||
|
||||
**Error handling:**
|
||||
- No plan file found → skip with "No plan file detected — skipping."
|
||||
- Plan file found but unreadable (permissions, encoding) → skip with "Plan file found but unreadable — skipping."
|
||||
|
||||
### Actionable Item Extraction
|
||||
|
||||
Read the plan file. Extract every actionable item — anything that describes work to be done. Look for:
|
||||
|
||||
- **Checkbox items:** `- [ ] ...` or `- [x] ...`
|
||||
- **Numbered steps** under implementation headings: "1. Create ...", "2. Add ...", "3. Modify ..."
|
||||
- **Imperative statements:** "Add X to Y", "Create a Z service", "Modify the W controller"
|
||||
- **File-level specifications:** "New file: path/to/file.ts", "Modify path/to/existing.rb"
|
||||
- **Test requirements:** "Test that X", "Add test for Y", "Verify Z"
|
||||
- **Data model changes:** "Add column X to table Y", "Create migration for Z"
|
||||
|
||||
**Ignore:**
|
||||
- Context/Background sections (`## Context`, `## Background`, `## Problem`)
|
||||
- Questions and open items (marked with ?, "TBD", "TODO: decide")
|
||||
- Review report sections (`## GSTACK REVIEW REPORT`)
|
||||
- Explicitly deferred items ("Future:", "Out of scope:", "NOT in scope:", "P2:", "P3:", "P4:")
|
||||
- CEO Review Decisions sections (these record choices, not work items)
|
||||
|
||||
**Cap:** Extract at most 50 items. If the plan has more, note: "Showing top 50 of N plan items — full list in plan file."
|
||||
|
||||
**No items found:** If the plan contains no extractable actionable items, skip with: "Plan file contains no actionable items — skipping completion audit."
|
||||
|
||||
For each item, note:
|
||||
- The item text (verbatim or concise summary)
|
||||
- Its category: CODE | TEST | MIGRATION | CONFIG | DOCS
|
||||
|
||||
### Cross-Reference Against Diff
|
||||
|
||||
Run `git diff origin/<base>...HEAD` and `git log origin/<base>..HEAD --oneline` to understand what was implemented.
|
||||
|
||||
For each extracted plan item, check the diff and classify:
|
||||
|
||||
- **DONE** — Clear evidence in the diff that this item was implemented. Cite the specific file(s) changed.
|
||||
- **PARTIAL** — Some work toward this item exists in the diff but it's incomplete (e.g., model created but controller missing, function exists but edge cases not handled).
|
||||
- **NOT DONE** — No evidence in the diff that this item was addressed.
|
||||
- **CHANGED** — The item was implemented using a different approach than the plan described, but the same goal is achieved. Note the difference.
|
||||
|
||||
**Be conservative with DONE** — require clear evidence in the diff. A file being touched is not enough; the specific functionality described must be present.
|
||||
**Be generous with CHANGED** — if the goal is met by different means, that counts as addressed.
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
PLAN COMPLETION AUDIT
|
||||
═══════════════════════════════
|
||||
Plan: {plan file path}
|
||||
|
||||
## Implementation Items
|
||||
[DONE] Create UserService — src/services/user_service.rb (+142 lines)
|
||||
[PARTIAL] Add validation — model validates but missing controller checks
|
||||
[NOT DONE] Add caching layer — no cache-related changes in diff
|
||||
[CHANGED] "Redis queue" → implemented with Sidekiq instead
|
||||
|
||||
## Test Items
|
||||
[DONE] Unit tests for UserService — test/services/user_service_test.rb
|
||||
[NOT DONE] E2E test for signup flow
|
||||
|
||||
## Migration Items
|
||||
[DONE] Create users table — db/migrate/20240315_create_users.rb
|
||||
|
||||
─────────────────────────────────
|
||||
COMPLETION: 4/7 DONE, 1 PARTIAL, 1 NOT DONE, 1 CHANGED
|
||||
─────────────────────────────────
|
||||
```
|
||||
|
||||
### Integration with Scope Drift Detection
|
||||
|
||||
The plan completion results augment the existing Scope Drift Detection. If a plan file is found:
|
||||
|
||||
- **NOT DONE items** become additional evidence for **MISSING REQUIREMENTS** in the scope drift report.
|
||||
- **Items in the diff that don't match any plan item** become evidence for **SCOPE CREEP** detection.
|
||||
|
||||
This is **INFORMATIONAL** — does not block the review (consistent with existing scope drift behavior).
|
||||
|
||||
Update the scope drift output to include plan file context:
|
||||
|
||||
```
|
||||
Scope Check: [CLEAN / DRIFT DETECTED / REQUIREMENTS MISSING]
|
||||
Intent: <from plan file — 1-line summary>
|
||||
Plan: <plan file path>
|
||||
Delivered: <1-line summary of what the diff actually does>
|
||||
Plan items: N DONE, M PARTIAL, K NOT DONE
|
||||
[If NOT DONE: list each missing item]
|
||||
[If scope creep: list each out-of-scope change not in the plan]
|
||||
```
|
||||
|
||||
**No plan file found:** Fall back to existing scope drift behavior (check TODOS.md and PR description only).
|
||||
|
||||
4. Evaluate with skepticism (incorporating plan completion results if available):
|
||||
|
||||
**SCOPE CREEP detection:**
|
||||
- Files changed that are unrelated to the stated intent
|
||||
@@ -606,6 +744,21 @@ If no test framework detected → include gaps as INFORMATIONAL findings only, n
|
||||
|
||||
**Diff is test-only changes:** Skip Step 4.75 entirely: "No new application code paths to audit."
|
||||
|
||||
### Coverage Warning
|
||||
|
||||
After producing the coverage diagram, check the coverage percentage. Read CLAUDE.md for a `## Test Coverage` section with a `Minimum:` field. If not found, use default: 60%.
|
||||
|
||||
If coverage is below the minimum threshold, output a prominent warning **before** the regular review findings:
|
||||
|
||||
```
|
||||
⚠️ COVERAGE WARNING: AI-assessed coverage is {X}%. {N} code paths untested.
|
||||
Consider writing tests before running /ship.
|
||||
```
|
||||
|
||||
This is INFORMATIONAL — does not block /review. But it makes low coverage visible early so the developer can address it before reaching the /ship coverage gate.
|
||||
|
||||
If coverage percentage cannot be determined, skip the warning silently.
|
||||
|
||||
This step subsumes the "Test Gaps" category from Pass 2 — do not duplicate findings between the checklist Test Gaps item and this coverage diagram. Include any coverage gaps alongside the findings from Step 4 and Step 4.5. They follow the same Fix-First flow — gaps are INFORMATIONAL findings.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +46,10 @@ Before reviewing code quality, check: **did they build what was requested — no
|
||||
**If no PR exists:** rely on commit messages and TODOS.md for stated intent — this is the common case since /review runs before /ship creates the PR.
|
||||
2. Identify the **stated intent** — what was this branch supposed to accomplish?
|
||||
3. Run `git diff origin/<base>...HEAD --stat` and compare the files changed against the stated intent.
|
||||
4. Evaluate with skepticism:
|
||||
|
||||
{{PLAN_COMPLETION_AUDIT_REVIEW}}
|
||||
|
||||
4. Evaluate with skepticism (incorporating plan completion results if available):
|
||||
|
||||
**SCOPE CREEP detection:**
|
||||
- Files changed that are unrelated to the stated intent
|
||||
|
||||
+2746
-11
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import { generateTestFailureTriage } from './preamble';
|
||||
import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup } from './browse';
|
||||
import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch } from './design';
|
||||
import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip, generateTestCoverageAuditReview } from './testing';
|
||||
import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview } from './review';
|
||||
import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec } from './review';
|
||||
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology } from './utility';
|
||||
|
||||
export const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
|
||||
@@ -41,4 +41,7 @@ export const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
|
||||
ADVERSARIAL_STEP: generateAdversarialStep,
|
||||
DEPLOY_BOOTSTRAP: generateDeployBootstrap,
|
||||
CODEX_PLAN_REVIEW: generateCodexPlanReview,
|
||||
PLAN_COMPLETION_AUDIT_SHIP: generatePlanCompletionAuditShip,
|
||||
PLAN_COMPLETION_AUDIT_REVIEW: generatePlanCompletionAuditReview,
|
||||
PLAN_VERIFICATION_EXEC: generatePlanVerificationExec,
|
||||
};
|
||||
|
||||
@@ -21,9 +21,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(${ctx.paths.binDir}/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(${ctx.paths.binDir}/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(${ctx.paths.binDir}/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=\${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -43,8 +45,11 @@ for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null
|
||||
}
|
||||
|
||||
function generateUpgradeCheck(ctx: TemplateContext): string {
|
||||
return `If \`PROACTIVE\` is \`"false"\`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
return `If \`PROACTIVE\` is \`"false"\`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`${ctx.paths.skillRoot}/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.`;
|
||||
}
|
||||
@@ -98,6 +103,29 @@ touch ~/.gstack/.telemetry-prompted
|
||||
This only happens once. If \`TEL_PROMPTED\` is \`yes\`, skip this entirely.`;
|
||||
}
|
||||
|
||||
function generateProactivePrompt(ctx: TemplateContext): string {
|
||||
return `If \`PROACTIVE_PROMPTED\` is \`no\` AND \`TEL_PROMPTED\` is \`yes\`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run \`${ctx.paths.binDir}/gstack-config set proactive true\`
|
||||
If B: run \`${ctx.paths.binDir}/gstack-config set proactive false\`
|
||||
|
||||
Always run:
|
||||
\`\`\`bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
\`\`\`
|
||||
|
||||
This only happens once. If \`PROACTIVE_PROMPTED\` is \`yes\`, skip this entirely.`;
|
||||
}
|
||||
|
||||
function generateAskUserFormat(_ctx: TemplateContext): string {
|
||||
return `## AskUserQuestion Format
|
||||
|
||||
@@ -382,6 +410,7 @@ export function generatePreamble(ctx: TemplateContext): string {
|
||||
generateUpgradeCheck(ctx),
|
||||
generateLakeIntro(),
|
||||
generateTelemetryPrompt(ctx),
|
||||
generateProactivePrompt(ctx),
|
||||
...(tier >= 2 ? [generateAskUserFormat(ctx), generateCompletenessSection()] : []),
|
||||
...(tier >= 3 ? [generateRepoModeSection(), generateSearchBeforeBuildingSection(ctx)] : []),
|
||||
generateContributorMode(),
|
||||
|
||||
@@ -592,3 +592,236 @@ SOURCE = "codex" if Codex ran, "claude" if subagent ran.
|
||||
|
||||
---`;
|
||||
}
|
||||
|
||||
// ─── Plan File Discovery (shared helper) ──────────────────────────────
|
||||
|
||||
function generatePlanFileDiscovery(): string {
|
||||
return `### Plan File Discovery
|
||||
|
||||
1. **Conversation context (primary):** Check if there is an active plan file in this conversation — Claude Code system messages include plan file paths when in plan mode. Look for references like \`~/.claude/plans/*.md\` in system messages. If found, use it directly — this is the most reliable signal.
|
||||
|
||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||
|
||||
\`\`\`bash
|
||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||
# Try branch name match first (most specific)
|
||||
PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1)
|
||||
# Fall back to repo name match
|
||||
[ -z "$PLAN" ] && PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1)
|
||||
# Last resort: most recent plan modified in the last 24 hours
|
||||
[ -z "$PLAN" ] && PLAN=$(find ~/.claude/plans -name '*.md' -mmin -1440 -maxdepth 1 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
|
||||
[ -n "$PLAN" ] && echo "PLAN_FILE: $PLAN" || echo "NO_PLAN_FILE"
|
||||
\`\`\`
|
||||
|
||||
3. **Validation:** If a plan file was found via content-based search (not conversation context), read the first 20 lines and verify it is relevant to the current branch's work. If it appears to be from a different project or feature, treat as "no plan file found."
|
||||
|
||||
**Error handling:**
|
||||
- No plan file found → skip with "No plan file detected — skipping."
|
||||
- Plan file found but unreadable (permissions, encoding) → skip with "Plan file found but unreadable — skipping."`;
|
||||
}
|
||||
|
||||
// ─── Plan Completion Audit ────────────────────────────────────────────
|
||||
|
||||
type PlanCompletionMode = 'ship' | 'review';
|
||||
|
||||
function generatePlanCompletionAuditInner(mode: PlanCompletionMode): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// ── Plan file discovery (shared) ──
|
||||
sections.push(generatePlanFileDiscovery());
|
||||
|
||||
// ── Item extraction ──
|
||||
sections.push(`
|
||||
### Actionable Item Extraction
|
||||
|
||||
Read the plan file. Extract every actionable item — anything that describes work to be done. Look for:
|
||||
|
||||
- **Checkbox items:** \`- [ ] ...\` or \`- [x] ...\`
|
||||
- **Numbered steps** under implementation headings: "1. Create ...", "2. Add ...", "3. Modify ..."
|
||||
- **Imperative statements:** "Add X to Y", "Create a Z service", "Modify the W controller"
|
||||
- **File-level specifications:** "New file: path/to/file.ts", "Modify path/to/existing.rb"
|
||||
- **Test requirements:** "Test that X", "Add test for Y", "Verify Z"
|
||||
- **Data model changes:** "Add column X to table Y", "Create migration for Z"
|
||||
|
||||
**Ignore:**
|
||||
- Context/Background sections (\`## Context\`, \`## Background\`, \`## Problem\`)
|
||||
- Questions and open items (marked with ?, "TBD", "TODO: decide")
|
||||
- Review report sections (\`## GSTACK REVIEW REPORT\`)
|
||||
- Explicitly deferred items ("Future:", "Out of scope:", "NOT in scope:", "P2:", "P3:", "P4:")
|
||||
- CEO Review Decisions sections (these record choices, not work items)
|
||||
|
||||
**Cap:** Extract at most 50 items. If the plan has more, note: "Showing top 50 of N plan items — full list in plan file."
|
||||
|
||||
**No items found:** If the plan contains no extractable actionable items, skip with: "Plan file contains no actionable items — skipping completion audit."
|
||||
|
||||
For each item, note:
|
||||
- The item text (verbatim or concise summary)
|
||||
- Its category: CODE | TEST | MIGRATION | CONFIG | DOCS`);
|
||||
|
||||
// ── Cross-reference against diff ──
|
||||
sections.push(`
|
||||
### Cross-Reference Against Diff
|
||||
|
||||
Run \`git diff origin/<base>...HEAD\` and \`git log origin/<base>..HEAD --oneline\` to understand what was implemented.
|
||||
|
||||
For each extracted plan item, check the diff and classify:
|
||||
|
||||
- **DONE** — Clear evidence in the diff that this item was implemented. Cite the specific file(s) changed.
|
||||
- **PARTIAL** — Some work toward this item exists in the diff but it's incomplete (e.g., model created but controller missing, function exists but edge cases not handled).
|
||||
- **NOT DONE** — No evidence in the diff that this item was addressed.
|
||||
- **CHANGED** — The item was implemented using a different approach than the plan described, but the same goal is achieved. Note the difference.
|
||||
|
||||
**Be conservative with DONE** — require clear evidence in the diff. A file being touched is not enough; the specific functionality described must be present.
|
||||
**Be generous with CHANGED** — if the goal is met by different means, that counts as addressed.`);
|
||||
|
||||
// ── Output format ──
|
||||
sections.push(`
|
||||
### Output Format
|
||||
|
||||
\`\`\`
|
||||
PLAN COMPLETION AUDIT
|
||||
═══════════════════════════════
|
||||
Plan: {plan file path}
|
||||
|
||||
## Implementation Items
|
||||
[DONE] Create UserService — src/services/user_service.rb (+142 lines)
|
||||
[PARTIAL] Add validation — model validates but missing controller checks
|
||||
[NOT DONE] Add caching layer — no cache-related changes in diff
|
||||
[CHANGED] "Redis queue" → implemented with Sidekiq instead
|
||||
|
||||
## Test Items
|
||||
[DONE] Unit tests for UserService — test/services/user_service_test.rb
|
||||
[NOT DONE] E2E test for signup flow
|
||||
|
||||
## Migration Items
|
||||
[DONE] Create users table — db/migrate/20240315_create_users.rb
|
||||
|
||||
─────────────────────────────────
|
||||
COMPLETION: 4/7 DONE, 1 PARTIAL, 1 NOT DONE, 1 CHANGED
|
||||
─────────────────────────────────
|
||||
\`\`\``);
|
||||
|
||||
// ── Gate logic (mode-specific) ──
|
||||
if (mode === 'ship') {
|
||||
sections.push(`
|
||||
### Gate Logic
|
||||
|
||||
After producing the completion checklist:
|
||||
|
||||
- **All DONE or CHANGED:** Pass. "Plan completion: PASS — all items addressed." Continue.
|
||||
- **Only PARTIAL items (no NOT DONE):** Continue with a note in the PR body. Not blocking.
|
||||
- **Any NOT DONE items:** Use AskUserQuestion:
|
||||
- Show the completion checklist above
|
||||
- "{N} items from the plan are NOT DONE. These were part of the original plan but are missing from the implementation."
|
||||
- RECOMMENDATION: depends on item count and severity. If 1-2 minor items (docs, config), recommend B. If core functionality is missing, recommend A.
|
||||
- Options:
|
||||
A) Stop — implement the missing items before shipping
|
||||
B) Ship anyway — defer these to a follow-up (will create P1 TODOs in Step 5.5)
|
||||
C) These items were intentionally dropped — remove from scope
|
||||
- If A: STOP. List the missing items for the user to implement.
|
||||
- If B: Continue. For each NOT DONE item, create a P1 TODO in Step 5.5 with "Deferred from plan: {plan file path}".
|
||||
- If C: Continue. Note in PR body: "Plan items intentionally dropped: {list}."
|
||||
|
||||
**No plan file found:** Skip entirely. "No plan file detected — skipping plan completion audit."
|
||||
|
||||
**Include in PR body (Step 8):** Add a \`## Plan Completion\` section with the checklist summary.`);
|
||||
} else {
|
||||
// review mode
|
||||
sections.push(`
|
||||
### Integration with Scope Drift Detection
|
||||
|
||||
The plan completion results augment the existing Scope Drift Detection. If a plan file is found:
|
||||
|
||||
- **NOT DONE items** become additional evidence for **MISSING REQUIREMENTS** in the scope drift report.
|
||||
- **Items in the diff that don't match any plan item** become evidence for **SCOPE CREEP** detection.
|
||||
|
||||
This is **INFORMATIONAL** — does not block the review (consistent with existing scope drift behavior).
|
||||
|
||||
Update the scope drift output to include plan file context:
|
||||
|
||||
\`\`\`
|
||||
Scope Check: [CLEAN / DRIFT DETECTED / REQUIREMENTS MISSING]
|
||||
Intent: <from plan file — 1-line summary>
|
||||
Plan: <plan file path>
|
||||
Delivered: <1-line summary of what the diff actually does>
|
||||
Plan items: N DONE, M PARTIAL, K NOT DONE
|
||||
[If NOT DONE: list each missing item]
|
||||
[If scope creep: list each out-of-scope change not in the plan]
|
||||
\`\`\`
|
||||
|
||||
**No plan file found:** Fall back to existing scope drift behavior (check TODOS.md and PR description only).`);
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
export function generatePlanCompletionAuditShip(_ctx: TemplateContext): string {
|
||||
return generatePlanCompletionAuditInner('ship');
|
||||
}
|
||||
|
||||
export function generatePlanCompletionAuditReview(_ctx: TemplateContext): string {
|
||||
return generatePlanCompletionAuditInner('review');
|
||||
}
|
||||
|
||||
// ─── Plan Verification Execution ──────────────────────────────────────
|
||||
|
||||
export function generatePlanVerificationExec(_ctx: TemplateContext): string {
|
||||
return `## Step 3.47: Plan Verification
|
||||
|
||||
Automatically verify the plan's testing/verification steps using the \`/qa-only\` skill.
|
||||
|
||||
### 1. Check for verification section
|
||||
|
||||
Using the plan file already discovered in Step 3.45, look for a verification section. Match any of these headings: \`## Verification\`, \`## Test plan\`, \`## Testing\`, \`## How to test\`, \`## Manual testing\`, or any section with verification-flavored items (URLs to visit, things to check visually, interactions to test).
|
||||
|
||||
**If no verification section found:** Skip with "No verification steps found in plan — skipping auto-verification."
|
||||
**If no plan file was found in Step 3.45:** Skip (already handled).
|
||||
|
||||
### 2. Check for running dev server
|
||||
|
||||
Before invoking browse-based verification, check if a dev server is reachable:
|
||||
|
||||
\`\`\`bash
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000 2>/dev/null || \\
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080 2>/dev/null || \\
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:5173 2>/dev/null || \\
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000 2>/dev/null || echo "NO_SERVER"
|
||||
\`\`\`
|
||||
|
||||
**If NO_SERVER:** Skip with "No dev server detected — skipping plan verification. Run /qa separately after deploying."
|
||||
|
||||
### 3. Invoke /qa-only inline
|
||||
|
||||
Read the \`/qa-only\` skill from disk:
|
||||
|
||||
\`\`\`bash
|
||||
cat \${CLAUDE_SKILL_DIR}/../qa-only/SKILL.md
|
||||
\`\`\`
|
||||
|
||||
**If unreadable:** Skip with "Could not load /qa-only — skipping plan verification."
|
||||
|
||||
Follow the /qa-only workflow with these modifications:
|
||||
- **Skip the preamble** (already handled by /ship)
|
||||
- **Use the plan's verification section as the primary test input** — treat each verification item as a test case
|
||||
- **Use the detected dev server URL** as the base URL
|
||||
- **Skip the fix loop** — this is report-only verification during /ship
|
||||
- **Cap at the verification items from the plan** — do not expand into general site QA
|
||||
|
||||
### 4. Gate logic
|
||||
|
||||
- **All verification items PASS:** Continue silently. "Plan verification: PASS."
|
||||
- **Any FAIL:** Use AskUserQuestion:
|
||||
- Show the failures with screenshot evidence
|
||||
- RECOMMENDATION: Choose A if failures indicate broken functionality. Choose B if cosmetic only.
|
||||
- Options:
|
||||
A) Fix the failures before shipping (recommended for functional issues)
|
||||
B) Ship anyway — known issues (acceptable for cosmetic issues)
|
||||
- **No verification section / no server / unreadable skill:** Skip (non-blocking).
|
||||
|
||||
### 5. Include in PR body
|
||||
|
||||
Add a \`## Verification Results\` section to the PR body (Step 8):
|
||||
- If verification ran: summary of results (N PASS, M FAIL, K SKIPPED)
|
||||
- If skipped: reason for skipping (no plan, no server, no verification section)`;
|
||||
}
|
||||
|
||||
@@ -454,7 +454,40 @@ find . -name '*.test.*' -o -name '*.spec.*' -o -name '*_test.*' -o -name '*_spec
|
||||
\`\`\`
|
||||
|
||||
For PR body: \`Tests: {before} → {after} (+{delta} new)\`
|
||||
Coverage line: \`Test Coverage Audit: N new code paths. M covered (X%). K tests generated, J committed.\``);
|
||||
Coverage line: \`Test Coverage Audit: N new code paths. M covered (X%). K tests generated, J committed.\`
|
||||
|
||||
**7. Coverage gate:**
|
||||
|
||||
Before proceeding, check CLAUDE.md for a \`## Test Coverage\` section with \`Minimum:\` and \`Target:\` fields. If found, use those percentages. Otherwise use defaults: Minimum = 60%, Target = 80%.
|
||||
|
||||
Using the coverage percentage from the diagram in substep 4 (the \`COVERAGE: X/Y (Z%)\` line):
|
||||
|
||||
- **>= target:** Pass. "Coverage gate: PASS ({X}%)." Continue.
|
||||
- **>= minimum, < target:** Use AskUserQuestion:
|
||||
- "AI-assessed coverage is {X}%. {N} code paths are untested. Target is {target}%."
|
||||
- RECOMMENDATION: Choose A because untested code paths are where production bugs hide.
|
||||
- Options:
|
||||
A) Generate more tests for remaining gaps (recommended)
|
||||
B) Ship anyway — I accept the coverage risk
|
||||
C) These paths don't need tests — mark as intentionally uncovered
|
||||
- If A: Loop back to substep 5 (generate tests) targeting the remaining gaps. After second pass, if still below target, present AskUserQuestion again with updated numbers. Maximum 2 generation passes total.
|
||||
- If B: Continue. Include in PR body: "Coverage gate: {X}% — user accepted risk."
|
||||
- If C: Continue. Include in PR body: "Coverage gate: {X}% — {N} paths intentionally uncovered."
|
||||
|
||||
- **< minimum:** Use AskUserQuestion:
|
||||
- "AI-assessed coverage is critically low ({X}%). {N} of {M} code paths have no tests. Minimum threshold is {minimum}%."
|
||||
- RECOMMENDATION: Choose A because less than {minimum}% means more code is untested than tested.
|
||||
- Options:
|
||||
A) Generate tests for remaining gaps (recommended)
|
||||
B) Override — ship with low coverage (I understand the risk)
|
||||
- If A: Loop back to substep 5. Maximum 2 passes. If still below minimum after 2 passes, present the override choice again.
|
||||
- If B: Continue. Include in PR body: "Coverage gate: OVERRIDDEN at {X}%."
|
||||
|
||||
**Coverage percentage undetermined:** If the coverage diagram doesn't produce a clear numeric percentage (ambiguous output, parse error), **skip the gate** with: "Coverage gate: could not determine percentage — skipping." Do not default to 0% or block.
|
||||
|
||||
**Test-only diffs:** Skip the gate (same as the existing fast-path).
|
||||
|
||||
**100% coverage:** "Coverage gate: PASS (100%)." Continue.`);
|
||||
|
||||
// ── Test plan artifact (ship mode) ──
|
||||
sections.push(`
|
||||
@@ -504,7 +537,22 @@ If test framework is detected and gaps were identified:
|
||||
|
||||
If no test framework detected → include gaps as INFORMATIONAL findings only, no generation.
|
||||
|
||||
**Diff is test-only changes:** Skip Step 4.75 entirely: "No new application code paths to audit."`);
|
||||
**Diff is test-only changes:** Skip Step 4.75 entirely: "No new application code paths to audit."
|
||||
|
||||
### Coverage Warning
|
||||
|
||||
After producing the coverage diagram, check the coverage percentage. Read CLAUDE.md for a \`## Test Coverage\` section with a \`Minimum:\` field. If not found, use default: 60%.
|
||||
|
||||
If coverage is below the minimum threshold, output a prominent warning **before** the regular review findings:
|
||||
|
||||
\`\`\`
|
||||
⚠️ COVERAGE WARNING: AI-assessed coverage is {X}%. {N} code paths untested.
|
||||
Consider writing tests before running /ship.
|
||||
\`\`\`
|
||||
|
||||
This is INFORMATIONAL — does not block /review. But it makes low coverage visible early so the developer can address it before reaching the /ship coverage gate.
|
||||
|
||||
If coverage percentage cannot be determined, skip the warning silently.`);
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
|
||||
@@ -3,7 +3,6 @@ name: setup-browser-cookies
|
||||
preamble-tier: 1
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /setup-browser-cookies.
|
||||
Import cookies from your real Chromium browser into the headless browse session.
|
||||
Opens an interactive picker UI where you select which cookie domains to import.
|
||||
Use before QA testing authenticated pages. Use when asked to "import cookies",
|
||||
@@ -27,9 +26,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -47,8 +48,11 @@ echo '{"skill":"setup-browser-cookies","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -97,6 +101,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## Contributor Mode
|
||||
|
||||
If `_CONTRIB` is `true`: you are in **contributor mode**. At the end of each major workflow step, rate your gstack experience 0-10. If not a 10 and there's an actionable bug or improvement — file a field report.
|
||||
|
||||
+28
-3
@@ -3,7 +3,6 @@ name: setup-deploy
|
||||
preamble-tier: 2
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /setup-deploy.
|
||||
Configure deployment settings for /land-and-deploy. Detects your deploy
|
||||
platform (Fly.io, Render, Vercel, Netlify, Heroku, GitHub Actions, custom),
|
||||
production URL, health check endpoints, and deploy status commands. Writes
|
||||
@@ -33,9 +32,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -53,8 +54,11 @@ echo '{"skill":"setup-deploy","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -103,6 +107,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
|
||||
+276
-4
@@ -3,7 +3,6 @@ name: ship
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /ship.
|
||||
Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR. Use when asked to "ship", "deploy", "push to main", "create a PR", or "merge and push".
|
||||
Proactively suggest when the user says code is ready or asks about deploying.
|
||||
allowed-tools:
|
||||
@@ -31,9 +30,11 @@ _SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr
|
||||
find ~/.gstack/sessions -mmin +120 -type f -delete 2>/dev/null || true
|
||||
_CONTRIB=$(~/.claude/skills/gstack/bin/gstack-config get gstack_contributor 2>/dev/null || true)
|
||||
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
||||
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
||||
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
echo "BRANCH: $_BRANCH"
|
||||
echo "PROACTIVE: $_PROACTIVE"
|
||||
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
||||
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
||||
REPO_MODE=${REPO_MODE:-unknown}
|
||||
echo "REPO_MODE: $REPO_MODE"
|
||||
@@ -51,8 +52,11 @@ echo '{"skill":"ship","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basenam
|
||||
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do [ -f "$_PF" ] && ~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true; break; done
|
||||
```
|
||||
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills — only invoke
|
||||
them when the user explicitly asks. The user opted out of proactive suggestions.
|
||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
||||
The user opted out of proactive behavior.
|
||||
|
||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||
|
||||
@@ -101,6 +105,27 @@ touch ~/.gstack/.telemetry-prompted
|
||||
|
||||
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
||||
ask the user about proactive behavior. Use AskUserQuestion:
|
||||
|
||||
> gstack can proactively figure out when you might need a skill while you work —
|
||||
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
||||
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
||||
|
||||
Options:
|
||||
- A) Keep it on (recommended)
|
||||
- B) Turn it off — I'll type /commands myself
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
||||
|
||||
Always run:
|
||||
```bash
|
||||
touch ~/.gstack/.proactive-prompted
|
||||
```
|
||||
|
||||
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
||||
|
||||
## AskUserQuestion Format
|
||||
|
||||
**ALWAYS follow this structure for every AskUserQuestion call:**
|
||||
@@ -283,6 +308,9 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
|
||||
- Pre-landing review finds ASK items that need user judgment
|
||||
- MINOR or MAJOR version bump needed (ask — see Step 4)
|
||||
- Greptile review comments that need user decision (complex fixes, false positives)
|
||||
- AI-assessed coverage below minimum threshold (hard gate with user override — see Step 3.4)
|
||||
- Plan items NOT DONE with no user override (see Step 3.45)
|
||||
- Plan verification failures (see Step 3.47)
|
||||
- TODOS.md missing and user wants to create one (ask — see Step 5.5)
|
||||
- TODOS.md disorganized and user wants to reorganize (ask — see Step 5.5)
|
||||
- Screenshots: asking whether to capture PR screenshots (see Step 6.75)
|
||||
@@ -295,7 +323,7 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
|
||||
- Multi-file changesets (auto-split into bisectable commits)
|
||||
- TODOS.md completed-item detection (auto-mark)
|
||||
- Auto-fixable review findings (dead code, N+1, stale comments — fixed automatically)
|
||||
- Test coverage gaps (auto-generate and commit, or flag in PR body)
|
||||
- Test coverage gaps within target threshold (auto-generate and commit, or flag in PR body)
|
||||
|
||||
---
|
||||
|
||||
@@ -954,6 +982,39 @@ find . -name '*.test.*' -o -name '*.spec.*' -o -name '*_test.*' -o -name '*_spec
|
||||
For PR body: `Tests: {before} → {after} (+{delta} new)`
|
||||
Coverage line: `Test Coverage Audit: N new code paths. M covered (X%). K tests generated, J committed.`
|
||||
|
||||
**7. Coverage gate:**
|
||||
|
||||
Before proceeding, check CLAUDE.md for a `## Test Coverage` section with `Minimum:` and `Target:` fields. If found, use those percentages. Otherwise use defaults: Minimum = 60%, Target = 80%.
|
||||
|
||||
Using the coverage percentage from the diagram in substep 4 (the `COVERAGE: X/Y (Z%)` line):
|
||||
|
||||
- **>= target:** Pass. "Coverage gate: PASS ({X}%)." Continue.
|
||||
- **>= minimum, < target:** Use AskUserQuestion:
|
||||
- "AI-assessed coverage is {X}%. {N} code paths are untested. Target is {target}%."
|
||||
- RECOMMENDATION: Choose A because untested code paths are where production bugs hide.
|
||||
- Options:
|
||||
A) Generate more tests for remaining gaps (recommended)
|
||||
B) Ship anyway — I accept the coverage risk
|
||||
C) These paths don't need tests — mark as intentionally uncovered
|
||||
- If A: Loop back to substep 5 (generate tests) targeting the remaining gaps. After second pass, if still below target, present AskUserQuestion again with updated numbers. Maximum 2 generation passes total.
|
||||
- If B: Continue. Include in PR body: "Coverage gate: {X}% — user accepted risk."
|
||||
- If C: Continue. Include in PR body: "Coverage gate: {X}% — {N} paths intentionally uncovered."
|
||||
|
||||
- **< minimum:** Use AskUserQuestion:
|
||||
- "AI-assessed coverage is critically low ({X}%). {N} of {M} code paths have no tests. Minimum threshold is {minimum}%."
|
||||
- RECOMMENDATION: Choose A because less than {minimum}% means more code is untested than tested.
|
||||
- Options:
|
||||
A) Generate tests for remaining gaps (recommended)
|
||||
B) Override — ship with low coverage (I understand the risk)
|
||||
- If A: Loop back to substep 5. Maximum 2 passes. If still below minimum after 2 passes, present the override choice again.
|
||||
- If B: Continue. Include in PR body: "Coverage gate: OVERRIDDEN at {X}%."
|
||||
|
||||
**Coverage percentage undetermined:** If the coverage diagram doesn't produce a clear numeric percentage (ambiguous output, parse error), **skip the gate** with: "Coverage gate: could not determine percentage — skipping." Do not default to 0% or block.
|
||||
|
||||
**Test-only diffs:** Skip the gate (same as the existing fast-path).
|
||||
|
||||
**100% coverage:** "Coverage gate: PASS (100%)." Continue.
|
||||
|
||||
### Test Plan Artifact
|
||||
|
||||
After producing the coverage diagram, write a test plan artifact so `/qa` and `/qa-only` can consume it:
|
||||
@@ -987,6 +1048,181 @@ Repo: {owner/repo}
|
||||
|
||||
---
|
||||
|
||||
## Step 3.45: Plan Completion Audit
|
||||
|
||||
### Plan File Discovery
|
||||
|
||||
1. **Conversation context (primary):** Check if there is an active plan file in this conversation — Claude Code system messages include plan file paths when in plan mode. Look for references like `~/.claude/plans/*.md` in system messages. If found, use it directly — this is the most reliable signal.
|
||||
|
||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||
|
||||
```bash
|
||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||
# Try branch name match first (most specific)
|
||||
PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1)
|
||||
# Fall back to repo name match
|
||||
[ -z "$PLAN" ] && PLAN=$(ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1)
|
||||
# Last resort: most recent plan modified in the last 24 hours
|
||||
[ -z "$PLAN" ] && PLAN=$(find ~/.claude/plans -name '*.md' -mmin -1440 -maxdepth 1 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
|
||||
[ -n "$PLAN" ] && echo "PLAN_FILE: $PLAN" || echo "NO_PLAN_FILE"
|
||||
```
|
||||
|
||||
3. **Validation:** If a plan file was found via content-based search (not conversation context), read the first 20 lines and verify it is relevant to the current branch's work. If it appears to be from a different project or feature, treat as "no plan file found."
|
||||
|
||||
**Error handling:**
|
||||
- No plan file found → skip with "No plan file detected — skipping."
|
||||
- Plan file found but unreadable (permissions, encoding) → skip with "Plan file found but unreadable — skipping."
|
||||
|
||||
### Actionable Item Extraction
|
||||
|
||||
Read the plan file. Extract every actionable item — anything that describes work to be done. Look for:
|
||||
|
||||
- **Checkbox items:** `- [ ] ...` or `- [x] ...`
|
||||
- **Numbered steps** under implementation headings: "1. Create ...", "2. Add ...", "3. Modify ..."
|
||||
- **Imperative statements:** "Add X to Y", "Create a Z service", "Modify the W controller"
|
||||
- **File-level specifications:** "New file: path/to/file.ts", "Modify path/to/existing.rb"
|
||||
- **Test requirements:** "Test that X", "Add test for Y", "Verify Z"
|
||||
- **Data model changes:** "Add column X to table Y", "Create migration for Z"
|
||||
|
||||
**Ignore:**
|
||||
- Context/Background sections (`## Context`, `## Background`, `## Problem`)
|
||||
- Questions and open items (marked with ?, "TBD", "TODO: decide")
|
||||
- Review report sections (`## GSTACK REVIEW REPORT`)
|
||||
- Explicitly deferred items ("Future:", "Out of scope:", "NOT in scope:", "P2:", "P3:", "P4:")
|
||||
- CEO Review Decisions sections (these record choices, not work items)
|
||||
|
||||
**Cap:** Extract at most 50 items. If the plan has more, note: "Showing top 50 of N plan items — full list in plan file."
|
||||
|
||||
**No items found:** If the plan contains no extractable actionable items, skip with: "Plan file contains no actionable items — skipping completion audit."
|
||||
|
||||
For each item, note:
|
||||
- The item text (verbatim or concise summary)
|
||||
- Its category: CODE | TEST | MIGRATION | CONFIG | DOCS
|
||||
|
||||
### Cross-Reference Against Diff
|
||||
|
||||
Run `git diff origin/<base>...HEAD` and `git log origin/<base>..HEAD --oneline` to understand what was implemented.
|
||||
|
||||
For each extracted plan item, check the diff and classify:
|
||||
|
||||
- **DONE** — Clear evidence in the diff that this item was implemented. Cite the specific file(s) changed.
|
||||
- **PARTIAL** — Some work toward this item exists in the diff but it's incomplete (e.g., model created but controller missing, function exists but edge cases not handled).
|
||||
- **NOT DONE** — No evidence in the diff that this item was addressed.
|
||||
- **CHANGED** — The item was implemented using a different approach than the plan described, but the same goal is achieved. Note the difference.
|
||||
|
||||
**Be conservative with DONE** — require clear evidence in the diff. A file being touched is not enough; the specific functionality described must be present.
|
||||
**Be generous with CHANGED** — if the goal is met by different means, that counts as addressed.
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
PLAN COMPLETION AUDIT
|
||||
═══════════════════════════════
|
||||
Plan: {plan file path}
|
||||
|
||||
## Implementation Items
|
||||
[DONE] Create UserService — src/services/user_service.rb (+142 lines)
|
||||
[PARTIAL] Add validation — model validates but missing controller checks
|
||||
[NOT DONE] Add caching layer — no cache-related changes in diff
|
||||
[CHANGED] "Redis queue" → implemented with Sidekiq instead
|
||||
|
||||
## Test Items
|
||||
[DONE] Unit tests for UserService — test/services/user_service_test.rb
|
||||
[NOT DONE] E2E test for signup flow
|
||||
|
||||
## Migration Items
|
||||
[DONE] Create users table — db/migrate/20240315_create_users.rb
|
||||
|
||||
─────────────────────────────────
|
||||
COMPLETION: 4/7 DONE, 1 PARTIAL, 1 NOT DONE, 1 CHANGED
|
||||
─────────────────────────────────
|
||||
```
|
||||
|
||||
### Gate Logic
|
||||
|
||||
After producing the completion checklist:
|
||||
|
||||
- **All DONE or CHANGED:** Pass. "Plan completion: PASS — all items addressed." Continue.
|
||||
- **Only PARTIAL items (no NOT DONE):** Continue with a note in the PR body. Not blocking.
|
||||
- **Any NOT DONE items:** Use AskUserQuestion:
|
||||
- Show the completion checklist above
|
||||
- "{N} items from the plan are NOT DONE. These were part of the original plan but are missing from the implementation."
|
||||
- RECOMMENDATION: depends on item count and severity. If 1-2 minor items (docs, config), recommend B. If core functionality is missing, recommend A.
|
||||
- Options:
|
||||
A) Stop — implement the missing items before shipping
|
||||
B) Ship anyway — defer these to a follow-up (will create P1 TODOs in Step 5.5)
|
||||
C) These items were intentionally dropped — remove from scope
|
||||
- If A: STOP. List the missing items for the user to implement.
|
||||
- If B: Continue. For each NOT DONE item, create a P1 TODO in Step 5.5 with "Deferred from plan: {plan file path}".
|
||||
- If C: Continue. Note in PR body: "Plan items intentionally dropped: {list}."
|
||||
|
||||
**No plan file found:** Skip entirely. "No plan file detected — skipping plan completion audit."
|
||||
|
||||
**Include in PR body (Step 8):** Add a `## Plan Completion` section with the checklist summary.
|
||||
|
||||
---
|
||||
|
||||
## Step 3.47: Plan Verification
|
||||
|
||||
Automatically verify the plan's testing/verification steps using the `/qa-only` skill.
|
||||
|
||||
### 1. Check for verification section
|
||||
|
||||
Using the plan file already discovered in Step 3.45, look for a verification section. Match any of these headings: `## Verification`, `## Test plan`, `## Testing`, `## How to test`, `## Manual testing`, or any section with verification-flavored items (URLs to visit, things to check visually, interactions to test).
|
||||
|
||||
**If no verification section found:** Skip with "No verification steps found in plan — skipping auto-verification."
|
||||
**If no plan file was found in Step 3.45:** Skip (already handled).
|
||||
|
||||
### 2. Check for running dev server
|
||||
|
||||
Before invoking browse-based verification, check if a dev server is reachable:
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000 2>/dev/null || \
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080 2>/dev/null || \
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:5173 2>/dev/null || \
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000 2>/dev/null || echo "NO_SERVER"
|
||||
```
|
||||
|
||||
**If NO_SERVER:** Skip with "No dev server detected — skipping plan verification. Run /qa separately after deploying."
|
||||
|
||||
### 3. Invoke /qa-only inline
|
||||
|
||||
Read the `/qa-only` skill from disk:
|
||||
|
||||
```bash
|
||||
cat ${CLAUDE_SKILL_DIR}/../qa-only/SKILL.md
|
||||
```
|
||||
|
||||
**If unreadable:** Skip with "Could not load /qa-only — skipping plan verification."
|
||||
|
||||
Follow the /qa-only workflow with these modifications:
|
||||
- **Skip the preamble** (already handled by /ship)
|
||||
- **Use the plan's verification section as the primary test input** — treat each verification item as a test case
|
||||
- **Use the detected dev server URL** as the base URL
|
||||
- **Skip the fix loop** — this is report-only verification during /ship
|
||||
- **Cap at the verification items from the plan** — do not expand into general site QA
|
||||
|
||||
### 4. Gate logic
|
||||
|
||||
- **All verification items PASS:** Continue silently. "Plan verification: PASS."
|
||||
- **Any FAIL:** Use AskUserQuestion:
|
||||
- Show the failures with screenshot evidence
|
||||
- RECOMMENDATION: Choose A if failures indicate broken functionality. Choose B if cosmetic only.
|
||||
- Options:
|
||||
A) Fix the failures before shipping (recommended for functional issues)
|
||||
B) Ship anyway — known issues (acceptable for cosmetic issues)
|
||||
- **No verification section / no server / unreadable skill:** Skip (non-blocking).
|
||||
|
||||
### 5. Include in PR body
|
||||
|
||||
Add a `## Verification Results` section to the PR body (Step 8):
|
||||
- If verification ran: summary of results (N PASS, M FAIL, K SKIPPED)
|
||||
- If skipped: reason for skipping (no plan, no server, no verification section)
|
||||
|
||||
---
|
||||
|
||||
## Step 3.5: Pre-Landing Review
|
||||
|
||||
Review the diff for structural issues that tests don't catch.
|
||||
@@ -1523,6 +1759,16 @@ gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
||||
<If no Greptile comments found: "No Greptile comments.">
|
||||
<If no PR existed during Step 3.75: omit this section entirely>
|
||||
|
||||
## Plan Completion
|
||||
<If plan file found: completion checklist summary from Step 3.45>
|
||||
<If no plan file: "No plan file detected.">
|
||||
<If plan items deferred: list deferred items>
|
||||
|
||||
## Verification Results
|
||||
<If verification ran: summary from Step 3.47 (N PASS, M FAIL, K SKIPPED)>
|
||||
<If skipped: reason (no plan, no server, no verification section)>
|
||||
<If not applicable: omit this section>
|
||||
|
||||
## TODOS
|
||||
<If items marked complete: bullet list of completed items with version>
|
||||
<If no items completed: "No TODO items completed in this PR.">
|
||||
@@ -1575,6 +1821,32 @@ doc updates — the user runs `/ship` and documentation stays current without a
|
||||
|
||||
---
|
||||
|
||||
## Step 8.75: Persist ship metrics
|
||||
|
||||
Log coverage and plan completion data so `/retro` can track trends:
|
||||
|
||||
```bash
|
||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
||||
```
|
||||
|
||||
Append to `~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl`:
|
||||
|
||||
```bash
|
||||
echo '{"skill":"ship","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","coverage_pct":COVERAGE_PCT,"plan_items_total":PLAN_TOTAL,"plan_items_done":PLAN_DONE,"verification_result":"VERIFY_RESULT","version":"VERSION","branch":"BRANCH"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
|
||||
```
|
||||
|
||||
Substitute from earlier steps:
|
||||
- **COVERAGE_PCT**: coverage percentage from Step 3.4 diagram (integer, or -1 if undetermined)
|
||||
- **PLAN_TOTAL**: total plan items extracted in Step 3.45 (0 if no plan file)
|
||||
- **PLAN_DONE**: count of DONE + CHANGED items from Step 3.45 (0 if no plan file)
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped" from Step 3.47
|
||||
- **VERSION**: from the VERSION file
|
||||
- **BRANCH**: current branch name
|
||||
|
||||
This step is automatic — never skip it, never ask for confirmation.
|
||||
|
||||
---
|
||||
|
||||
## Important Rules
|
||||
|
||||
- **Never skip tests.** If tests fail, stop.
|
||||
|
||||
+50
-1
@@ -32,6 +32,9 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
|
||||
- Pre-landing review finds ASK items that need user judgment
|
||||
- MINOR or MAJOR version bump needed (ask — see Step 4)
|
||||
- Greptile review comments that need user decision (complex fixes, false positives)
|
||||
- AI-assessed coverage below minimum threshold (hard gate with user override — see Step 3.4)
|
||||
- Plan items NOT DONE with no user override (see Step 3.45)
|
||||
- Plan verification failures (see Step 3.47)
|
||||
- TODOS.md missing and user wants to create one (ask — see Step 5.5)
|
||||
- TODOS.md disorganized and user wants to reorganize (ask — see Step 5.5)
|
||||
- Screenshots: asking whether to capture PR screenshots (see Step 6.75)
|
||||
@@ -44,7 +47,7 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
|
||||
- Multi-file changesets (auto-split into bisectable commits)
|
||||
- TODOS.md completed-item detection (auto-mark)
|
||||
- Auto-fixable review findings (dead code, N+1, stale comments — fixed automatically)
|
||||
- Test coverage gaps (auto-generate and commit, or flag in PR body)
|
||||
- Test coverage gaps within target threshold (auto-generate and commit, or flag in PR body)
|
||||
|
||||
---
|
||||
|
||||
@@ -226,6 +229,16 @@ If multiple suites need to run, run them sequentially (each needs a test lane).
|
||||
|
||||
---
|
||||
|
||||
## Step 3.45: Plan Completion Audit
|
||||
|
||||
{{PLAN_COMPLETION_AUDIT_SHIP}}
|
||||
|
||||
---
|
||||
|
||||
{{PLAN_VERIFICATION_EXEC}}
|
||||
|
||||
---
|
||||
|
||||
## Step 3.5: Pre-Landing Review
|
||||
|
||||
Review the diff for structural issues that tests don't catch.
|
||||
@@ -575,6 +588,16 @@ gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
||||
<If no Greptile comments found: "No Greptile comments.">
|
||||
<If no PR existed during Step 3.75: omit this section entirely>
|
||||
|
||||
## Plan Completion
|
||||
<If plan file found: completion checklist summary from Step 3.45>
|
||||
<If no plan file: "No plan file detected.">
|
||||
<If plan items deferred: list deferred items>
|
||||
|
||||
## Verification Results
|
||||
<If verification ran: summary from Step 3.47 (N PASS, M FAIL, K SKIPPED)>
|
||||
<If skipped: reason (no plan, no server, no verification section)>
|
||||
<If not applicable: omit this section>
|
||||
|
||||
## TODOS
|
||||
<If items marked complete: bullet list of completed items with version>
|
||||
<If no items completed: "No TODO items completed in this PR.">
|
||||
@@ -627,6 +650,32 @@ doc updates — the user runs `/ship` and documentation stays current without a
|
||||
|
||||
---
|
||||
|
||||
## Step 8.75: Persist ship metrics
|
||||
|
||||
Log coverage and plan completion data so `/retro` can track trends:
|
||||
|
||||
```bash
|
||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
||||
```
|
||||
|
||||
Append to `~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl`:
|
||||
|
||||
```bash
|
||||
echo '{"skill":"ship","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","coverage_pct":COVERAGE_PCT,"plan_items_total":PLAN_TOTAL,"plan_items_done":PLAN_DONE,"verification_result":"VERIFY_RESULT","version":"VERSION","branch":"BRANCH"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl
|
||||
```
|
||||
|
||||
Substitute from earlier steps:
|
||||
- **COVERAGE_PCT**: coverage percentage from Step 3.4 diagram (integer, or -1 if undetermined)
|
||||
- **PLAN_TOTAL**: total plan items extracted in Step 3.45 (0 if no plan file)
|
||||
- **PLAN_DONE**: count of DONE + CHANGED items from Step 3.45 (0 if no plan file)
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped" from Step 3.47
|
||||
- **VERSION**: from the VERSION file
|
||||
- **BRANCH**: current branch name
|
||||
|
||||
This step is automatic — never skip it, never ask for confirmation.
|
||||
|
||||
---
|
||||
|
||||
## Important Rules
|
||||
|
||||
- **Never skip tests.** If tests fail, stop.
|
||||
|
||||
+2
-4
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Supabase project config for gstack telemetry
|
||||
# These are PUBLIC keys — safe to commit (like Firebase public config).
|
||||
# RLS policies restrict what the anon/publishable key can do (INSERT only).
|
||||
# RLS denies all access to the anon key. All reads and writes go through
|
||||
# edge functions (which use SUPABASE_SERVICE_ROLE_KEY server-side).
|
||||
|
||||
GSTACK_SUPABASE_URL="https://frugpmstpnojnhfyimgv.supabase.co"
|
||||
GSTACK_SUPABASE_ANON_KEY="sb_publishable_tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK"
|
||||
|
||||
# Telemetry ingest endpoint (Data API)
|
||||
GSTACK_TELEMETRY_ENDPOINT="${GSTACK_SUPABASE_URL}/rest/v1"
|
||||
|
||||
# gstack.gg web app (auth + screenshot upload)
|
||||
GSTACK_WEB_URL="https://gstack.gg"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// gstack community-pulse edge function
|
||||
// Returns weekly active installation count for preamble display.
|
||||
// Cached for 1 hour via Cache-Control header.
|
||||
// Returns aggregated community stats for the dashboard:
|
||||
// weekly active count, top skills, crash clusters, version distribution.
|
||||
// Uses server-side cache (community_pulse_cache table) to prevent DoS.
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
Deno.serve(async () => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL") ?? "",
|
||||
@@ -11,68 +14,123 @@ Deno.serve(async () => {
|
||||
);
|
||||
|
||||
try {
|
||||
// Count unique update checks in the last 7 days (install base proxy)
|
||||
// Check cache first
|
||||
const { data: cached } = await supabase
|
||||
.from("community_pulse_cache")
|
||||
.select("data, refreshed_at")
|
||||
.eq("id", 1)
|
||||
.single();
|
||||
|
||||
if (cached?.refreshed_at) {
|
||||
const age = Date.now() - new Date(cached.refreshed_at).getTime();
|
||||
if (age < CACHE_MAX_AGE_MS) {
|
||||
return new Response(JSON.stringify(cached.data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cache is stale or missing — recompute
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
// This week's unique installs (by install_fingerprint, filtered to source=live)
|
||||
const { data: thisWeekData } = await supabase
|
||||
// Weekly active (update checks this week)
|
||||
const { count: thisWeek } = await supabase
|
||||
.from("update_checks")
|
||||
.select("install_fingerprint")
|
||||
.eq("source", "live")
|
||||
.gte("checked_at", weekAgo);
|
||||
|
||||
// Last week's unique installs (for change %)
|
||||
const { data: lastWeekData } = await supabase
|
||||
// Last week (for change %)
|
||||
const { count: lastWeek } = await supabase
|
||||
.from("update_checks")
|
||||
.select("install_fingerprint")
|
||||
.eq("source", "live")
|
||||
.gte("checked_at", twoWeeksAgo)
|
||||
.lt("checked_at", weekAgo);
|
||||
|
||||
let current = new Set((thisWeekData ?? []).map((e: { install_fingerprint: string }) => e.install_fingerprint).filter(Boolean)).size;
|
||||
let previous = new Set((lastWeekData ?? []).map((e: { install_fingerprint: string }) => e.install_fingerprint).filter(Boolean)).size;
|
||||
|
||||
// Fallback: if no fingerprinted data, count distinct sessions from telemetry_events
|
||||
if (current === 0) {
|
||||
const { data: thisWeekSessions } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("session_id")
|
||||
.eq("event_type", "skill_run")
|
||||
.eq("source", "live")
|
||||
.gte("event_timestamp", weekAgo);
|
||||
|
||||
const { data: lastWeekSessions } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("session_id")
|
||||
.eq("event_type", "skill_run")
|
||||
.eq("source", "live")
|
||||
.gte("event_timestamp", twoWeeksAgo)
|
||||
.lt("event_timestamp", weekAgo);
|
||||
|
||||
current = new Set((thisWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size;
|
||||
previous = new Set((lastWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size;
|
||||
}
|
||||
const current = thisWeek ?? 0;
|
||||
const previous = lastWeek ?? 0;
|
||||
const changePct = previous > 0
|
||||
? Math.round(((current - previous) / previous) * 100)
|
||||
: 0;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
weekly_active: current,
|
||||
change_pct: changePct,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600", // 1 hour cache
|
||||
},
|
||||
// Top skills (last 7 days)
|
||||
const { data: skillRows } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("skill")
|
||||
.eq("event_type", "skill_run")
|
||||
.gte("event_timestamp", weekAgo)
|
||||
.not("skill", "is", null)
|
||||
.limit(1000);
|
||||
|
||||
const skillCounts: Record<string, number> = {};
|
||||
for (const row of skillRows ?? []) {
|
||||
if (row.skill) {
|
||||
skillCounts[row.skill] = (skillCounts[row.skill] ?? 0) + 1;
|
||||
}
|
||||
);
|
||||
}
|
||||
const topSkills = Object.entries(skillCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([skill, count]) => ({ skill, count }));
|
||||
|
||||
// Crash clusters (top 5)
|
||||
const { data: crashes } = await supabase
|
||||
.from("crash_clusters")
|
||||
.select("error_class, gstack_version, total_occurrences, identified_users")
|
||||
.limit(5);
|
||||
|
||||
// Version distribution (last 7 days)
|
||||
const versionCounts: Record<string, number> = {};
|
||||
const { data: versionRows } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("gstack_version")
|
||||
.eq("event_type", "skill_run")
|
||||
.gte("event_timestamp", weekAgo)
|
||||
.limit(1000);
|
||||
|
||||
for (const row of versionRows ?? []) {
|
||||
if (row.gstack_version) {
|
||||
versionCounts[row.gstack_version] = (versionCounts[row.gstack_version] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
const topVersions = Object.entries(versionCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([version, count]) => ({ version, count }));
|
||||
|
||||
const result = {
|
||||
weekly_active: current,
|
||||
change_pct: changePct,
|
||||
top_skills: topSkills,
|
||||
crashes: crashes ?? [],
|
||||
versions: topVersions,
|
||||
};
|
||||
|
||||
// Upsert cache
|
||||
await supabase
|
||||
.from("community_pulse_cache")
|
||||
.upsert({
|
||||
id: 1,
|
||||
data: result,
|
||||
refreshed_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new Response(
|
||||
JSON.stringify({ weekly_active: 0, change_pct: 0 }),
|
||||
JSON.stringify({ weekly_active: 0, change_pct: 0, top_skills: [], crashes: [], versions: [] }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
-- 002_tighten_rls.sql
|
||||
-- Lock down read/update access. Keep INSERT policies so old clients can still
|
||||
-- write via PostgREST while new clients migrate to edge functions.
|
||||
|
||||
-- Drop all SELECT policies (anon key should not read telemetry data)
|
||||
DROP POLICY IF EXISTS "anon_select" ON telemetry_events;
|
||||
DROP POLICY IF EXISTS "anon_select" ON installations;
|
||||
DROP POLICY IF EXISTS "anon_select" ON update_checks;
|
||||
|
||||
-- Drop dangerous UPDATE policy (was unrestricted on all columns)
|
||||
DROP POLICY IF EXISTS "anon_update_last_seen" ON installations;
|
||||
|
||||
-- Keep INSERT policies — old clients (pre-v0.11.16) still POST directly to
|
||||
-- PostgREST. These will be dropped in a future migration once adoption of
|
||||
-- edge-function-based sync is widespread.
|
||||
-- (anon_insert_only ON telemetry_events — kept)
|
||||
-- (anon_insert_only ON installations — kept)
|
||||
-- (anon_insert_only ON update_checks — kept)
|
||||
|
||||
-- Explicitly revoke view access (belt-and-suspenders)
|
||||
REVOKE SELECT ON crash_clusters FROM anon;
|
||||
REVOKE SELECT ON skill_sequences FROM anon;
|
||||
|
||||
-- Keep error_message and failed_step columns (exist on live schema, may be
|
||||
-- used in future). Add them to the migration record so repo matches live.
|
||||
ALTER TABLE telemetry_events ADD COLUMN IF NOT EXISTS error_message TEXT;
|
||||
ALTER TABLE telemetry_events ADD COLUMN IF NOT EXISTS failed_step TEXT;
|
||||
|
||||
-- Cache table for community-pulse aggregation (prevents DoS via repeated queries)
|
||||
CREATE TABLE IF NOT EXISTS community_pulse_cache (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||
data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
refreshed_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
ALTER TABLE community_pulse_cache ENABLE ROW LEVEL SECURITY;
|
||||
-- No anon policies — only service_role_key (used by edge functions) can read/write
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-rls.sh — smoke test after deploying 002_tighten_rls.sql
|
||||
#
|
||||
# Verifies:
|
||||
# - SELECT denied on all tables and views (security fix)
|
||||
# - UPDATE denied on installations (security fix)
|
||||
# - INSERT still allowed on tables (kept for old client compat)
|
||||
#
|
||||
# Run manually after deploying the migration:
|
||||
# bash supabase/verify-rls.sh
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/config.sh"
|
||||
|
||||
URL="$GSTACK_SUPABASE_URL"
|
||||
KEY="$GSTACK_SUPABASE_ANON_KEY"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TOTAL=0
|
||||
|
||||
# check <description> <expected> <method> <path> [data]
|
||||
# expected: "deny" (want 401/403) or "allow" (want 200/201)
|
||||
check() {
|
||||
local desc="$1"
|
||||
local expected="$2"
|
||||
local method="$3"
|
||||
local path="$4"
|
||||
local data="${5:-}"
|
||||
TOTAL=$(( TOTAL + 1 ))
|
||||
|
||||
local resp_file
|
||||
resp_file="$(mktemp 2>/dev/null || echo "/tmp/verify-rls-$$-$TOTAL")"
|
||||
|
||||
local http_code
|
||||
if [ "$method" = "GET" ]; then
|
||||
http_code="$(curl -s -o "$resp_file" -w '%{http_code}' --max-time 10 \
|
||||
"${URL}/rest/v1/${path}" \
|
||||
-H "apikey: ${KEY}" \
|
||||
-H "Authorization: Bearer ${KEY}" \
|
||||
-H "Content-Type: application/json" 2>/dev/null)" || http_code="000"
|
||||
elif [ "$method" = "POST" ]; then
|
||||
http_code="$(curl -s -o "$resp_file" -w '%{http_code}' --max-time 10 \
|
||||
-X POST "${URL}/rest/v1/${path}" \
|
||||
-H "apikey: ${KEY}" \
|
||||
-H "Authorization: Bearer ${KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d "$data" 2>/dev/null)" || http_code="000"
|
||||
elif [ "$method" = "PATCH" ]; then
|
||||
http_code="$(curl -s -o "$resp_file" -w '%{http_code}' --max-time 10 \
|
||||
-X PATCH "${URL}/rest/v1/${path}" \
|
||||
-H "apikey: ${KEY}" \
|
||||
-H "Authorization: Bearer ${KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" 2>/dev/null)" || http_code="000"
|
||||
fi
|
||||
|
||||
# Trim to last 3 chars (the HTTP code) in case of concatenation
|
||||
http_code="$(echo "$http_code" | grep -oE '[0-9]{3}$' || echo "000")"
|
||||
|
||||
if [ "$expected" = "deny" ]; then
|
||||
case "$http_code" in
|
||||
401|403)
|
||||
echo " PASS $desc (HTTP $http_code, denied)"
|
||||
PASS=$(( PASS + 1 )) ;;
|
||||
200|204)
|
||||
# For GETs: 200+empty means RLS filtering (pass). 200+data means leak (fail).
|
||||
# For PATCH: 204 means no rows matched — could be RLS or missing row.
|
||||
if [ "$method" = "GET" ]; then
|
||||
body="$(cat "$resp_file" 2>/dev/null || echo "")"
|
||||
if [ "$body" = "[]" ] || [ -z "$body" ]; then
|
||||
echo " PASS $desc (HTTP $http_code, empty — RLS filtering)"
|
||||
PASS=$(( PASS + 1 ))
|
||||
else
|
||||
echo " FAIL $desc (HTTP $http_code, got data!)"
|
||||
FAIL=$(( FAIL + 1 ))
|
||||
fi
|
||||
else
|
||||
# PATCH 204 = no rows affected. RLS blocked the update or row doesn't exist.
|
||||
# Either way, the attacker can't modify data.
|
||||
echo " PASS $desc (HTTP $http_code, no rows affected)"
|
||||
PASS=$(( PASS + 1 ))
|
||||
fi ;;
|
||||
000)
|
||||
echo " WARN $desc (connection failed)"
|
||||
FAIL=$(( FAIL + 1 )) ;;
|
||||
*)
|
||||
echo " WARN $desc (HTTP $http_code — unexpected)"
|
||||
FAIL=$(( FAIL + 1 )) ;;
|
||||
esac
|
||||
elif [ "$expected" = "allow" ]; then
|
||||
case "$http_code" in
|
||||
200|201|204|409)
|
||||
# 409 = conflict (duplicate key) — INSERT policy works, row already exists
|
||||
echo " PASS $desc (HTTP $http_code, allowed as expected)"
|
||||
PASS=$(( PASS + 1 )) ;;
|
||||
401|403)
|
||||
echo " FAIL $desc (HTTP $http_code, denied — should be allowed)"
|
||||
FAIL=$(( FAIL + 1 )) ;;
|
||||
000)
|
||||
echo " WARN $desc (connection failed)"
|
||||
FAIL=$(( FAIL + 1 )) ;;
|
||||
*)
|
||||
echo " WARN $desc (HTTP $http_code — unexpected)"
|
||||
FAIL=$(( FAIL + 1 )) ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
rm -f "$resp_file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
echo "RLS Verification (after 002_tighten_rls.sql)"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Read denial (should be blocked):"
|
||||
check "SELECT telemetry_events" deny GET "telemetry_events?select=*&limit=1"
|
||||
check "SELECT installations" deny GET "installations?select=*&limit=1"
|
||||
check "SELECT update_checks" deny GET "update_checks?select=*&limit=1"
|
||||
check "SELECT crash_clusters" deny GET "crash_clusters?select=*&limit=1"
|
||||
check "SELECT skill_sequences" deny GET "skill_sequences?select=skill_a&limit=1"
|
||||
|
||||
echo ""
|
||||
echo "Update denial (should be blocked):"
|
||||
check "UPDATE installations" deny PATCH "installations?installation_id=eq.test_verify_rls" '{"gstack_version":"hacked"}'
|
||||
|
||||
echo ""
|
||||
echo "Insert allowed (kept for old client compat):"
|
||||
check "INSERT telemetry_events" allow POST "telemetry_events" '{"gstack_version":"verify_rls_test","os":"test","event_timestamp":"2026-01-01T00:00:00Z","outcome":"test"}'
|
||||
check "INSERT update_checks" allow POST "update_checks" '{"gstack_version":"verify_rls_test","os":"test"}'
|
||||
check "INSERT installations" allow POST "installations" '{"installation_id":"verify_rls_test"}'
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Results: $PASS passed, $FAIL failed (of $TOTAL checks)"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "VERDICT: FAIL"
|
||||
exit 1
|
||||
else
|
||||
echo "VERDICT: PASS — reads/updates blocked, inserts allowed"
|
||||
exit 0
|
||||
fi
|
||||
+163
-1
@@ -677,6 +677,168 @@ describe('PLAN_FILE_REVIEW_REPORT resolver', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- {{PLAN_COMPLETION_AUDIT}} resolver tests ---
|
||||
|
||||
describe('PLAN_COMPLETION_AUDIT placeholders', () => {
|
||||
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
const reviewSkill = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('ship SKILL.md contains plan completion audit step', () => {
|
||||
expect(shipSkill).toContain('Plan Completion Audit');
|
||||
expect(shipSkill).toContain('Step 3.45');
|
||||
});
|
||||
|
||||
test('review SKILL.md contains plan completion in scope drift', () => {
|
||||
expect(reviewSkill).toContain('Plan File Discovery');
|
||||
expect(reviewSkill).toContain('Actionable Item Extraction');
|
||||
expect(reviewSkill).toContain('Integration with Scope Drift Detection');
|
||||
});
|
||||
|
||||
test('both modes share plan file discovery methodology', () => {
|
||||
expect(shipSkill).toContain('Plan File Discovery');
|
||||
expect(reviewSkill).toContain('Plan File Discovery');
|
||||
// Both should have conversation context first
|
||||
expect(shipSkill).toContain('Conversation context (primary)');
|
||||
expect(reviewSkill).toContain('Conversation context (primary)');
|
||||
// Both should have grep fallback
|
||||
expect(shipSkill).toContain('Content-based search (fallback)');
|
||||
expect(reviewSkill).toContain('Content-based search (fallback)');
|
||||
});
|
||||
|
||||
test('ship mode has gate logic for NOT DONE items', () => {
|
||||
expect(shipSkill).toContain('NOT DONE');
|
||||
expect(shipSkill).toContain('Stop — implement the missing items');
|
||||
expect(shipSkill).toContain('Ship anyway — defer');
|
||||
expect(shipSkill).toContain('intentionally dropped');
|
||||
});
|
||||
|
||||
test('review mode is INFORMATIONAL only', () => {
|
||||
expect(reviewSkill).toContain('INFORMATIONAL');
|
||||
expect(reviewSkill).toContain('MISSING REQUIREMENTS');
|
||||
expect(reviewSkill).toContain('SCOPE CREEP');
|
||||
});
|
||||
|
||||
test('item extraction has 50-item cap', () => {
|
||||
expect(shipSkill).toContain('at most 50 items');
|
||||
});
|
||||
|
||||
test('uses file-level traceability (not commit-level)', () => {
|
||||
expect(shipSkill).toContain('Cite the specific file');
|
||||
expect(shipSkill).not.toContain('commit-level traceability');
|
||||
});
|
||||
});
|
||||
|
||||
// --- {{PLAN_VERIFICATION_EXEC}} resolver tests ---
|
||||
|
||||
describe('PLAN_VERIFICATION_EXEC placeholder', () => {
|
||||
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('ship SKILL.md contains plan verification step', () => {
|
||||
expect(shipSkill).toContain('Step 3.47');
|
||||
expect(shipSkill).toContain('Plan Verification');
|
||||
});
|
||||
|
||||
test('references /qa-only invocation', () => {
|
||||
expect(shipSkill).toContain('qa-only/SKILL.md');
|
||||
expect(shipSkill).toContain('qa-only');
|
||||
});
|
||||
|
||||
test('contains localhost reachability check', () => {
|
||||
expect(shipSkill).toContain('localhost:3000');
|
||||
expect(shipSkill).toContain('NO_SERVER');
|
||||
});
|
||||
|
||||
test('skips gracefully when no verification section', () => {
|
||||
expect(shipSkill).toContain('No verification steps found in plan');
|
||||
});
|
||||
|
||||
test('skips gracefully when no dev server', () => {
|
||||
expect(shipSkill).toContain('No dev server detected');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Coverage gate tests ---
|
||||
|
||||
describe('Coverage gate in ship', () => {
|
||||
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
const reviewSkill = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('ship SKILL.md contains coverage gate with thresholds', () => {
|
||||
expect(shipSkill).toContain('Coverage gate');
|
||||
expect(shipSkill).toContain('>= target');
|
||||
expect(shipSkill).toContain('< minimum');
|
||||
});
|
||||
|
||||
test('ship SKILL.md supports configurable thresholds via CLAUDE.md', () => {
|
||||
expect(shipSkill).toContain('## Test Coverage');
|
||||
expect(shipSkill).toContain('Minimum:');
|
||||
expect(shipSkill).toContain('Target:');
|
||||
});
|
||||
|
||||
test('coverage gate skips on parse failure (not block)', () => {
|
||||
expect(shipSkill).toContain('could not determine percentage — skipping');
|
||||
});
|
||||
|
||||
test('review SKILL.md contains coverage WARNING', () => {
|
||||
expect(reviewSkill).toContain('COVERAGE WARNING');
|
||||
expect(reviewSkill).toContain('Consider writing tests before running /ship');
|
||||
});
|
||||
|
||||
test('review coverage warning is INFORMATIONAL', () => {
|
||||
expect(reviewSkill).toContain('INFORMATIONAL');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Ship metrics logging ---
|
||||
|
||||
describe('Ship metrics logging', () => {
|
||||
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('ship SKILL.md contains metrics persistence step', () => {
|
||||
expect(shipSkill).toContain('Step 8.75');
|
||||
expect(shipSkill).toContain('coverage_pct');
|
||||
expect(shipSkill).toContain('plan_items_total');
|
||||
expect(shipSkill).toContain('plan_items_done');
|
||||
expect(shipSkill).toContain('verification_result');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Plan file discovery shared helper ---
|
||||
|
||||
describe('Plan file discovery shared helper', () => {
|
||||
// The shared helper should appear in ship (via PLAN_COMPLETION_AUDIT_SHIP)
|
||||
// and in review (via PLAN_COMPLETION_AUDIT_REVIEW)
|
||||
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
const reviewSkill = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('plan file discovery appears in both ship and review', () => {
|
||||
expect(shipSkill).toContain('Plan File Discovery');
|
||||
expect(reviewSkill).toContain('Plan File Discovery');
|
||||
});
|
||||
|
||||
test('both include conversation context first', () => {
|
||||
expect(shipSkill).toContain('Conversation context (primary)');
|
||||
expect(reviewSkill).toContain('Conversation context (primary)');
|
||||
});
|
||||
|
||||
test('both include content-based fallback', () => {
|
||||
expect(shipSkill).toContain('Content-based search (fallback)');
|
||||
expect(reviewSkill).toContain('Content-based search (fallback)');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Retro plan completion ---
|
||||
|
||||
describe('Retro plan completion section', () => {
|
||||
const retroSkill = fs.readFileSync(path.join(ROOT, 'retro', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('retro SKILL.md contains plan completion section', () => {
|
||||
expect(retroSkill).toContain('### Plan Completion');
|
||||
expect(retroSkill).toContain('plan_items_total');
|
||||
expect(retroSkill).toContain('Plan Completion This Period');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Plan status footer in preamble ---
|
||||
|
||||
describe('Plan status footer in preamble', () => {
|
||||
@@ -1399,7 +1561,7 @@ describe('telemetry', () => {
|
||||
test('generated SKILL.md contains telemetry opt-in prompt', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('.telemetry-prompted');
|
||||
expect(content).toContain('gstack can share usage data');
|
||||
expect(content).toContain('Community mode shares usage data');
|
||||
expect(content).toContain('gstack-config set telemetry community');
|
||||
expect(content).toContain('gstack-config set telemetry anonymous');
|
||||
expect(content).toContain('gstack-config set telemetry off');
|
||||
|
||||
+15
-15
@@ -9,7 +9,7 @@ import { describe, test, beforeAll, afterAll } from 'bun:test';
|
||||
import type { SkillTestResult } from './session-runner';
|
||||
import { EvalCollector, judgePassed } from './eval-store';
|
||||
import type { EvalTestEntry } from './eval-store';
|
||||
import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from './touchfiles';
|
||||
import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, E2E_TIERS, GLOBAL_TOUCHFILES } from './touchfiles';
|
||||
import { WorktreeManager } from '../../lib/worktree';
|
||||
import type { HarvestResult } from '../../lib/worktree';
|
||||
import { spawnSync } from 'child_process';
|
||||
@@ -32,13 +32,6 @@ export const evalsEnabled = !!process.env.EVALS;
|
||||
// Set EVALS_ALL=1 to force all tests. Set EVALS_BASE to override base branch.
|
||||
export let selectedTests: string[] | null = null; // null = run all
|
||||
|
||||
// EVALS_FAST: skip the 8 slowest tests (all Opus quality tests) for quick feedback
|
||||
const FAST_EXCLUDED_TESTS = [
|
||||
'plan-ceo-review-selective', 'plan-ceo-review', 'retro', 'retro-base-branch',
|
||||
'design-consultation-core', 'design-consultation-existing',
|
||||
'qa-fix-loop', 'design-review-fix',
|
||||
];
|
||||
|
||||
if (evalsEnabled && !process.env.EVALS_ALL) {
|
||||
const baseBranch = process.env.EVALS_BASE
|
||||
|| detectBaseBranch(ROOT)
|
||||
@@ -57,15 +50,22 @@ if (evalsEnabled && !process.env.EVALS_ALL) {
|
||||
// If changedFiles is empty (e.g., on main branch), selectedTests stays null → run all
|
||||
}
|
||||
|
||||
// Apply EVALS_FAST filter after diff-based selection
|
||||
if (evalsEnabled && process.env.EVALS_FAST) {
|
||||
// EVALS_TIER: filter tests by tier after diff-based selection.
|
||||
// 'gate' = gate tests only (CI default — blocks merge)
|
||||
// 'periodic' = periodic tests only (weekly cron / manual)
|
||||
// not set = run all selected tests (local dev default, backward compat)
|
||||
if (evalsEnabled && process.env.EVALS_TIER) {
|
||||
const tier = process.env.EVALS_TIER as 'gate' | 'periodic';
|
||||
const tierTests = Object.entries(E2E_TIERS)
|
||||
.filter(([, t]) => t === tier)
|
||||
.map(([name]) => name);
|
||||
|
||||
if (selectedTests === null) {
|
||||
// Run all minus excluded
|
||||
selectedTests = Object.keys(E2E_TOUCHFILES).filter(t => !FAST_EXCLUDED_TESTS.includes(t));
|
||||
selectedTests = tierTests;
|
||||
} else {
|
||||
selectedTests = selectedTests.filter(t => !FAST_EXCLUDED_TESTS.includes(t));
|
||||
selectedTests = selectedTests.filter(t => tierTests.includes(t));
|
||||
}
|
||||
process.stderr.write(`EVALS_FAST: excluded ${FAST_EXCLUDED_TESTS.length} slow tests, running ${selectedTests.length}\n\n`);
|
||||
process.stderr.write(`EVALS_TIER=${tier}: ${selectedTests.length} tests\n\n`);
|
||||
}
|
||||
|
||||
export const describeE2E = evalsEnabled ? describe : describe.skip;
|
||||
@@ -207,7 +207,7 @@ export async function finalizeEvalCollector(evalCollector: EvalCollector | null)
|
||||
if (evalsEnabled) {
|
||||
const gstackDir = path.join(os.homedir(), '.gstack');
|
||||
fs.mkdirSync(gstackDir, { recursive: true });
|
||||
for (const f of ['.completeness-intro-seen', '.telemetry-prompted']) {
|
||||
for (const f of ['.completeness-intro-seen', '.telemetry-prompted', '.proactive-prompted']) {
|
||||
const p = path.join(gstackDir, f);
|
||||
if (!fs.existsSync(p)) fs.writeFileSync(p, '');
|
||||
}
|
||||
|
||||
+159
-34
@@ -32,25 +32,25 @@ export function matchGlob(file: string, pattern: string): boolean {
|
||||
* Each test lists the file patterns that, if changed, require the test to run.
|
||||
*/
|
||||
export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
// Browse core
|
||||
'browse-basic': ['browse/src/**'],
|
||||
'browse-snapshot': ['browse/src/**'],
|
||||
// Browse core (+ test-server dependency)
|
||||
'browse-basic': ['browse/src/**', 'browse/test/test-server.ts'],
|
||||
'browse-snapshot': ['browse/src/**', 'browse/test/test-server.ts'],
|
||||
|
||||
// SKILL.md setup + preamble (depend on ROOT SKILL.md only)
|
||||
'skillmd-setup-discovery': ['SKILL.md', 'SKILL.md.tmpl'],
|
||||
'skillmd-no-local-binary': ['SKILL.md', 'SKILL.md.tmpl'],
|
||||
'skillmd-outside-git': ['SKILL.md', 'SKILL.md.tmpl'],
|
||||
// SKILL.md setup + preamble (depend on ROOT SKILL.md + gen-skill-docs)
|
||||
'skillmd-setup-discovery': ['SKILL.md', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
'skillmd-no-local-binary': ['SKILL.md', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
'skillmd-outside-git': ['SKILL.md', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
'contributor-mode': ['SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
'session-awareness': ['SKILL.md', 'SKILL.md.tmpl'],
|
||||
'session-awareness': ['SKILL.md', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
// QA
|
||||
'qa-quick': ['qa/**', 'browse/src/**'],
|
||||
'qa-b6-static': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval.html', 'test/fixtures/qa-eval-ground-truth.json'],
|
||||
'qa-b7-spa': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval-spa.html', 'test/fixtures/qa-eval-spa-ground-truth.json'],
|
||||
'qa-b8-checkout': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval-checkout.html', 'test/fixtures/qa-eval-checkout-ground-truth.json'],
|
||||
// QA (+ test-server dependency)
|
||||
'qa-quick': ['qa/**', 'browse/src/**', 'browse/test/test-server.ts'],
|
||||
'qa-b6-static': ['qa/**', 'browse/src/**', 'browse/test/test-server.ts', 'test/helpers/llm-judge.ts', 'browse/test/fixtures/qa-eval.html', 'test/fixtures/qa-eval-ground-truth.json'],
|
||||
'qa-b7-spa': ['qa/**', 'browse/src/**', 'browse/test/test-server.ts', 'test/helpers/llm-judge.ts', 'browse/test/fixtures/qa-eval-spa.html', 'test/fixtures/qa-eval-spa-ground-truth.json'],
|
||||
'qa-b8-checkout': ['qa/**', 'browse/src/**', 'browse/test/test-server.ts', 'test/helpers/llm-judge.ts', 'browse/test/fixtures/qa-eval-checkout.html', 'test/fixtures/qa-eval-checkout-ground-truth.json'],
|
||||
'qa-only-no-fix': ['qa-only/**', 'qa/templates/**'],
|
||||
'qa-fix-loop': ['qa/**', 'browse/src/**'],
|
||||
'qa-fix-loop': ['qa/**', 'browse/src/**', 'browse/test/test-server.ts'],
|
||||
'qa-bootstrap': ['qa/**', 'ship/**'],
|
||||
|
||||
// Review
|
||||
@@ -68,14 +68,18 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'plan-ceo-review-benefits': ['plan-ceo-review/**', 'scripts/gen-skill-docs.ts'],
|
||||
'plan-eng-review': ['plan-eng-review/**'],
|
||||
'plan-eng-review-artifact': ['plan-eng-review/**'],
|
||||
'plan-review-report': ['plan-eng-review/**', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
// Codex offering verification
|
||||
'codex-offered-office-hours': ['office-hours/**', 'scripts/gen-skill-docs.ts'],
|
||||
'codex-offered-ceo-review': ['plan-ceo-review/**', 'scripts/gen-skill-docs.ts'],
|
||||
'codex-offered-design-review': ['plan-design-review/**', 'scripts/gen-skill-docs.ts'],
|
||||
'codex-offered-eng-review': ['plan-eng-review/**', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
// Ship
|
||||
'ship-base-branch': ['ship/**', 'bin/gstack-repo-mode'],
|
||||
'ship-local-workflow': ['ship/**', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
// Setup browser cookies
|
||||
'setup-cookies-detect': ['setup-browser-cookies/**'],
|
||||
|
||||
// Retro
|
||||
'retro': ['retro/**'],
|
||||
'retro-base-branch': ['retro/**'],
|
||||
@@ -94,23 +98,28 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
// Codex (Claude E2E — tests /codex skill via Claude)
|
||||
'codex-review': ['codex/**'],
|
||||
|
||||
// Codex E2E (tests skills via Codex CLI)
|
||||
'codex-discover-skill': ['codex/**', '.agents/skills/**', 'test/helpers/codex-session-runner.ts'],
|
||||
'codex-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'codex/**', 'test/helpers/codex-session-runner.ts'],
|
||||
// Codex E2E (tests skills via Codex CLI + worktree)
|
||||
'codex-discover-skill': ['codex/**', '.agents/skills/**', 'test/helpers/codex-session-runner.ts', 'lib/worktree.ts'],
|
||||
'codex-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'codex/**', 'test/helpers/codex-session-runner.ts', 'lib/worktree.ts'],
|
||||
|
||||
// Gemini E2E (tests skills via Gemini CLI)
|
||||
'gemini-discover-skill': ['.agents/skills/**', 'test/helpers/gemini-session-runner.ts'],
|
||||
'gemini-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'test/helpers/gemini-session-runner.ts'],
|
||||
// Gemini E2E (tests skills via Gemini CLI + worktree)
|
||||
'gemini-discover-skill': ['.agents/skills/**', 'test/helpers/gemini-session-runner.ts', 'lib/worktree.ts'],
|
||||
'gemini-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'test/helpers/gemini-session-runner.ts', 'lib/worktree.ts'],
|
||||
|
||||
|
||||
// Coverage audit (shared fixture) + triage
|
||||
// Coverage audit (shared fixture) + triage + gates
|
||||
'ship-coverage-audit': ['ship/**', 'test/fixtures/coverage-audit-fixture.ts', 'bin/gstack-repo-mode'],
|
||||
'review-coverage-audit': ['review/**', 'test/fixtures/coverage-audit-fixture.ts'],
|
||||
'plan-eng-coverage-audit': ['plan-eng-review/**', 'test/fixtures/coverage-audit-fixture.ts'],
|
||||
'ship-triage': ['ship/**', 'bin/gstack-repo-mode'],
|
||||
|
||||
// Plan completion audit + verification
|
||||
'ship-plan-completion': ['ship/**', 'scripts/gen-skill-docs.ts'],
|
||||
'ship-plan-verification': ['ship/**', 'qa-only/**', 'scripts/gen-skill-docs.ts'],
|
||||
'review-plan-completion': ['review/**', 'scripts/gen-skill-docs.ts'],
|
||||
|
||||
// Design
|
||||
'design-consultation-core': ['design-consultation/**', 'scripts/gen-skill-docs.ts'],
|
||||
'design-consultation-core': ['design-consultation/**', 'scripts/gen-skill-docs.ts', 'test/helpers/llm-judge.ts'],
|
||||
'design-consultation-existing': ['design-consultation/**', 'scripts/gen-skill-docs.ts'],
|
||||
'design-consultation-research': ['design-consultation/**', 'scripts/gen-skill-docs.ts'],
|
||||
'design-consultation-preview': ['design-consultation/**', 'scripts/gen-skill-docs.ts'],
|
||||
@@ -144,6 +153,124 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'journey-visual-qa': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
|
||||
};
|
||||
|
||||
/**
|
||||
* E2E test tiers — 'gate' blocks PRs, 'periodic' runs weekly/on-demand.
|
||||
* Must have exactly the same keys as E2E_TOUCHFILES.
|
||||
*/
|
||||
export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
|
||||
// Browse core — gate (if browse breaks, everything breaks)
|
||||
'browse-basic': 'gate',
|
||||
'browse-snapshot': 'gate',
|
||||
|
||||
// SKILL.md setup — gate (if setup breaks, no skill works)
|
||||
'skillmd-setup-discovery': 'gate',
|
||||
'skillmd-no-local-binary': 'gate',
|
||||
'skillmd-outside-git': 'gate',
|
||||
'contributor-mode': 'gate',
|
||||
'session-awareness': 'gate',
|
||||
|
||||
// QA — gate for functional, periodic for quality/benchmarks
|
||||
'qa-quick': 'gate',
|
||||
'qa-b6-static': 'periodic',
|
||||
'qa-b7-spa': 'periodic',
|
||||
'qa-b8-checkout': 'periodic',
|
||||
'qa-only-no-fix': 'gate', // CRITICAL guardrail: Edit tool forbidden
|
||||
'qa-fix-loop': 'periodic',
|
||||
'qa-bootstrap': 'gate',
|
||||
|
||||
// Review — gate for functional/guardrails, periodic for quality
|
||||
'review-sql-injection': 'gate', // Security guardrail
|
||||
'review-enum-completeness': 'gate',
|
||||
'review-base-branch': 'gate',
|
||||
'review-design-lite': 'periodic', // 4/7 threshold is subjective
|
||||
'review-coverage-audit': 'gate',
|
||||
|
||||
// Office Hours
|
||||
'office-hours-spec-review': 'gate',
|
||||
|
||||
// Plan reviews — gate for cheap functional, periodic for Opus quality
|
||||
'plan-ceo-review': 'periodic',
|
||||
'plan-ceo-review-selective': 'periodic',
|
||||
'plan-ceo-review-benefits': 'gate',
|
||||
'plan-eng-review': 'periodic',
|
||||
'plan-eng-review-artifact': 'periodic',
|
||||
'plan-eng-coverage-audit': 'gate',
|
||||
'plan-review-report': 'gate',
|
||||
|
||||
// Codex offering verification
|
||||
'codex-offered-office-hours': 'gate',
|
||||
'codex-offered-ceo-review': 'gate',
|
||||
'codex-offered-design-review': 'gate',
|
||||
'codex-offered-eng-review': 'gate',
|
||||
|
||||
// Ship — gate (end-to-end ship path)
|
||||
'ship-base-branch': 'gate',
|
||||
'ship-local-workflow': 'gate',
|
||||
'ship-coverage-audit': 'gate',
|
||||
'ship-triage': 'gate',
|
||||
'ship-plan-completion': 'gate',
|
||||
'ship-plan-verification': 'gate',
|
||||
'review-plan-completion': 'gate',
|
||||
|
||||
// Retro — gate for cheap branch detection, periodic for full Opus retro
|
||||
'retro': 'periodic',
|
||||
'retro-base-branch': 'gate',
|
||||
|
||||
// Global discover
|
||||
'global-discover': 'gate',
|
||||
|
||||
// CSO — gate for security guardrails, periodic for quality
|
||||
'cso-full-audit': 'gate', // Hardcoded secrets detection
|
||||
'cso-diff-mode': 'gate',
|
||||
'cso-infra-scope': 'periodic',
|
||||
|
||||
// Document-release — gate (CHANGELOG guardrail)
|
||||
'document-release': 'gate',
|
||||
|
||||
// Codex — periodic (Opus, requires codex CLI)
|
||||
'codex-review': 'periodic',
|
||||
|
||||
// Multi-AI — periodic (require external CLIs)
|
||||
'codex-discover-skill': 'periodic',
|
||||
'codex-review-findings': 'periodic',
|
||||
'gemini-discover-skill': 'periodic',
|
||||
'gemini-review-findings': 'periodic',
|
||||
|
||||
// Design — gate for cheap functional, periodic for Opus/quality
|
||||
'design-consultation-core': 'periodic',
|
||||
'design-consultation-existing': 'periodic',
|
||||
'design-consultation-research': 'gate',
|
||||
'design-consultation-preview': 'gate',
|
||||
'plan-design-review-plan-mode': 'periodic',
|
||||
'plan-design-review-no-ui-scope': 'gate',
|
||||
'design-review-fix': 'periodic',
|
||||
|
||||
// gstack-upgrade
|
||||
'gstack-upgrade-happy-path': 'gate',
|
||||
|
||||
// Deploy skills
|
||||
'land-and-deploy-workflow': 'gate',
|
||||
'canary-workflow': 'gate',
|
||||
'benchmark-workflow': 'gate',
|
||||
'setup-deploy-workflow': 'gate',
|
||||
|
||||
// Autoplan — periodic (not yet implemented)
|
||||
'autoplan-core': 'periodic',
|
||||
|
||||
// Skill routing — periodic (LLM routing is non-deterministic)
|
||||
'journey-ideation': 'periodic',
|
||||
'journey-plan-eng': 'periodic',
|
||||
'journey-think-bigger': 'periodic',
|
||||
'journey-debug': 'periodic',
|
||||
'journey-qa': 'periodic',
|
||||
'journey-code-review': 'periodic',
|
||||
'journey-ship': 'periodic',
|
||||
'journey-docs': 'periodic',
|
||||
'journey-retro': 'periodic',
|
||||
'journey-design-system': 'periodic',
|
||||
'journey-visual-qa': 'periodic',
|
||||
};
|
||||
|
||||
/**
|
||||
* LLM-judge test touchfiles — keyed by test description string.
|
||||
*/
|
||||
@@ -190,17 +317,15 @@ export const LLM_JUDGE_TOUCHFILES: Record<string, string[]> = {
|
||||
|
||||
/**
|
||||
* Changes to any of these files trigger ALL tests (both E2E and LLM-judge).
|
||||
*
|
||||
* Keep this list minimal — only files that genuinely affect every test.
|
||||
* Scoped dependencies (gen-skill-docs, llm-judge, test-server, worktree,
|
||||
* codex/gemini session runners) belong in individual test entries instead.
|
||||
*/
|
||||
export const GLOBAL_TOUCHFILES = [
|
||||
'test/helpers/session-runner.ts',
|
||||
'test/helpers/codex-session-runner.ts',
|
||||
'test/helpers/gemini-session-runner.ts',
|
||||
'test/helpers/eval-store.ts',
|
||||
'test/helpers/llm-judge.ts',
|
||||
'scripts/gen-skill-docs.ts',
|
||||
'test/helpers/touchfiles.ts',
|
||||
'browse/test/test-server.ts',
|
||||
'lib/worktree.ts',
|
||||
'test/helpers/session-runner.ts', // All E2E tests use this runner
|
||||
'test/helpers/eval-store.ts', // All E2E tests store results here
|
||||
'test/helpers/touchfiles.ts', // Self-referential — reclassifying wrong is dangerous
|
||||
];
|
||||
|
||||
// --- Base branch detection ---
|
||||
|
||||
@@ -535,6 +535,199 @@ Write your summary to ${benefitsDir}/benefits-summary.md`,
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
// --- Plan Review Report E2E ---
|
||||
// Verifies that plan-eng-review writes a "## GSTACK REVIEW REPORT" section
|
||||
// to the bottom of the plan file (the living review status footer).
|
||||
|
||||
describeIfSelected('Plan Review Report E2E', ['plan-review-report'], () => {
|
||||
let planDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
planDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-review-report-'));
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
spawnSync(cmd, args, { cwd: planDir, stdio: 'pipe', timeout: 5000 });
|
||||
|
||||
run('git', ['init', '-b', 'main']);
|
||||
run('git', ['config', 'user.email', 'test@test.com']);
|
||||
run('git', ['config', 'user.name', 'Test']);
|
||||
|
||||
fs.writeFileSync(path.join(planDir, 'plan.md'), `# Plan: Add Notifications System
|
||||
|
||||
## Context
|
||||
We're building a real-time notification system for our SaaS app.
|
||||
|
||||
## Changes
|
||||
1. WebSocket server for push notifications
|
||||
2. Notification preferences API
|
||||
3. Email digest fallback for offline users
|
||||
4. PostgreSQL table for notification storage
|
||||
|
||||
## Architecture
|
||||
- WebSocket: Socket.io on Express
|
||||
- Queue: Bull + Redis for email digests
|
||||
- Storage: PostgreSQL notifications table
|
||||
- Frontend: React toast component
|
||||
|
||||
## Open questions
|
||||
- Retry policy for failed WebSocket delivery?
|
||||
- Max notifications stored per user?
|
||||
`);
|
||||
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'add plan']);
|
||||
|
||||
// Copy plan-eng-review skill
|
||||
fs.mkdirSync(path.join(planDir, 'plan-eng-review'), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(ROOT, 'plan-eng-review', 'SKILL.md'),
|
||||
path.join(planDir, 'plan-eng-review', 'SKILL.md'),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(planDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
test('/plan-eng-review writes GSTACK REVIEW REPORT to plan file', async () => {
|
||||
const result = await runSkillTest({
|
||||
prompt: `Read plan-eng-review/SKILL.md for the review workflow.
|
||||
|
||||
Read plan.md — that's the plan to review. This is a standalone plan document, not a codebase — skip any codebase exploration steps.
|
||||
|
||||
Proceed directly to the full review. Skip any AskUserQuestion calls — this is non-interactive.
|
||||
Skip the preamble bash block, lake intro, telemetry, and contributor mode sections.
|
||||
|
||||
CRITICAL REQUIREMENT: plan.md IS the plan file for this review session. After completing your review, you MUST write a "## GSTACK REVIEW REPORT" section to the END of plan.md, exactly as described in the "Plan File Review Report" section of SKILL.md. If gstack-review-read is not available or returns NO_REVIEWS, write the placeholder table with all four review rows (CEO, Codex, Eng, Design). Use the Edit tool to append to plan.md — do NOT overwrite the existing plan content.
|
||||
|
||||
This review report at the bottom of the plan is the MOST IMPORTANT deliverable of this test.`,
|
||||
workingDirectory: planDir,
|
||||
maxTurns: 20,
|
||||
timeout: 360_000,
|
||||
testName: 'plan-review-report',
|
||||
runId,
|
||||
model: 'claude-opus-4-6',
|
||||
});
|
||||
|
||||
logCost('/plan-eng-review report', result);
|
||||
recordE2E(evalCollector, '/plan-review-report', 'Plan Review Report E2E', result, {
|
||||
passed: ['success', 'error_max_turns'].includes(result.exitReason),
|
||||
});
|
||||
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||
|
||||
// Verify the review report was written to the plan file
|
||||
const planContent = fs.readFileSync(path.join(planDir, 'plan.md'), 'utf-8');
|
||||
|
||||
// Original plan content should still be present
|
||||
expect(planContent).toContain('# Plan: Add Notifications System');
|
||||
expect(planContent).toContain('WebSocket');
|
||||
|
||||
// Review report section must exist
|
||||
expect(planContent).toContain('## GSTACK REVIEW REPORT');
|
||||
|
||||
// Report should be at the bottom of the file
|
||||
const reportIndex = planContent.lastIndexOf('## GSTACK REVIEW REPORT');
|
||||
const afterReport = planContent.slice(reportIndex);
|
||||
|
||||
// Should contain the review table with standard rows
|
||||
expect(afterReport).toMatch(/\|\s*Review\s*\|/);
|
||||
expect(afterReport).toContain('CEO Review');
|
||||
expect(afterReport).toContain('Eng Review');
|
||||
expect(afterReport).toContain('Design Review');
|
||||
|
||||
console.log('Plan review report found at bottom of plan.md');
|
||||
}, 420_000);
|
||||
});
|
||||
|
||||
// --- Codex Offering E2E ---
|
||||
// Verifies that Codex is properly offered (with availability check, user prompt,
|
||||
// and fallback) in office-hours, plan-ceo-review, plan-design-review, plan-eng-review.
|
||||
|
||||
describeIfSelected('Codex Offering E2E', [
|
||||
'codex-offered-office-hours', 'codex-offered-ceo-review',
|
||||
'codex-offered-design-review', 'codex-offered-eng-review',
|
||||
], () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-codex-offer-'));
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
spawnSync(cmd, args, { cwd: testDir, stdio: 'pipe', timeout: 5000 });
|
||||
|
||||
run('git', ['init', '-b', 'main']);
|
||||
run('git', ['config', 'user.email', 'test@test.com']);
|
||||
run('git', ['config', 'user.name', 'Test']);
|
||||
fs.writeFileSync(path.join(testDir, 'README.md'), '# Test Project\n');
|
||||
run('git', ['add', '.']);
|
||||
run('git', ['commit', '-m', 'init']);
|
||||
|
||||
// Copy all 4 SKILL.md files
|
||||
for (const skill of ['office-hours', 'plan-ceo-review', 'plan-design-review', 'plan-eng-review']) {
|
||||
fs.mkdirSync(path.join(testDir, skill), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(ROOT, skill, 'SKILL.md'),
|
||||
path.join(testDir, skill, 'SKILL.md'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(testDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
async function checkCodexOffering(skill: string, testName: string, featureName: string) {
|
||||
const result = await runSkillTest({
|
||||
prompt: `Read ${skill}/SKILL.md. Search for ALL sections related to "codex", "outside voice", or "second opinion".
|
||||
|
||||
Summarize the Codex/${featureName} integration — answer these specific questions:
|
||||
1. How is Codex availability checked? (what exact bash command?)
|
||||
2. How is the user prompted? (via AskUserQuestion? what are the options?)
|
||||
3. What happens when Codex is NOT available? (fallback to subagent? skip entirely?)
|
||||
4. Is this step blocking (gates the workflow) or optional (can be skipped)?
|
||||
5. What prompt/context is sent to Codex?
|
||||
|
||||
Write your summary to ${testDir}/${testName}-summary.md`,
|
||||
workingDirectory: testDir,
|
||||
maxTurns: 8,
|
||||
timeout: 120_000,
|
||||
testName,
|
||||
runId,
|
||||
});
|
||||
|
||||
logCost(`/${skill} codex offering`, result);
|
||||
recordE2E(evalCollector, `/${testName}`, 'Codex Offering E2E', result);
|
||||
expect(result.exitReason).toBe('success');
|
||||
|
||||
const summaryPath = path.join(testDir, `${testName}-summary.md`);
|
||||
expect(fs.existsSync(summaryPath)).toBe(true);
|
||||
|
||||
const summary = fs.readFileSync(summaryPath, 'utf-8').toLowerCase();
|
||||
// All skills should have codex availability check
|
||||
expect(summary).toMatch(/which codex/);
|
||||
// All skills should have fallback behavior
|
||||
expect(summary).toMatch(/fallback|subagent|unavailable|not available|skip/);
|
||||
// All skills should show it's optional/non-blocking
|
||||
expect(summary).toMatch(/optional|non.?blocking|skip|not.*required/);
|
||||
|
||||
console.log(`${skill}: Codex offering verified`);
|
||||
}
|
||||
|
||||
testConcurrentIfSelected('codex-offered-office-hours', async () => {
|
||||
await checkCodexOffering('office-hours', 'codex-offered-office-hours', 'second opinion');
|
||||
}, 180_000);
|
||||
|
||||
testConcurrentIfSelected('codex-offered-ceo-review', async () => {
|
||||
await checkCodexOffering('plan-ceo-review', 'codex-offered-ceo-review', 'outside voice');
|
||||
}, 180_000);
|
||||
|
||||
testConcurrentIfSelected('codex-offered-design-review', async () => {
|
||||
await checkCodexOffering('plan-design-review', 'codex-offered-design-review', 'design outside voices');
|
||||
}, 180_000);
|
||||
|
||||
testConcurrentIfSelected('codex-offered-eng-review', async () => {
|
||||
await checkCodexOffering('plan-eng-review', 'codex-offered-eng-review', 'outside voice');
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
// Module-level afterAll — finalize eval collector after all tests complete
|
||||
afterAll(async () => {
|
||||
await finalizeEvalCollector(evalCollector);
|
||||
|
||||
@@ -175,76 +175,30 @@ describeIfSelected('Ship workflow E2E', ['ship-local-workflow'], () => {
|
||||
|
||||
logCost('/ship local workflow', result);
|
||||
|
||||
// Check push succeeded
|
||||
const remoteLog = spawnSync('git', ['log', '--oneline'], { cwd: shipRemoteDir, stdio: 'pipe' });
|
||||
const remoteCommits = remoteLog.stdout.toString().trim().split('\n').length;
|
||||
// Check push succeeded — verify the feature branch exists on the bare remote
|
||||
const branchCheck = spawnSync('git', ['branch', '--list', 'feature/ship-test'], { cwd: shipRemoteDir, stdio: 'pipe' });
|
||||
const branchExists = branchCheck.stdout.toString().trim().length > 0;
|
||||
|
||||
// Check VERSION was bumped
|
||||
// Check VERSION was bumped locally (even if push failed, this shows the LLM did the work)
|
||||
const versionContent = fs.existsSync(path.join(shipWorkDir, 'VERSION'))
|
||||
? fs.readFileSync(path.join(shipWorkDir, 'VERSION'), 'utf-8').trim() : '';
|
||||
const versionBumped = versionContent !== '0.1.0.0';
|
||||
|
||||
recordE2E(evalCollector, '/ship local workflow', 'Ship workflow E2E', result, {
|
||||
passed: remoteCommits > 1 && ['success', 'error_max_turns'].includes(result.exitReason),
|
||||
passed: branchExists && versionBumped && ['success', 'error_max_turns'].includes(result.exitReason),
|
||||
});
|
||||
|
||||
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||
expect(remoteCommits).toBeGreaterThan(1);
|
||||
console.log(`Remote commits: ${remoteCommits}, VERSION: ${versionContent}, bumped: ${versionBumped}`);
|
||||
expect(branchExists).toBe(true);
|
||||
expect(versionBumped).toBe(true);
|
||||
console.log(`Branch pushed: ${branchExists}, VERSION: ${versionContent}, bumped: ${versionBumped}`);
|
||||
}, 150_000);
|
||||
});
|
||||
|
||||
// --- Browser cookie detection smoke test ---
|
||||
|
||||
describeIfSelected('Setup Browser Cookies E2E', ['setup-cookies-detect'], () => {
|
||||
let cookieDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
cookieDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-cookies-'));
|
||||
// Copy skill files
|
||||
fs.mkdirSync(path.join(cookieDir, 'setup-browser-cookies'), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(ROOT, 'setup-browser-cookies', 'SKILL.md'),
|
||||
path.join(cookieDir, 'setup-browser-cookies', 'SKILL.md'),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(cookieDir, { recursive: true, force: true }); } catch {}
|
||||
});
|
||||
|
||||
testConcurrentIfSelected('setup-cookies-detect', async () => {
|
||||
const result = await runSkillTest({
|
||||
prompt: `Read setup-browser-cookies/SKILL.md for the cookie import workflow.
|
||||
|
||||
This is a test environment. List which browsers you can detect on this system by checking for their cookie database files.
|
||||
Write the detected browsers to ${cookieDir}/detected-browsers.md.
|
||||
Do NOT launch the cookie picker UI — just detect and report.`,
|
||||
workingDirectory: cookieDir,
|
||||
maxTurns: 5,
|
||||
timeout: 45_000,
|
||||
testName: 'setup-cookies-detect',
|
||||
runId,
|
||||
});
|
||||
|
||||
logCost('/setup-browser-cookies detect', result);
|
||||
|
||||
const detectPath = path.join(cookieDir, 'detected-browsers.md');
|
||||
const detectExists = fs.existsSync(detectPath);
|
||||
const detectContent = detectExists ? fs.readFileSync(detectPath, 'utf-8') : '';
|
||||
const hasBrowserName = /chrome|arc|brave|edge|comet|safari|firefox/i.test(detectContent);
|
||||
|
||||
recordE2E(evalCollector, '/setup-browser-cookies detect', 'Setup Browser Cookies E2E', result, {
|
||||
passed: detectExists && hasBrowserName && ['success', 'error_max_turns'].includes(result.exitReason),
|
||||
});
|
||||
|
||||
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||
expect(detectExists).toBe(true);
|
||||
if (detectExists) {
|
||||
expect(hasBrowserName).toBe(true);
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
// setup-cookies-detect REMOVED: The cookie-import-browser module has 30+ thorough
|
||||
// unit tests in browse/test/cookie-import-browser.test.ts (decryption, profile
|
||||
// detection, error handling, path traversal). The E2E just tested LLM instruction-
|
||||
// following ("write a file saying no browsers") on a CI box with no browsers.
|
||||
|
||||
// --- gstack-upgrade E2E ---
|
||||
|
||||
|
||||
@@ -73,11 +73,14 @@ describeIfSelected('LLM-as-judge quality evals', [
|
||||
const scores = await judge('command reference table', section);
|
||||
console.log('Command reference scores:', JSON.stringify(scores, null, 2));
|
||||
|
||||
// Completeness threshold is 3 (not 4) — the command reference table is
|
||||
// intentionally terse (quick-reference format). The judge consistently scores
|
||||
// completeness=3 because detailed argument docs live in per-command sections.
|
||||
evalCollector?.addTest({
|
||||
name: 'command reference table',
|
||||
suite: 'LLM-as-judge quality evals',
|
||||
tier: 'llm-judge',
|
||||
passed: scores.clarity >= 4 && scores.completeness >= 4 && scores.actionability >= 4,
|
||||
passed: scores.clarity >= 4 && scores.completeness >= 3 && scores.actionability >= 4,
|
||||
duration_ms: Date.now() - t0,
|
||||
cost_usd: 0.02,
|
||||
judge_scores: { clarity: scores.clarity, completeness: scores.completeness, actionability: scores.actionability },
|
||||
@@ -85,7 +88,7 @@ describeIfSelected('LLM-as-judge quality evals', [
|
||||
});
|
||||
|
||||
expect(scores.clarity).toBeGreaterThanOrEqual(4);
|
||||
expect(scores.completeness).toBeGreaterThanOrEqual(4);
|
||||
expect(scores.completeness).toBeGreaterThanOrEqual(3);
|
||||
expect(scores.actionability).toBeGreaterThanOrEqual(4);
|
||||
}, 30_000);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { runSkillTest } from './helpers/session-runner';
|
||||
import type { SkillTestResult } from './helpers/session-runner';
|
||||
import { EvalCollector } from './helpers/eval-store';
|
||||
import type { EvalTestEntry } from './helpers/eval-store';
|
||||
import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from './helpers/touchfiles';
|
||||
import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, E2E_TIERS, GLOBAL_TOUCHFILES } from './helpers/touchfiles';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -42,6 +42,21 @@ if (evalsEnabled && !process.env.EVALS_ALL) {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply EVALS_TIER filter (same logic as e2e-helpers.ts)
|
||||
if (evalsEnabled && process.env.EVALS_TIER) {
|
||||
const tier = process.env.EVALS_TIER as 'gate' | 'periodic';
|
||||
const tierTests = Object.entries(E2E_TIERS)
|
||||
.filter(([, t]) => t === tier)
|
||||
.map(([name]) => name);
|
||||
|
||||
if (selectedTests === null) {
|
||||
selectedTests = tierTests;
|
||||
} else {
|
||||
selectedTests = selectedTests.filter(t => tierTests.includes(t));
|
||||
}
|
||||
process.stderr.write(`Routing EVALS_TIER=${tier}: ${selectedTests.length} tests\n\n`);
|
||||
}
|
||||
|
||||
// --- Helper functions ---
|
||||
|
||||
/** Copy all SKILL.md files for auto-discovery.
|
||||
@@ -140,6 +155,15 @@ function recordRouting(name: string, result: SkillTestResult, expectedSkill: str
|
||||
});
|
||||
}
|
||||
|
||||
// Skip individual tests based on selectedTests (diff + tier filtering)
|
||||
const testIfSelected = (name: string, fn: () => Promise<void>, timeout?: number) => {
|
||||
if (selectedTests !== null && !selectedTests.includes(name)) {
|
||||
test.skip(name, () => {});
|
||||
} else {
|
||||
test.concurrent(name, fn, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
describeE2E('Skill Routing E2E — Developer Journey', () => {
|
||||
@@ -147,7 +171,7 @@ describeE2E('Skill Routing E2E — Developer Journey', () => {
|
||||
evalCollector?.finalize();
|
||||
});
|
||||
|
||||
test.concurrent('journey-ideation', async () => {
|
||||
testIfSelected('journey-ideation', async () => {
|
||||
const tmpDir = createRoutingWorkDir('ideation');
|
||||
try {
|
||||
|
||||
@@ -176,7 +200,7 @@ describeE2E('Skill Routing E2E — Developer Journey', () => {
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-plan-eng', async () => {
|
||||
testIfSelected('journey-plan-eng', async () => {
|
||||
const tmpDir = createRoutingWorkDir('plan-eng');
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, 'plan.md'), `# Waitlist App Architecture
|
||||
@@ -226,7 +250,7 @@ describeE2E('Skill Routing E2E — Developer Journey', () => {
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-think-bigger', async () => {
|
||||
testIfSelected('journey-think-bigger', async () => {
|
||||
const tmpDir = createRoutingWorkDir('think-bigger');
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, 'plan.md'), `# Waitlist App Architecture
|
||||
@@ -277,7 +301,7 @@ describeE2E('Skill Routing E2E — Developer Journey', () => {
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
test.concurrent('journey-debug', async () => {
|
||||
testIfSelected('journey-debug', async () => {
|
||||
const tmpDir = createRoutingWorkDir('debug');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
@@ -335,7 +359,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-qa', async () => {
|
||||
testIfSelected('journey-qa', async () => {
|
||||
const tmpDir = createRoutingWorkDir('qa');
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'waitlist-app', scripts: { dev: 'next dev' } }, null, 2));
|
||||
@@ -371,7 +395,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-code-review', async () => {
|
||||
testIfSelected('journey-code-review', async () => {
|
||||
const tmpDir = createRoutingWorkDir('code-review');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
@@ -411,7 +435,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-ship', async () => {
|
||||
testIfSelected('journey-ship', async () => {
|
||||
const tmpDir = createRoutingWorkDir('ship');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
@@ -450,7 +474,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-docs', async () => {
|
||||
testIfSelected('journey-docs', async () => {
|
||||
const tmpDir = createRoutingWorkDir('docs');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
@@ -487,7 +511,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-retro', async () => {
|
||||
testIfSelected('journey-retro', async () => {
|
||||
const tmpDir = createRoutingWorkDir('retro');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
@@ -530,7 +554,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-design-system', async () => {
|
||||
testIfSelected('journey-design-system', async () => {
|
||||
const tmpDir = createRoutingWorkDir('design-system');
|
||||
try {
|
||||
|
||||
@@ -559,7 +583,7 @@ export default app;
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.concurrent('journey-visual-qa', async () => {
|
||||
testIfSelected('journey-visual-qa', async () => {
|
||||
const tmpDir = createRoutingWorkDir('visual-qa');
|
||||
try {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
|
||||
+22
-5
@@ -78,8 +78,9 @@ describe('gstack-telemetry-log', () => {
|
||||
|
||||
const events = parseJsonl();
|
||||
expect(events).toHaveLength(1);
|
||||
// install_fingerprint should be a UUID (lowercase)
|
||||
expect(events[0].install_fingerprint).toMatch(/^[a-f0-9-]{36}$/);
|
||||
// install_fingerprint should be a UUID v4 (or hex fallback)
|
||||
expect(events[0].install_fingerprint).toMatch(/^[a-f0-9-]{32,36}$/);
|
||||
|
||||
});
|
||||
|
||||
test('includes install_fingerprint for anonymous tier (not null — UUID is not PII)', () => {
|
||||
@@ -306,16 +307,32 @@ describe('gstack-analytics', () => {
|
||||
});
|
||||
|
||||
describe('gstack-telemetry-sync', () => {
|
||||
test('exits silently with no endpoint configured', () => {
|
||||
// Default: GSTACK_TELEMETRY_ENDPOINT is not set → exit 0
|
||||
test('exits silently with no Supabase URL configured', () => {
|
||||
// Default: GSTACK_SUPABASE_URL is not set → exit 0
|
||||
const result = run(`${BIN}/gstack-telemetry-sync`);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('exits silently with no JSONL file', () => {
|
||||
const result = run(`${BIN}/gstack-telemetry-sync`, { GSTACK_TELEMETRY_ENDPOINT: 'http://localhost:9999' });
|
||||
const result = run(`${BIN}/gstack-telemetry-sync`, { GSTACK_SUPABASE_URL: 'http://localhost:9999' });
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('does not rename JSONL field names (edge function expects raw names)', () => {
|
||||
setConfig('telemetry', 'anonymous');
|
||||
run(`${BIN}/gstack-telemetry-log --skill qa --duration 60 --outcome success --session-id raw-fields-1`);
|
||||
|
||||
const events = parseJsonl();
|
||||
expect(events).toHaveLength(1);
|
||||
// Edge function expects these raw field names, NOT Postgres column names
|
||||
expect(events[0]).toHaveProperty('v');
|
||||
expect(events[0]).toHaveProperty('ts');
|
||||
expect(events[0]).toHaveProperty('sessions');
|
||||
// Should NOT have Postgres column names
|
||||
expect(events[0]).not.toHaveProperty('schema_version');
|
||||
expect(events[0]).not.toHaveProperty('event_timestamp');
|
||||
expect(events[0]).not.toHaveProperty('concurrent_sessions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-community-dashboard', () => {
|
||||
|
||||
+47
-6
@@ -13,6 +13,7 @@ import {
|
||||
selectTests,
|
||||
detectBaseBranch,
|
||||
E2E_TOUCHFILES,
|
||||
E2E_TIERS,
|
||||
LLM_JUDGE_TOUCHFILES,
|
||||
GLOBAL_TOUCHFILES,
|
||||
} from './helpers/touchfiles';
|
||||
@@ -80,8 +81,9 @@ describe('selectTests', () => {
|
||||
expect(result.selected).toContain('plan-ceo-review-selective');
|
||||
expect(result.selected).toContain('plan-ceo-review-benefits');
|
||||
expect(result.selected).toContain('autoplan-core');
|
||||
expect(result.selected.length).toBe(4);
|
||||
expect(result.skipped.length).toBe(Object.keys(E2E_TOUCHFILES).length - 4);
|
||||
expect(result.selected).toContain('codex-offered-ceo-review');
|
||||
expect(result.selected.length).toBe(5);
|
||||
expect(result.skipped.length).toBe(Object.keys(E2E_TOUCHFILES).length - 5);
|
||||
});
|
||||
|
||||
test('global touchfile triggers ALL tests', () => {
|
||||
@@ -91,10 +93,19 @@ describe('selectTests', () => {
|
||||
expect(result.reason).toContain('global');
|
||||
});
|
||||
|
||||
test('gen-skill-docs.ts is a global touchfile', () => {
|
||||
test('gen-skill-docs.ts is a scoped touchfile, not global', () => {
|
||||
const result = selectTests(['scripts/gen-skill-docs.ts'], E2E_TOUCHFILES);
|
||||
expect(result.selected.length).toBe(Object.keys(E2E_TOUCHFILES).length);
|
||||
expect(result.reason).toContain('global');
|
||||
// Should select tests that list gen-skill-docs.ts in their touchfiles, not ALL tests
|
||||
expect(result.selected.length).toBeGreaterThan(0);
|
||||
expect(result.selected.length).toBeLessThan(Object.keys(E2E_TOUCHFILES).length);
|
||||
expect(result.reason).toBe('diff');
|
||||
// Should include tests that depend on gen-skill-docs.ts
|
||||
expect(result.selected).toContain('skillmd-setup-discovery');
|
||||
expect(result.selected).toContain('contributor-mode');
|
||||
expect(result.selected).toContain('journey-ideation');
|
||||
// Should NOT include tests that don't depend on it
|
||||
expect(result.selected).not.toContain('retro');
|
||||
expect(result.selected).not.toContain('cso-full-audit');
|
||||
});
|
||||
|
||||
test('unrelated file selects nothing', () => {
|
||||
@@ -143,7 +154,7 @@ describe('selectTests', () => {
|
||||
});
|
||||
|
||||
test('global touchfiles work for LLM-judge tests too', () => {
|
||||
const result = selectTests(['scripts/gen-skill-docs.ts'], LLM_JUDGE_TOUCHFILES);
|
||||
const result = selectTests(['test/helpers/session-runner.ts'], LLM_JUDGE_TOUCHFILES);
|
||||
expect(result.selected.length).toBe(Object.keys(LLM_JUDGE_TOUCHFILES).length);
|
||||
});
|
||||
});
|
||||
@@ -233,6 +244,36 @@ describe('TOUCHFILES completeness', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('E2E_TIERS covers exactly the same tests as E2E_TOUCHFILES', () => {
|
||||
const touchfileKeys = new Set(Object.keys(E2E_TOUCHFILES));
|
||||
const tierKeys = new Set(Object.keys(E2E_TIERS));
|
||||
|
||||
const missingFromTiers = [...touchfileKeys].filter(k => !tierKeys.has(k));
|
||||
const extraInTiers = [...tierKeys].filter(k => !touchfileKeys.has(k));
|
||||
|
||||
if (missingFromTiers.length > 0) {
|
||||
throw new Error(
|
||||
`E2E tests missing TIER entries: ${missingFromTiers.join(', ')}\n` +
|
||||
`Add these to E2E_TIERS in test/helpers/touchfiles.ts`,
|
||||
);
|
||||
}
|
||||
if (extraInTiers.length > 0) {
|
||||
throw new Error(
|
||||
`E2E_TIERS has extra entries not in E2E_TOUCHFILES: ${extraInTiers.join(', ')}\n` +
|
||||
`Remove these from E2E_TIERS or add to E2E_TOUCHFILES`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('E2E_TIERS only contains valid tier values', () => {
|
||||
const validTiers = ['gate', 'periodic'];
|
||||
for (const [name, tier] of Object.entries(E2E_TIERS)) {
|
||||
if (!validTiers.includes(tier)) {
|
||||
throw new Error(`E2E_TIERS['${name}'] has invalid tier '${tier}'. Valid: ${validTiers.join(', ')}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('every LLM-judge test has a TOUCHFILES entry', () => {
|
||||
const llmContent = fs.readFileSync(
|
||||
path.join(ROOT, 'test', 'skill-llm-eval.test.ts'),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: unfreeze
|
||||
version: 0.1.0
|
||||
description: |
|
||||
MANUAL TRIGGER ONLY: invoke only when user types /unfreeze.
|
||||
Clear the freeze boundary set by /freeze, allowing edits to all directories
|
||||
again. Use when you want to widen edit scope without ending the session.
|
||||
Use when asked to "unfreeze", "unlock edits", "remove freeze", or
|
||||
|
||||
Reference in New Issue
Block a user