mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
* fix(benchmark): reconcile the collection, comparison, budget and trend phases with the vet-the-number section
The perf-collection judge (run 40558235014, both attempts, six samples) named
the same contradictions: Phase 3 offered an optional 3-run median while the
mandatory vet section demands 5 alternating runs per side and a stored
baseline cannot be interleaved; the budget example read '4/6 passing' against
a rule that counts WARNING as below budget; the trend table showed Grade and
Bundle columns no stored file defined; Phase 9 named two files with no
contents. Phase 3 now runs 5 per page with median and range and names
--quick as the one-run ballpark; the baseline schema carries runs and
range_ms; Phase 5 says how the seven questions apply against a stored
baseline (compare the delta with both spreads, name the limiter from Phase 6,
inconclusive when either side is one run); the grade example counts 3 PASS,
1 WARNING, 2 FAIL; Phase 8 reads the benchmark and baseline JSON files by
timestamp; Phase 9 defines the JSON as the Phase 4 object plus grade, verdict
and per-metric status. Nearby prose tightened so the skeleton stays under its
31,300-byte cap (31,290). Judge after the repair: clarity, completeness and
actionability 4/4/4 on all three samples, twice.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: /benchmark phases reconciled with the vet-the-number section (GSTA-128)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: /benchmark runs five times per page in docs/skills.md
Step 14.5 of /ship on #3520: the /benchmark section said 'Multiple runs averaged' and
'3 runs each'; it now matches Phase 3 (five runs per page, median with range, --quick a
single-run ballpark). GSTA-128.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* RESTAMP: 1.91.282.0 -> 1.91.283.0 (stamped; queue_mode=stamp-at-merge base=origin/main@9ccb5233f309 policy=base origin/main@9ccb5233f309:.gstack/ship-policy.json)
ship, next in queue at 1.91.283.0
STAMP_VERSION: 1.91.283.0
claims: ignored (stamp-at-merge)
stage version: done (wrote 1.91.283.0)
stage release_tool: done (bun scripts/gen-agents-digest.ts)
stage changelog: done (Unreleased -> 1.91.283.0)
stage stamp_paths: skipped (no tentative mention newer than the fork version 1.91.281.0 and none of base 1.91.282.0 on a line this branch added)
stage mirror: skipped (no mirror in policy)
stage apply: done (4 file(s))
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 695e20bd5..15162a5d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## [Unreleased]
+## [1.91.283.0] - 2026-10-27
**/benchmark's collection, comparison, budget and trend phases now agree with its vet-the-number section.** The 2026-10-27 flaky pass (GSTA-128) read the `benchmark/SKILL.md perf collection` judge's six samples from run 40558235014: every sample named the same contradictions, so the prose was repaired and no threshold moved.
diff --git a/VERSION b/VERSION
index 269b6e89c..0b68a0506 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.91.282.0
+1.91.283.0
diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md
index 583844b7e..894898db8 100644
--- a/agents-digest/gstack-AGENTS.md
+++ b/agents-digest/gstack-AGENTS.md
@@ -1,4 +1,4 @@
-# gstack digest v1.91.282.0 — regenerate/re-copy after upgrading gstack
+# gstack digest v1.91.283.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
diff --git a/node_modules b/node_modules
new file mode 120000
index 000000000..858ae29ce
--- /dev/null
+++ b/node_modules
@@ -0,0 +1 @@
+/workspace/gstack/node_modules
\ No newline at end of file
diff --git a/package.json b/package.json
index 66ec462ee..da0b87294 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gstack",
- "version": "1.91.282",
+ "version": "1.91.283",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
FILES: CHANGELOG.md VERSION agents-digest/gstack-AGENTS.md package.json
MERGE_CONDITION: sha=aa8c07b76935d91ec548fb5a32fd429392f88460
next: NEW_VERSION=1.91.283.0; commit and push only the stamp diff above (FILES), with no edit of your own: the train's verify accepts the CHANGELOG only as the base's plus this PR's entry re-headed; merge with the expected head aa8c07b769 and recompute on a stale queue.
GSTACK_RESULT: skill=restamp status=complete run=/workspace/gstack/.gstack/tmp/restamp-journal.json
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
258 lines
13 KiB
Cheetah
258 lines
13 KiB
Cheetah
---
|
||
name: benchmark
|
||
preamble-tier: 1
|
||
version: 1.0.0
|
||
description: |
|
||
Web performance regression detection. Establishes
|
||
baselines for page load times, Core Web Vitals, and resource sizes.
|
||
Compares before/after on every PR. Tracks performance trends over time.
|
||
Use when: "web performance benchmark", "page speed", "lighthouse", "web vitals",
|
||
"bundle size", "load time". Not for AI model, agent eval or business
|
||
benchmarks; use /benchmark-models for cross-model skill comparisons. (gstack)
|
||
voice-triggers:
|
||
- "speed test"
|
||
- "check performance"
|
||
triggers:
|
||
- web performance benchmark
|
||
- check page speed
|
||
- detect page performance regression
|
||
allowed-tools:
|
||
- Bash
|
||
- Read
|
||
- Write
|
||
- Glob
|
||
- AskUserQuestion
|
||
---
|
||
|
||
{{PREAMBLE}}
|
||
|
||
{{ASIDE_SETUP}}
|
||
|
||
{{BROWSE_FALLBACK}}
|
||
|
||
# /benchmark — Performance Regression Detection
|
||
|
||
You are a **Performance Engineer**. Performance dies by a thousand paper cuts. Each PR adds 50ms here, 20KB there, until the app takes 8 seconds to load and nobody knows when it got slow.
|
||
|
||
You measure, baseline, compare and alert, reading `performance.getEntries()` from the live page in the Aside browser: real numbers, not estimates.
|
||
|
||
## User-invocable
|
||
When the user types `/benchmark`, run this skill.
|
||
|
||
## Arguments
|
||
- `/benchmark <url>` — full performance audit with baseline comparison
|
||
- `/benchmark <url> --baseline` — capture baseline (run before making changes)
|
||
- `/benchmark <url> --quick` — one-run ballpark, no baseline
|
||
- `/benchmark <url> --pages /,/dashboard,/api/health` — specify pages
|
||
- `/benchmark --diff` — benchmark only pages affected by current branch
|
||
- `/benchmark --trend` — show performance trends from historical data
|
||
|
||
---
|
||
{{SECTION_INDEX:benchmark}}
|
||
---
|
||
|
||
## Instructions
|
||
|
||
### Phase 1: Setup
|
||
|
||
```bash
|
||
SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG 2>/dev/null) || SLUG=unknown
|
||
mkdir -p .gstack/benchmark-reports
|
||
mkdir -p .gstack/benchmark-reports/baselines
|
||
git update-index --refresh >/dev/null 2>&1 || true
|
||
```
|
||
|
||
Run the refresh before timing anything that spawns git (a dev server's version stamp,
|
||
a build step, `--diff` discovery): in a benchmark or bisect worktree a stale index makes
|
||
the first git call pay a full refresh inside the timed region.
|
||
|
||
### Phase 2: Page Discovery
|
||
|
||
Use `--pages` when given. Otherwise open the URL, collect its same-origin `a[href]`
|
||
links (skip logout/delete-style URLs), and propose the homepage plus up to 5 main
|
||
navigation targets; confirm the list with the user before measuring.
|
||
|
||
If `--diff` mode:
|
||
```bash
|
||
git diff $(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || echo main)...HEAD --name-only
|
||
```
|
||
|
||
### Phase 3: Performance Data Collection
|
||
|
||
For each page, ONE `aside repl` script opens the page and prints every metric as a labelled line. Tabs die when the script ends, so nothing carries over between pages — each page gets its own run:
|
||
|
||
```bash
|
||
aside repl '
|
||
const pg = await openTab("<page-url>");
|
||
await pg.waitForLoadState("load");
|
||
console.log("NAV=" + await pg.evaluate(() => JSON.stringify(performance.getEntriesByType("navigation")[0]))); // stringify IN the page: PerformanceEntry fields are getters and serialize to {} across the bridge
|
||
console.log("PAINT=" + await pg.evaluate(() => JSON.stringify(performance.getEntriesByType("paint").map(p => ({ name: p.name, start: Math.round(p.startTime) })))));
|
||
console.log("LCP=" + await pg.evaluate(() => new Promise(res => { const po = new PerformanceObserver(l => { const e = l.getEntries().pop(); if (e) res(Math.round(e.startTime)); }); po.observe({ type: "largest-contentful-paint", buffered: true }); setTimeout(() => res(null), 3000); })));
|
||
console.log("RESOURCES=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").map(r => ({ name: r.name.split("/").pop().split("?")[0], type: r.initiatorType, size: r.transferSize, duration: Math.round(r.duration) })).sort((a, b) => b.duration - a.duration).slice(0, 15))));
|
||
console.log("SCRIPTS=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").filter(r => r.initiatorType === "script").map(r => ({ name: r.name.split("/").pop().split("?")[0], size: r.transferSize })))));
|
||
console.log("CSS=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").filter(r => r.initiatorType === "css").map(r => ({ name: r.name.split("/").pop().split("?")[0], size: r.transferSize })))));
|
||
console.log("SUMMARY=" + JSON.stringify(await pg.evaluate(() => { const r = performance.getEntriesByType("resource"); return { total_requests: r.length, total_transfer: r.reduce((s, e) => s + (e.transferSize || 0), 0), by_type: Object.entries(r.reduce((a, e) => { a[e.initiatorType] = (a[e.initiatorType] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]) }; })));
|
||
await closeTab(pg); console.log("GSTACK_STEP_OK");
|
||
'
|
||
```
|
||
|
||
`NAV=` is the navigation timing entry, `PAINT=` the paint entries (FCP lives here), `LCP=` the largest-contentful-paint start time, `RESOURCES=` the 15 slowest resources, `SCRIPTS=` / `CSS=` the bundle inventory, `SUMMARY=` request count, total transfer, and requests by type. A missing `GSTACK_STEP_OK` or a line starting with `[error` means the page did not load — record it as a failure, not a slow page.
|
||
|
||
Extract key metrics from the labelled lines (`NAV=` unless stated otherwise):
|
||
- **TTFB** (Time to First Byte): `responseStart - requestStart`
|
||
- **FCP** (First Contentful Paint): the `first-contentful-paint` entry in `PAINT=`
|
||
- **LCP** (Largest Contentful Paint): the `LCP=` line (`null` if the page emitted no LCP entry — record it as missing, not 0)
|
||
- **DOM Interactive**: `domInteractive - startTime`
|
||
- **DOM Complete**: `domComplete - startTime`
|
||
- **Full Load**: `loadEventEnd - startTime`
|
||
|
||
Run the script 5 times per page; later phases use each metric's median and report the run count and range. `--quick` is the one-run ballpark, with absolute numbers and budgets and no comparison verdict.
|
||
|
||
### Phase 4: Baseline Capture (--baseline mode)
|
||
|
||
Save metrics to baseline file:
|
||
|
||
```json
|
||
{
|
||
"url": "<url>",
|
||
"timestamp": "<ISO>",
|
||
"branch": "<branch>",
|
||
"runs": 5,
|
||
"pages": {
|
||
"/": {
|
||
"ttfb_ms": 120,
|
||
"fcp_ms": 450,
|
||
"lcp_ms": 800,
|
||
"dom_interactive_ms": 600,
|
||
"dom_complete_ms": 1200,
|
||
"full_load_ms": 1400,
|
||
"total_requests": 42,
|
||
"total_transfer_bytes": 1250000,
|
||
"js_bundle_bytes": 450000,
|
||
"css_bundle_bytes": 85000,
|
||
"range_ms": {"lcp": [760, 840], "full_load": [1350, 1480]},
|
||
"largest_resources": [
|
||
{"name": "main.js", "size": 320000, "duration": 180},
|
||
{"name": "vendor.js", "size": 130000, "duration": 90}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Each `_ms` value is the median of `runs` runs; `range_ms` holds each timing metric's min and max (two shown). Write to `.gstack/benchmark-reports/baselines/baseline.json`.
|
||
Also retain an immutable `{UTC-timestamp}-baseline.json` beside it for trends. Without `--baseline`, never overwrite it; Phase 9 saves the run.
|
||
|
||
### Phase 5: Comparison
|
||
|
||
{{SECTION:vet-the-number}}
|
||
|
||
If baseline exists, compare current metrics against it. The baseline ran earlier, so question 5's alternation is unavailable; instead compare each delta with both sides' `range_ms` and name the limiter from the Phase 6 resources. A baseline without `runs` counts as one run; one run on either side, or an unnamed limiter, makes the verdict inconclusive (say which) and recommends a fresh `--baseline`.
|
||
Without a baseline, report absolute numbers and budgets only, mark the comparison unavailable and recommend `--baseline`. Missing metrics remain N/A. A zero baseline makes percentage change N/A; absolute timing thresholds still apply.
|
||
|
||
```
|
||
PERFORMANCE REPORT — [url]
|
||
══════════════════════════
|
||
VERDICT: slower — LCP 800ms → 1600ms at the median, outside both ranges, bound by the hero image decode
|
||
Branch: [current-branch] vs baseline ([baseline-branch])
|
||
Runs: 5 per side (medians; ranges in the JSON report)
|
||
|
||
Page: /
|
||
Metric Baseline Current Delta Status
|
||
TTFB 120ms 135ms +15ms OK
|
||
FCP 450ms 480ms +30ms OK
|
||
LCP 800ms 1600ms +800ms REGRESSION
|
||
DOM Interactive 600ms 650ms +50ms OK
|
||
DOM Complete 1200ms 1350ms +150ms OK
|
||
Full Load 1400ms 2100ms +700ms REGRESSION
|
||
Total Requests 42 58 +16 WARNING
|
||
Transfer Size 1.2MB 1.8MB +0.6MB REGRESSION
|
||
JS Bundle 450KB 720KB +270KB REGRESSION
|
||
CSS Bundle 85KB 88KB +3KB OK
|
||
|
||
REGRESSIONS DETECTED: 4
|
||
[1] LCP 800ms → 1600ms — a large new image or blocking resource
|
||
[2] Transfer 1.2MB → 1.8MB — new JS bundles
|
||
[3] JS 450KB → 720KB — new dependency or missing tree-shaking
|
||
[4] Full load 1400ms → 2100ms — see Phase 6
|
||
```
|
||
|
||
**Regression thresholds:**
|
||
- Timing metrics: >50% increase OR >500ms absolute increase = REGRESSION
|
||
- Timing metrics: >20% increase = WARNING
|
||
- Bundle size and total transfer: >25% increase = REGRESSION
|
||
- Bundle size and total transfer: >10% increase = WARNING
|
||
- Request count: >30% increase = WARNING (no separate regression threshold)
|
||
Apply REGRESSION before WARNING; otherwise OK. A delta smaller than either side's spread (`range_ms` max − min) is OK (no measurable difference) whatever threshold it crosses. Negative deltas are improvements.
|
||
|
||
### Phase 6: Slowest Resources
|
||
|
||
```
|
||
TOP 10 SLOWEST RESOURCES
|
||
═════════════════════════
|
||
# Resource Type Size Duration
|
||
1 vendor.chunk.js script 320KB 480ms
|
||
2 main.js script 250KB 320ms
|
||
3 hero-image.webp img 180KB 280ms
|
||
4 analytics.js script 45KB 250ms ← third-party
|
||
5 fonts/inter-var.woff2 font 95KB 180ms
|
||
...
|
||
|
||
RECOMMENDATIONS:
|
||
- vendor.chunk.js: code-split — 320KB on initial load
|
||
- analytics.js: load async/defer — blocks rendering for 250ms
|
||
- hero-image.webp: add width/height (CLS), lazy-load
|
||
```
|
||
|
||
Third-party = any host but the page's origin. Each recommendation names a resource here or in a Phase 5 regression, the metric it moves and one remedy (split, defer, compress, lazy-load, cache).
|
||
|
||
### Phase 7: Performance Budget
|
||
|
||
Check against industry budgets:
|
||
For each available metric, FAIL at or above the budget, WARNING from 90% to below 100%, otherwise PASS. Missing metrics are N/A and excluded. Grade by the proportion below budget (PASS or WARNING): A = all, B = at least two-thirds, C = at least half, D = fewer than half, N/A = none measured.
|
||
|
||
```
|
||
PERFORMANCE BUDGET CHECK
|
||
════════════════════════
|
||
Metric Budget Actual Status
|
||
FCP < 1.8s 0.48s PASS
|
||
LCP < 2.5s 1.6s PASS
|
||
Total JS < 500KB 720KB FAIL
|
||
Total CSS < 100KB 88KB PASS
|
||
Total Transfer < 2MB 1.8MB WARNING (90%)
|
||
HTTP Requests < 50 58 FAIL
|
||
|
||
Grade: B (4/6 below budget: 3 PASS, 1 WARNING, 2 FAIL)
|
||
```
|
||
|
||
### Phase 8: Trend Analysis (--trend mode)
|
||
|
||
Load every `*-benchmark.json` and `baselines/*-baseline.json` under `.gstack/benchmark-reports/`, order by `timestamp`, show the last 5 per page: FCP and LCP medians, Bundle = `js_bundle_bytes`, Requests = `total_requests`, Grade = the file's `grade` (`-` for baseline files).
|
||
|
||
```
|
||
PERFORMANCE TRENDS (last 5 benchmarks)
|
||
══════════════════════════════════════
|
||
Date FCP LCP Bundle Requests Grade
|
||
2026-03-10 420ms 750ms 380KB 38 A
|
||
2026-03-12 440ms 780ms 410KB 40 A
|
||
2026-03-14 450ms 800ms 450KB 42 A
|
||
2026-03-16 460ms 850ms 520KB 48 B
|
||
2026-03-18 480ms 1600ms 720KB 58 B
|
||
|
||
TREND: degrading. LCP doubled in 8 days; JS bundle +50KB/week.
|
||
```
|
||
|
||
### Phase 9: Save Report
|
||
|
||
Write `.gstack/benchmark-reports/{UTC-timestamp}-benchmark.md` (the Phase 5–8 output) and `.json` beside it: the Phase 4 object plus, per page, `grade` (Phase 7) and, with a baseline, `verdict` and each metric's `status` (Phase 5).
|
||
|
||
## Important Rules
|
||
|
||
- **Measure, don't guess.** Use actual performance.getEntries() data, not estimates.
|
||
- **Vet every number before reporting it.** The seven questions in the section above decide whether a delta is a result, noise or an unmeasured limit; name the limiter or call the verdict inconclusive.
|
||
- **Baseline is essential.** Without one you report absolute numbers and detect no regressions. Encourage one.
|
||
- **Relative thresholds, not absolute.** 2000ms is fine for a complex dashboard, terrible for a landing page. Compare against YOUR baseline.
|
||
- **Third-party scripts are context.** Flag them; the user can't fix a slow Google Analytics. Recommend first-party fixes.
|
||
- **Bundle size is the leading indicator.** Load time varies with the network; bundle size is deterministic. Track it.
|
||
- **Read-only.** Produce the report. Don't modify code unless explicitly asked.
|