opengstack 0.13.10 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +0 -16
- package/{skills/benchmark/SKILL.md → commands/benchmark.md} +0 -17
- package/{skills/browse/SKILL.md → commands/browse.md} +0 -17
- package/{skills/ship/SKILL.md → commands/canary.md} +0 -18
- package/{skills/careful/SKILL.md → commands/careful.md} +0 -20
- package/{skills/canary/SKILL.md → commands/codex.md} +0 -17
- package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +0 -15
- package/commands/cso.md +72 -0
- package/commands/design-consultation.md +72 -0
- package/commands/design-review.md +72 -0
- package/commands/design-shotgun.md +72 -0
- package/commands/document-release.md +72 -0
- package/{skills/freeze/SKILL.md → commands/freeze.md} +0 -26
- package/{skills/gstack-upgrade/SKILL.md → commands/gstack-upgrade.md} +0 -14
- package/{skills/guard/SKILL.md → commands/guard.md} +0 -31
- package/commands/investigate.md +72 -0
- package/commands/land-and-deploy.md +72 -0
- package/commands/office-hours.md +72 -0
- package/commands/plan-ceo-review.md +72 -0
- package/commands/plan-design-review.md +72 -0
- package/commands/plan-eng-review.md +72 -0
- package/commands/qa-only.md +72 -0
- package/commands/qa.md +72 -0
- package/commands/retro.md +72 -0
- package/commands/review.md +72 -0
- package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +0 -14
- package/commands/setup-deploy.md +72 -0
- package/commands/ship.md +72 -0
- package/{skills/unfreeze/SKILL.md → commands/unfreeze.md} +0 -12
- package/package.json +4 -4
- package/scripts/install-commands.js +45 -0
- package/skills/autoplan/SKILL.md +0 -96
- package/skills/autoplan/SKILL.md.tmpl +0 -694
- package/skills/benchmark/SKILL.md.tmpl +0 -222
- package/skills/browse/SKILL.md.tmpl +0 -131
- package/skills/browse/bin/find-browse +0 -21
- package/skills/browse/bin/remote-slug +0 -14
- package/skills/browse/scripts/build-node-server.sh +0 -48
- package/skills/browse/src/activity.ts +0 -208
- package/skills/browse/src/browser-manager.ts +0 -959
- package/skills/browse/src/buffers.ts +0 -137
- package/skills/browse/src/bun-polyfill.cjs +0 -109
- package/skills/browse/src/cli.ts +0 -678
- package/skills/browse/src/commands.ts +0 -128
- package/skills/browse/src/config.ts +0 -150
- package/skills/browse/src/cookie-import-browser.ts +0 -625
- package/skills/browse/src/cookie-picker-routes.ts +0 -230
- package/skills/browse/src/cookie-picker-ui.ts +0 -688
- package/skills/browse/src/find-browse.ts +0 -61
- package/skills/browse/src/meta-commands.ts +0 -550
- package/skills/browse/src/platform.ts +0 -17
- package/skills/browse/src/read-commands.ts +0 -358
- package/skills/browse/src/server.ts +0 -1192
- package/skills/browse/src/sidebar-agent.ts +0 -280
- package/skills/browse/src/sidebar-utils.ts +0 -21
- package/skills/browse/src/snapshot.ts +0 -407
- package/skills/browse/src/url-validation.ts +0 -95
- package/skills/browse/src/write-commands.ts +0 -364
- package/skills/browse/test/activity.test.ts +0 -120
- package/skills/browse/test/adversarial-security.test.ts +0 -32
- package/skills/browse/test/browser-manager-unit.test.ts +0 -17
- package/skills/browse/test/bun-polyfill.test.ts +0 -72
- package/skills/browse/test/commands.test.ts +0 -2075
- package/skills/browse/test/compare-board.test.ts +0 -342
- package/skills/browse/test/config.test.ts +0 -316
- package/skills/browse/test/cookie-import-browser.test.ts +0 -519
- package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
- package/skills/browse/test/file-drop.test.ts +0 -271
- package/skills/browse/test/find-browse.test.ts +0 -50
- package/skills/browse/test/findport.test.ts +0 -191
- package/skills/browse/test/fixtures/basic.html +0 -33
- package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
- package/skills/browse/test/fixtures/dialog.html +0 -15
- package/skills/browse/test/fixtures/empty.html +0 -2
- package/skills/browse/test/fixtures/forms.html +0 -55
- package/skills/browse/test/fixtures/iframe.html +0 -30
- package/skills/browse/test/fixtures/network-idle.html +0 -30
- package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
- package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
- package/skills/browse/test/fixtures/qa-eval.html +0 -51
- package/skills/browse/test/fixtures/responsive.html +0 -49
- package/skills/browse/test/fixtures/snapshot.html +0 -55
- package/skills/browse/test/fixtures/spa.html +0 -24
- package/skills/browse/test/fixtures/states.html +0 -17
- package/skills/browse/test/fixtures/upload.html +0 -25
- package/skills/browse/test/gstack-config.test.ts +0 -138
- package/skills/browse/test/gstack-update-check.test.ts +0 -514
- package/skills/browse/test/handoff.test.ts +0 -235
- package/skills/browse/test/path-validation.test.ts +0 -91
- package/skills/browse/test/platform.test.ts +0 -37
- package/skills/browse/test/server-auth.test.ts +0 -65
- package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
- package/skills/browse/test/sidebar-agent.test.ts +0 -199
- package/skills/browse/test/sidebar-integration.test.ts +0 -320
- package/skills/browse/test/sidebar-unit.test.ts +0 -96
- package/skills/browse/test/snapshot.test.ts +0 -467
- package/skills/browse/test/state-ttl.test.ts +0 -35
- package/skills/browse/test/test-server.ts +0 -57
- package/skills/browse/test/url-validation.test.ts +0 -72
- package/skills/browse/test/watch.test.ts +0 -129
- package/skills/canary/SKILL.md.tmpl +0 -212
- package/skills/careful/SKILL.md.tmpl +0 -56
- package/skills/careful/bin/check-careful.sh +0 -112
- package/skills/codex/SKILL.md +0 -90
- package/skills/codex/SKILL.md.tmpl +0 -417
- package/skills/connect-chrome/SKILL.md.tmpl +0 -195
- package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
- package/skills/cso/SKILL.md +0 -93
- package/skills/cso/SKILL.md.tmpl +0 -606
- package/skills/design-consultation/SKILL.md +0 -94
- package/skills/design-consultation/SKILL.md.tmpl +0 -415
- package/skills/design-review/SKILL.md +0 -94
- package/skills/design-review/SKILL.md.tmpl +0 -290
- package/skills/design-shotgun/SKILL.md +0 -91
- package/skills/design-shotgun/SKILL.md.tmpl +0 -285
- package/skills/document-release/SKILL.md +0 -91
- package/skills/document-release/SKILL.md.tmpl +0 -359
- package/skills/freeze/SKILL.md.tmpl +0 -77
- package/skills/freeze/bin/check-freeze.sh +0 -79
- package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
- package/skills/guard/SKILL.md.tmpl +0 -77
- package/skills/investigate/SKILL.md +0 -105
- package/skills/investigate/SKILL.md.tmpl +0 -194
- package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
- package/skills/office-hours/SKILL.md +0 -96
- package/skills/office-hours/SKILL.md.tmpl +0 -645
- package/skills/plan-ceo-review/SKILL.md +0 -94
- package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
- package/skills/plan-design-review/SKILL.md +0 -92
- package/skills/plan-design-review/SKILL.md.tmpl +0 -446
- package/skills/plan-eng-review/SKILL.md +0 -93
- package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
- package/skills/qa/SKILL.md +0 -95
- package/skills/qa/SKILL.md.tmpl +0 -316
- package/skills/qa/references/issue-taxonomy.md +0 -85
- package/skills/qa/templates/qa-report-template.md +0 -126
- package/skills/qa-only/SKILL.md +0 -89
- package/skills/qa-only/SKILL.md.tmpl +0 -101
- package/skills/retro/SKILL.md +0 -89
- package/skills/retro/SKILL.md.tmpl +0 -820
- package/skills/review/SKILL.md +0 -92
- package/skills/review/SKILL.md.tmpl +0 -281
- package/skills/review/TODOS-format.md +0 -62
- package/skills/review/checklist.md +0 -220
- package/skills/review/design-checklist.md +0 -132
- package/skills/review/greptile-triage.md +0 -220
- package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
- package/skills/setup-deploy/SKILL.md +0 -92
- package/skills/setup-deploy/SKILL.md.tmpl +0 -215
- package/skills/ship/SKILL.md.tmpl +0 -636
- package/skills/unfreeze/SKILL.md.tmpl +0 -36
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: benchmark
|
|
3
|
-
preamble-tier: 1
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
description: |
|
|
6
|
-
Performance regression detection using the browse daemon. Establishes
|
|
7
|
-
baselines for page load times, Core Web Vitals, and resource sizes.
|
|
8
|
-
Compares before/after on every PR. Tracks performance trends over time.
|
|
9
|
-
Use when: "performance", "benchmark", "page speed", "lighthouse", "web vitals",
|
|
10
|
-
"bundle size", "load time".
|
|
11
|
-
allowed-tools:
|
|
12
|
-
- Bash
|
|
13
|
-
- Read
|
|
14
|
-
- Write
|
|
15
|
-
- Glob
|
|
16
|
-
- AskUserQuestion
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
{{PREAMBLE}}
|
|
20
|
-
|
|
21
|
-
{{BROWSE_SETUP}}
|
|
22
|
-
|
|
23
|
-
# /benchmark — Performance Regression Detection
|
|
24
|
-
|
|
25
|
-
You are a **Performance Engineer** who has optimized apps serving millions of requests. You know that performance doesn't degrade in one big regression — it dies by a thousand paper cuts. Each PR adds 50ms here, 20KB there, and one day the app takes 8 seconds to load and nobody knows when it got slow.
|
|
26
|
-
|
|
27
|
-
Your job is to measure, baseline, compare, and alert. You use the browse daemon's `perf` command and JavaScript evaluation to gather real performance data from running pages.
|
|
28
|
-
|
|
29
|
-
## User-invocable
|
|
30
|
-
When the user types `/benchmark`, run this skill.
|
|
31
|
-
|
|
32
|
-
## Arguments
|
|
33
|
-
- `/benchmark <url>` — full performance audit with baseline comparison
|
|
34
|
-
- `/benchmark <url> --baseline` — capture baseline (run before making changes)
|
|
35
|
-
- `/benchmark <url> --quick` — single-pass timing check (no baseline needed)
|
|
36
|
-
- `/benchmark <url> --pages /,/dashboard,/api/health` — specify pages
|
|
37
|
-
- `/benchmark --diff` — benchmark only pages affected by current branch
|
|
38
|
-
- `/benchmark --trend` — show performance trends from historical data
|
|
39
|
-
|
|
40
|
-
## Instructions
|
|
41
|
-
|
|
42
|
-
### Phase 1: Setup
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
eval "$(~/.claude/skills/opengstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")"
|
|
46
|
-
mkdir -p .gstack/benchmark-reports
|
|
47
|
-
mkdir -p .gstack/benchmark-reports/baselines
|
|
48
|
-
|
|
49
|
-
### Phase 2: Page Discovery
|
|
50
|
-
|
|
51
|
-
Same as /canary — auto-discover from navigation or use `--pages`.
|
|
52
|
-
|
|
53
|
-
If `--diff` mode:
|
|
54
|
-
```bash
|
|
55
|
-
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
|
|
56
|
-
|
|
57
|
-
### Phase 3: Performance Data Collection
|
|
58
|
-
|
|
59
|
-
For each page, collect comprehensive performance metrics:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
$B goto <page-url>
|
|
63
|
-
$B perf
|
|
64
|
-
|
|
65
|
-
Then gather detailed metrics via JavaScript:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
$B eval "JSON.stringify(performance.getEntriesByType('navigation')[0])"
|
|
69
|
-
|
|
70
|
-
Extract key metrics:
|
|
71
|
-
- **TTFB** (Time to First Byte): `responseStart - requestStart`
|
|
72
|
-
- **FCP** (First Contentful Paint): from PerformanceObserver or `paint` entries
|
|
73
|
-
- **LCP** (Largest Contentful Paint): from PerformanceObserver
|
|
74
|
-
- **DOM Interactive**: `domInteractive - navigationStart`
|
|
75
|
-
- **DOM Complete**: `domComplete - navigationStart`
|
|
76
|
-
- **Full Load**: `loadEventEnd - navigationStart`
|
|
77
|
-
|
|
78
|
-
Resource analysis:
|
|
79
|
-
```bash
|
|
80
|
-
$B eval "JSON.stringify(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))"
|
|
81
|
-
|
|
82
|
-
Bundle size check:
|
|
83
|
-
```bash
|
|
84
|
-
$B eval "JSON.stringify(performance.getEntriesByType('resource').filter(r => r.initiatorType === 'script').map(r => ({name: r.name.split('/').pop().split('?')[0], size: r.transferSize})))"
|
|
85
|
-
$B eval "JSON.stringify(performance.getEntriesByType('resource').filter(r => r.initiatorType === 'css').map(r => ({name: r.name.split('/').pop().split('?')[0], size: r.transferSize})))"
|
|
86
|
-
|
|
87
|
-
Network summary:
|
|
88
|
-
```bash
|
|
89
|
-
$B eval "(() => { const r = performance.getEntriesByType('resource'); return JSON.stringify({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])})})()"
|
|
90
|
-
|
|
91
|
-
### Phase 4: Baseline Capture (--baseline mode)
|
|
92
|
-
|
|
93
|
-
Save metrics to baseline file:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"url": "<url>",
|
|
98
|
-
"timestamp": "<ISO>",
|
|
99
|
-
"branch": "<branch>",
|
|
100
|
-
"pages": {
|
|
101
|
-
"/": {
|
|
102
|
-
"ttfb_ms": 120,
|
|
103
|
-
"fcp_ms": 450,
|
|
104
|
-
"lcp_ms": 800,
|
|
105
|
-
"dom_interactive_ms": 600,
|
|
106
|
-
"dom_complete_ms": 1200,
|
|
107
|
-
"full_load_ms": 1400,
|
|
108
|
-
"total_requests": 42,
|
|
109
|
-
"total_transfer_bytes": 1250000,
|
|
110
|
-
"js_bundle_bytes": 450000,
|
|
111
|
-
"css_bundle_bytes": 85000,
|
|
112
|
-
"largest_resources": [
|
|
113
|
-
{"name": "main.js", "size": 320000, "duration": 180},
|
|
114
|
-
{"name": "vendor.js", "size": 130000, "duration": 90}
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
Write to `.gstack/benchmark-reports/baselines/baseline.json`.
|
|
121
|
-
|
|
122
|
-
### Phase 5: Comparison
|
|
123
|
-
|
|
124
|
-
If baseline exists, compare current metrics against it:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
PERFORMANCE REPORT — [url]
|
|
128
|
-
══════════════════════════
|
|
129
|
-
Branch: [current-branch] vs baseline ([baseline-branch])
|
|
130
|
-
|
|
131
|
-
Page: /
|
|
132
|
-
─────────────────────────────────────────────────────
|
|
133
|
-
Metric Baseline Current Delta Status
|
|
134
|
-
──────── ──────── ─────── ───── ──────
|
|
135
|
-
TTFB 120ms 135ms +15ms OK
|
|
136
|
-
FCP 450ms 480ms +30ms OK
|
|
137
|
-
LCP 800ms 1600ms +800ms REGRESSION
|
|
138
|
-
DOM Interactive 600ms 650ms +50ms OK
|
|
139
|
-
DOM Complete 1200ms 1350ms +150ms WARNING
|
|
140
|
-
Full Load 1400ms 2100ms +700ms REGRESSION
|
|
141
|
-
Total Requests 42 58 +16 WARNING
|
|
142
|
-
Transfer Size 1.2MB 1.8MB +0.6MB REGRESSION
|
|
143
|
-
JS Bundle 450KB 720KB +270KB REGRESSION
|
|
144
|
-
CSS Bundle 85KB 88KB +3KB OK
|
|
145
|
-
|
|
146
|
-
REGRESSIONS DETECTED: 3
|
|
147
|
-
[1] LCP doubled (800ms → 1600ms) — likely a large new image or blocking resource
|
|
148
|
-
[2] Total transfer +50% (1.2MB → 1.8MB) — check new JS bundles
|
|
149
|
-
[3] JS bundle +60% (450KB → 720KB) — new dependency or missing tree-shaking
|
|
150
|
-
|
|
151
|
-
**Regression thresholds:**
|
|
152
|
-
- Timing metrics: >50% increase OR >500ms absolute increase = REGRESSION
|
|
153
|
-
- Timing metrics: >20% increase = WARNING
|
|
154
|
-
- Bundle size: >25% increase = REGRESSION
|
|
155
|
-
- Bundle size: >10% increase = WARNING
|
|
156
|
-
- Request count: >30% increase = WARNING
|
|
157
|
-
|
|
158
|
-
### Phase 6: Slowest Resources
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
TOP 10 SLOWEST RESOURCES
|
|
162
|
-
═════════════════════════
|
|
163
|
-
# Resource Type Size Duration
|
|
164
|
-
1 vendor.chunk.js script 320KB 480ms
|
|
165
|
-
2 main.js script 250KB 320ms
|
|
166
|
-
3 hero-image.webp img 180KB 280ms
|
|
167
|
-
4 analytics.js script 45KB 250ms ← third-party
|
|
168
|
-
5 fonts/inter-var.woff2 font 95KB 180ms
|
|
169
|
-
...
|
|
170
|
-
|
|
171
|
-
RECOMMENDATIONS:
|
|
172
|
-
- vendor.chunk.js: Consider code-splitting — 320KB is large for initial load
|
|
173
|
-
- analytics.js: Load async/defer — blocks rendering for 250ms
|
|
174
|
-
- hero-image.webp: Add width/height to prevent CLS, consider lazy loading
|
|
175
|
-
|
|
176
|
-
### Phase 7: Performance Budget
|
|
177
|
-
|
|
178
|
-
Check against industry budgets:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
PERFORMANCE BUDGET CHECK
|
|
182
|
-
════════════════════════
|
|
183
|
-
Metric Budget Actual Status
|
|
184
|
-
──────── ────── ────── ──────
|
|
185
|
-
FCP < 1.8s 0.48s PASS
|
|
186
|
-
LCP < 2.5s 1.6s PASS
|
|
187
|
-
Total JS < 500KB 720KB FAIL
|
|
188
|
-
Total CSS < 100KB 88KB PASS
|
|
189
|
-
Total Transfer < 2MB 1.8MB WARNING (90%)
|
|
190
|
-
HTTP Requests < 50 58 FAIL
|
|
191
|
-
|
|
192
|
-
Grade: B (4/6 passing)
|
|
193
|
-
|
|
194
|
-
### Phase 8: Trend Analysis (--trend mode)
|
|
195
|
-
|
|
196
|
-
Load historical baseline files and show trends:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
PERFORMANCE TRENDS (last 5 benchmarks)
|
|
200
|
-
══════════════════════════════════════
|
|
201
|
-
Date FCP LCP Bundle Requests Grade
|
|
202
|
-
2026-03-10 420ms 750ms 380KB 38 A
|
|
203
|
-
2026-03-12 440ms 780ms 410KB 40 A
|
|
204
|
-
2026-03-14 450ms 800ms 450KB 42 A
|
|
205
|
-
2026-03-16 460ms 850ms 520KB 48 B
|
|
206
|
-
2026-03-18 480ms 1600ms 720KB 58 B
|
|
207
|
-
|
|
208
|
-
TREND: Performance degrading. LCP doubled in 8 days.
|
|
209
|
-
JS bundle growing 50KB/week. Investigate.
|
|
210
|
-
|
|
211
|
-
### Phase 9: Save Report
|
|
212
|
-
|
|
213
|
-
Write to `.gstack/benchmark-reports/{date}-benchmark.md` and `.gstack/benchmark-reports/{date}-benchmark.json`.
|
|
214
|
-
|
|
215
|
-
## Important Rules
|
|
216
|
-
|
|
217
|
-
- **Measure, don't guess.** Use actual performance.getEntries() data, not estimates.
|
|
218
|
-
- **Baseline is essential.** Without a baseline, you can report absolute numbers but can't detect regressions. Always encourage baseline capture.
|
|
219
|
-
- **Relative thresholds, not absolute.** 2000ms load time is fine for a complex dashboard, terrible for a landing page. Compare against YOUR baseline.
|
|
220
|
-
- **Third-party scripts are context.** Flag them, but the user can't fix Google Analytics being slow. Focus recommendations on first-party resources.
|
|
221
|
-
- **Bundle size is the leading indicator.** Load time varies with network. Bundle size is deterministic. Track it religiously.
|
|
222
|
-
- **Read-only.** Produce the report. Don't modify code unless explicitly asked.
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: browse
|
|
3
|
-
preamble-tier: 1
|
|
4
|
-
version: 1.1.0
|
|
5
|
-
description: |
|
|
6
|
-
Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with
|
|
7
|
-
elements, verify page state, diff before/after actions, take annotated screenshots, check
|
|
8
|
-
responsive layouts, test forms and uploads, handle dialogs, and assert element states.
|
|
9
|
-
~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a
|
|
10
|
-
user flow, or file a bug with evidence. Use when asked to "open in browser", "test the
|
|
11
|
-
site", "take a screenshot", or "dogfood this".
|
|
12
|
-
allowed-tools:
|
|
13
|
-
- Bash
|
|
14
|
-
- Read
|
|
15
|
-
- AskUserQuestion
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
{{PREAMBLE}}
|
|
20
|
-
|
|
21
|
-
# browse: QA Testing & Dogfooding
|
|
22
|
-
|
|
23
|
-
Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command.
|
|
24
|
-
State persists between calls (cookies, tabs, login sessions).
|
|
25
|
-
|
|
26
|
-
{{BROWSE_SETUP}}
|
|
27
|
-
|
|
28
|
-
## Core QA Patterns
|
|
29
|
-
|
|
30
|
-
### 1. Verify a page loads correctly
|
|
31
|
-
```bash
|
|
32
|
-
$B goto https://yourapp.com
|
|
33
|
-
$B text # content loads?
|
|
34
|
-
$B console # JS errors?
|
|
35
|
-
$B network # failed requests?
|
|
36
|
-
$B is visible ".main-content" # key elements present?
|
|
37
|
-
|
|
38
|
-
### 2. Test a user flow
|
|
39
|
-
```bash
|
|
40
|
-
$B goto https://app.com/login
|
|
41
|
-
$B snapshot -i # see all interactive elements
|
|
42
|
-
$B fill @e3 "user@test.com"
|
|
43
|
-
$B fill @e4 "password"
|
|
44
|
-
$B click @e5 # submit
|
|
45
|
-
$B snapshot -D # diff: what changed after submit?
|
|
46
|
-
$B is visible ".dashboard" # success state present?
|
|
47
|
-
|
|
48
|
-
### 3. Verify an action worked
|
|
49
|
-
```bash
|
|
50
|
-
$B snapshot # baseline
|
|
51
|
-
$B click @e3 # do something
|
|
52
|
-
$B snapshot -D # unified diff shows exactly what changed
|
|
53
|
-
|
|
54
|
-
### 4. Visual evidence for bug reports
|
|
55
|
-
```bash
|
|
56
|
-
$B snapshot -i -a -o /tmp/annotated.png # labeled screenshot
|
|
57
|
-
$B screenshot /tmp/bug.png # plain screenshot
|
|
58
|
-
$B console # error log
|
|
59
|
-
|
|
60
|
-
### 5. Find all clickable elements (including non-ARIA)
|
|
61
|
-
```bash
|
|
62
|
-
$B snapshot -C # finds divs with cursor:pointer, onclick, tabindex
|
|
63
|
-
$B click @c1 # interact with them
|
|
64
|
-
|
|
65
|
-
### 6. Assert element states
|
|
66
|
-
```bash
|
|
67
|
-
$B is visible ".modal"
|
|
68
|
-
$B is enabled "#submit-btn"
|
|
69
|
-
$B is disabled "#submit-btn"
|
|
70
|
-
$B is checked "#agree-checkbox"
|
|
71
|
-
$B is editable "#name-field"
|
|
72
|
-
$B is focused "#search-input"
|
|
73
|
-
$B js "document.body.textContent.includes('Success')"
|
|
74
|
-
|
|
75
|
-
### 7. Test responsive layouts
|
|
76
|
-
```bash
|
|
77
|
-
$B responsive /tmp/layout # mobile + tablet + desktop screenshots
|
|
78
|
-
$B viewport 375x812 # or set specific viewport
|
|
79
|
-
$B screenshot /tmp/mobile.png
|
|
80
|
-
|
|
81
|
-
### 8. Test file uploads
|
|
82
|
-
```bash
|
|
83
|
-
$B upload "#file-input" /path/to/file.pdf
|
|
84
|
-
$B is visible ".upload-success"
|
|
85
|
-
|
|
86
|
-
### 9. Test dialogs
|
|
87
|
-
```bash
|
|
88
|
-
$B dialog-accept "yes" # set up handler
|
|
89
|
-
$B click "#delete-button" # trigger dialog
|
|
90
|
-
$B dialog # see what appeared
|
|
91
|
-
$B snapshot -D # verify deletion happened
|
|
92
|
-
|
|
93
|
-
### 10. Compare environments
|
|
94
|
-
```bash
|
|
95
|
-
$B diff https://staging.app.com https://prod.app.com
|
|
96
|
-
|
|
97
|
-
### 11. Show screenshots to the user
|
|
98
|
-
After `$B screenshot`, `$B snapshot -a -o`, or `$B responsive`, always use the Read tool on the output PNG(s) so the user can see them. Without this, screenshots are invisible.
|
|
99
|
-
|
|
100
|
-
## User Handoff
|
|
101
|
-
|
|
102
|
-
When you hit something you can't handle in headless mode (CAPTCHA, complex auth, multi-factor
|
|
103
|
-
login), hand off to the user:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
# 1. Open a visible Chrome at the current page
|
|
107
|
-
$B handoff "Stuck on CAPTCHA at login page"
|
|
108
|
-
|
|
109
|
-
# 2. Tell the user what happened (via AskUserQuestion)
|
|
110
|
-
# "I've opened Chrome at the login page. Please solve the CAPTCHA
|
|
111
|
-
# and let me know when you're done."
|
|
112
|
-
|
|
113
|
-
# 3. When user says "done", re-snapshot and continue
|
|
114
|
-
$B resume
|
|
115
|
-
|
|
116
|
-
**When to use handoff:**
|
|
117
|
-
- CAPTCHAs or bot detection
|
|
118
|
-
- Multi-factor authentication (SMS, authenticator app)
|
|
119
|
-
- OAuth flows that require user interaction
|
|
120
|
-
- Complex interactions the AI can't handle after 3 attempts
|
|
121
|
-
|
|
122
|
-
The browser preserves all state (cookies, localStorage, tabs) across the handoff.
|
|
123
|
-
After `resume`, you get a fresh snapshot of wherever the user left off.
|
|
124
|
-
|
|
125
|
-
## Snapshot Flags
|
|
126
|
-
|
|
127
|
-
{{SNAPSHOT_FLAGS}}
|
|
128
|
-
|
|
129
|
-
## Full Command List
|
|
130
|
-
|
|
131
|
-
{{COMMAND_REFERENCE}}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Shim: delegates to compiled find-browse binary, falls back to basic discovery.
|
|
3
|
-
# The compiled binary handles git root detection for workspace-local installs.
|
|
4
|
-
DIR="$(cd "$(dirname "$0")/.." && pwd)/dist"
|
|
5
|
-
if test -x "$DIR/find-browse"; then
|
|
6
|
-
exec "$DIR/find-browse" "$@"
|
|
7
|
-
fi
|
|
8
|
-
# Fallback: basic discovery with priority chain
|
|
9
|
-
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
10
|
-
for MARKER in .codex .agents .claude; do
|
|
11
|
-
if [ -n "$ROOT" ] && test -x "$ROOT/$MARKER/skills/gstack/browse/dist/browse"; then
|
|
12
|
-
echo "$ROOT/$MARKER/skills/gstack/browse/dist/browse"
|
|
13
|
-
exit 0
|
|
14
|
-
fi
|
|
15
|
-
if test -x "$HOME/$MARKER/skills/gstack/browse/dist/browse"; then
|
|
16
|
-
echo "$HOME/$MARKER/skills/gstack/browse/dist/browse"
|
|
17
|
-
exit 0
|
|
18
|
-
fi
|
|
19
|
-
done
|
|
20
|
-
echo "ERROR: browse binary not found. Run: cd <skill-dir> && ./setup" >&2
|
|
21
|
-
exit 1
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Output the remote slug (owner-repo) for the current git repo.
|
|
3
|
-
# Used by SKILL.md files to derive project-specific paths in ~/.gstack/projects/.
|
|
4
|
-
set -e
|
|
5
|
-
URL=$(git remote get-url origin 2>/dev/null || true)
|
|
6
|
-
if [ -n "$URL" ]; then
|
|
7
|
-
# Strip trailing .git if present, then extract owner/repo
|
|
8
|
-
URL="${URL%.git}"
|
|
9
|
-
# Handle both SSH (git@host:owner/repo) and HTTPS (https://host/owner/repo)
|
|
10
|
-
OWNER_REPO=$(echo "$URL" | sed -E 's#.*[:/]([^/]+)/([^/]+)$#\1-\2#')
|
|
11
|
-
echo "$OWNER_REPO"
|
|
12
|
-
else
|
|
13
|
-
basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
14
|
-
fi
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Build a Node.js-compatible server bundle for Windows.
|
|
3
|
-
#
|
|
4
|
-
# On Windows, Bun can't launch or connect to Playwright's Chromium
|
|
5
|
-
# (oven-sh/bun#4253, #9911). This script produces a server bundle
|
|
6
|
-
# that runs under Node.js with Bun API polyfills.
|
|
7
|
-
|
|
8
|
-
set -e
|
|
9
|
-
|
|
10
|
-
GSTACK_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
11
|
-
SRC_DIR="$GSTACK_DIR/browse/src"
|
|
12
|
-
DIST_DIR="$GSTACK_DIR/browse/dist"
|
|
13
|
-
|
|
14
|
-
echo "Building Node-compatible server bundle..."
|
|
15
|
-
|
|
16
|
-
# Step 1: Transpile server.ts to a single .mjs bundle (externalize runtime deps)
|
|
17
|
-
bun build "$SRC_DIR/server.ts" \
|
|
18
|
-
--target=node \
|
|
19
|
-
--outfile "$DIST_DIR/server-node.mjs" \
|
|
20
|
-
--external playwright \
|
|
21
|
-
--external playwright-core \
|
|
22
|
-
--external diff \
|
|
23
|
-
--external "bun:sqlite"
|
|
24
|
-
|
|
25
|
-
# Step 2: Post-process
|
|
26
|
-
# Replace import.meta.dir with a resolvable reference
|
|
27
|
-
perl -pi -e 's/import\.meta\.dir/__browseNodeSrcDir/g' "$DIST_DIR/server-node.mjs"
|
|
28
|
-
# Stub out bun:sqlite (macOS-only cookie import, not needed on Windows)
|
|
29
|
-
perl -pi -e 's|import { Database } from "bun:sqlite";|const Database = null; // bun:sqlite stubbed on Node|g' "$DIST_DIR/server-node.mjs"
|
|
30
|
-
|
|
31
|
-
# Step 3: Create the final file with polyfill header injected after the first line
|
|
32
|
-
{
|
|
33
|
-
head -1 "$DIST_DIR/server-node.mjs"
|
|
34
|
-
echo '// ── Windows Node.js compatibility (auto-generated) ──'
|
|
35
|
-
echo 'import { fileURLToPath as _ftp } from "node:url";'
|
|
36
|
-
echo 'import { dirname as _dn } from "node:path";'
|
|
37
|
-
echo 'const __browseNodeSrcDir = _dn(_dn(_ftp(import.meta.url))) + "/src";'
|
|
38
|
-
echo '{ const _r = createRequire(import.meta.url); _r("./bun-polyfill.cjs"); }'
|
|
39
|
-
echo '// ── end compatibility ──'
|
|
40
|
-
tail -n +2 "$DIST_DIR/server-node.mjs"
|
|
41
|
-
} > "$DIST_DIR/server-node.tmp.mjs"
|
|
42
|
-
|
|
43
|
-
mv "$DIST_DIR/server-node.tmp.mjs" "$DIST_DIR/server-node.mjs"
|
|
44
|
-
|
|
45
|
-
# Step 4: Copy polyfill to dist/
|
|
46
|
-
cp "$SRC_DIR/bun-polyfill.cjs" "$DIST_DIR/bun-polyfill.cjs"
|
|
47
|
-
|
|
48
|
-
echo "Node server bundle ready: $DIST_DIR/server-node.mjs"
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Activity streaming — real-time feed of browse commands for the Chrome extension Side Panel
|
|
3
|
-
*
|
|
4
|
-
* Architecture:
|
|
5
|
-
* handleCommand() ──► emitActivity(command_start)
|
|
6
|
-
* ──► emitActivity(command_end)
|
|
7
|
-
* wirePageEvents() ──► emitActivity(navigation)
|
|
8
|
-
*
|
|
9
|
-
* GET /activity/stream?after=ID ──► SSE via ReadableStream
|
|
10
|
-
* GET /activity/history?limit=N ──► REST fallback
|
|
11
|
-
*
|
|
12
|
-
* Privacy: filterArgs() redacts passwords, auth tokens, and sensitive query params.
|
|
13
|
-
* Backpressure: subscribers notified via queueMicrotask (never blocks command path).
|
|
14
|
-
* Gap detection: client sends ?after=ID, server detects if ring buffer overflowed.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { CircularBuffer } from './buffers';
|
|
18
|
-
|
|
19
|
-
// ─── Types ──────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
export interface ActivityEntry {
|
|
22
|
-
id: number;
|
|
23
|
-
timestamp: number;
|
|
24
|
-
type: 'command_start' | 'command_end' | 'navigation' | 'error';
|
|
25
|
-
command?: string;
|
|
26
|
-
args?: string[];
|
|
27
|
-
url?: string;
|
|
28
|
-
duration?: number;
|
|
29
|
-
status?: 'ok' | 'error';
|
|
30
|
-
error?: string;
|
|
31
|
-
result?: string;
|
|
32
|
-
tabs?: number;
|
|
33
|
-
mode?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ─── Buffer & Subscribers ───────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const BUFFER_CAPACITY = 1000;
|
|
39
|
-
const activityBuffer = new CircularBuffer<ActivityEntry>(BUFFER_CAPACITY);
|
|
40
|
-
let nextId = 1;
|
|
41
|
-
|
|
42
|
-
type ActivitySubscriber = (entry: ActivityEntry) => void;
|
|
43
|
-
const subscribers = new Set<ActivitySubscriber>();
|
|
44
|
-
|
|
45
|
-
// ─── Privacy Filtering ─────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const SENSITIVE_COMMANDS = new Set(['fill', 'type', 'cookie', 'header']);
|
|
48
|
-
const SENSITIVE_PARAM_PATTERN = /\b(password|token|secret|key|auth|bearer|api[_-]?key)\b/i;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Redact sensitive data from command args before streaming.
|
|
52
|
-
*/
|
|
53
|
-
export function filterArgs(command: string, args: string[]): string[] {
|
|
54
|
-
if (!args || args.length === 0) return args;
|
|
55
|
-
|
|
56
|
-
// fill: redact the value (last arg) for password-type fields
|
|
57
|
-
if (command === 'fill' && args.length >= 2) {
|
|
58
|
-
const selector = args[0];
|
|
59
|
-
// If the selector suggests a password field, redact the value
|
|
60
|
-
if (/password|passwd|secret|token/i.test(selector)) {
|
|
61
|
-
return [selector, '[REDACTED]'];
|
|
62
|
-
}
|
|
63
|
-
return args;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// header: redact Authorization and other sensitive headers
|
|
67
|
-
if (command === 'header' && args.length >= 1) {
|
|
68
|
-
const headerLine = args[0];
|
|
69
|
-
if (/^(authorization|x-api-key|cookie|set-cookie)/i.test(headerLine)) {
|
|
70
|
-
const colonIdx = headerLine.indexOf(':');
|
|
71
|
-
if (colonIdx > 0) {
|
|
72
|
-
return [headerLine.substring(0, colonIdx + 1) + '[REDACTED]'];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return args;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// cookie: redact cookie values
|
|
79
|
-
if (command === 'cookie' && args.length >= 1) {
|
|
80
|
-
const cookieStr = args[0];
|
|
81
|
-
const eqIdx = cookieStr.indexOf('=');
|
|
82
|
-
if (eqIdx > 0) {
|
|
83
|
-
return [cookieStr.substring(0, eqIdx + 1) + '[REDACTED]'];
|
|
84
|
-
}
|
|
85
|
-
return args;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// type: always redact (could be a password field)
|
|
89
|
-
if (command === 'type') {
|
|
90
|
-
return ['[REDACTED]'];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// URL args: redact sensitive query params
|
|
94
|
-
return args.map(arg => {
|
|
95
|
-
if (arg.startsWith('http://') || arg.startsWith('https://')) {
|
|
96
|
-
try {
|
|
97
|
-
const url = new URL(arg);
|
|
98
|
-
let redacted = false;
|
|
99
|
-
for (const key of url.searchParams.keys()) {
|
|
100
|
-
if (SENSITIVE_PARAM_PATTERN.test(key)) {
|
|
101
|
-
url.searchParams.set(key, '[REDACTED]');
|
|
102
|
-
redacted = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return redacted ? url.toString() : arg;
|
|
106
|
-
} catch {
|
|
107
|
-
return arg;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return arg;
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Truncate result text for streaming (max 200 chars).
|
|
116
|
-
*/
|
|
117
|
-
function truncateResult(result: string | undefined): string | undefined {
|
|
118
|
-
if (!result) return undefined;
|
|
119
|
-
if (result.length <= 200) return result;
|
|
120
|
-
return result.substring(0, 200) + '...';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ─── Public API ─────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Emit an activity event. Backpressure-safe: subscribers notified asynchronously.
|
|
127
|
-
*/
|
|
128
|
-
export function emitActivity(entry: Omit<ActivityEntry, 'id' | 'timestamp'>): ActivityEntry {
|
|
129
|
-
const full: ActivityEntry = {
|
|
130
|
-
...entry,
|
|
131
|
-
id: nextId++,
|
|
132
|
-
timestamp: Date.now(),
|
|
133
|
-
args: entry.args ? filterArgs(entry.command || '', entry.args) : undefined,
|
|
134
|
-
result: truncateResult(entry.result),
|
|
135
|
-
};
|
|
136
|
-
activityBuffer.push(full);
|
|
137
|
-
|
|
138
|
-
// Notify subscribers asynchronously — never block the command path
|
|
139
|
-
for (const notify of subscribers) {
|
|
140
|
-
queueMicrotask(() => {
|
|
141
|
-
try { notify(full); } catch { /* subscriber error — don't crash */ }
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return full;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Subscribe to live activity events. Returns unsubscribe function.
|
|
150
|
-
*/
|
|
151
|
-
export function subscribe(fn: ActivitySubscriber): () => void {
|
|
152
|
-
subscribers.add(fn);
|
|
153
|
-
return () => subscribers.delete(fn);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Get recent activity entries after the given cursor ID.
|
|
158
|
-
* Returns entries and gap info if the buffer has overflowed.
|
|
159
|
-
*/
|
|
160
|
-
export function getActivityAfter(afterId: number): {
|
|
161
|
-
entries: ActivityEntry[];
|
|
162
|
-
gap: boolean;
|
|
163
|
-
gapFrom?: number;
|
|
164
|
-
availableFrom?: number;
|
|
165
|
-
totalAdded: number;
|
|
166
|
-
} {
|
|
167
|
-
const total = activityBuffer.totalAdded;
|
|
168
|
-
const allEntries = activityBuffer.toArray();
|
|
169
|
-
|
|
170
|
-
if (afterId === 0) {
|
|
171
|
-
return { entries: allEntries, gap: false, totalAdded: total };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check for gap: if afterId is too old and has been evicted
|
|
175
|
-
const oldestId = allEntries.length > 0 ? allEntries[0].id : nextId;
|
|
176
|
-
if (afterId < oldestId) {
|
|
177
|
-
return {
|
|
178
|
-
entries: allEntries,
|
|
179
|
-
gap: true,
|
|
180
|
-
gapFrom: afterId + 1,
|
|
181
|
-
availableFrom: oldestId,
|
|
182
|
-
totalAdded: total,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Filter to entries after the cursor
|
|
187
|
-
const filtered = allEntries.filter(e => e.id > afterId);
|
|
188
|
-
return { entries: filtered, gap: false, totalAdded: total };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get the N most recent activity entries.
|
|
193
|
-
*/
|
|
194
|
-
export function getActivityHistory(limit: number = 50): {
|
|
195
|
-
entries: ActivityEntry[];
|
|
196
|
-
totalAdded: number;
|
|
197
|
-
} {
|
|
198
|
-
const allEntries = activityBuffer.toArray();
|
|
199
|
-
const sliced = limit < allEntries.length ? allEntries.slice(-limit) : allEntries;
|
|
200
|
-
return { entries: sliced, totalAdded: activityBuffer.totalAdded };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Get subscriber count (for debugging/health).
|
|
205
|
-
*/
|
|
206
|
-
export function getSubscriberCount(): number {
|
|
207
|
-
return subscribers.size;
|
|
208
|
-
}
|