@runchr/gstack-antigravity 0.1.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.

Potentially problematic release.


This version of @runchr/gstack-antigravity might be problematic. Click here for more details.

Files changed (297) hide show
  1. package/.agents/rules/ETHOS.md +129 -0
  2. package/.agents/rules/global-gstack.md +117 -0
  3. package/.agents/rules/persona-gstack-autoplan.md +14 -0
  4. package/.agents/rules/persona-gstack-benchmark.md +14 -0
  5. package/.agents/rules/persona-gstack-browse.md +14 -0
  6. package/.agents/rules/persona-gstack-canary.md +14 -0
  7. package/.agents/rules/persona-gstack-careful.md +14 -0
  8. package/.agents/rules/persona-gstack-codex.md +14 -0
  9. package/.agents/rules/persona-gstack-cso.md +14 -0
  10. package/.agents/rules/persona-gstack-design-consultation.md +14 -0
  11. package/.agents/rules/persona-gstack-design-review.md +14 -0
  12. package/.agents/rules/persona-gstack-document-release.md +14 -0
  13. package/.agents/rules/persona-gstack-freeze.md +14 -0
  14. package/.agents/rules/persona-gstack-gstack-upgrade.md +14 -0
  15. package/.agents/rules/persona-gstack-guard.md +14 -0
  16. package/.agents/rules/persona-gstack-investigate.md +14 -0
  17. package/.agents/rules/persona-gstack-land-and-deploy.md +14 -0
  18. package/.agents/rules/persona-gstack-office-hours.md +14 -0
  19. package/.agents/rules/persona-gstack-plan-ceo-review.md +14 -0
  20. package/.agents/rules/persona-gstack-plan-design-review.md +14 -0
  21. package/.agents/rules/persona-gstack-plan-eng-review.md +14 -0
  22. package/.agents/rules/persona-gstack-qa-only.md +14 -0
  23. package/.agents/rules/persona-gstack-qa.md +14 -0
  24. package/.agents/rules/persona-gstack-retro.md +14 -0
  25. package/.agents/rules/persona-gstack-review.md +14 -0
  26. package/.agents/rules/persona-gstack-setup-browser-cookies.md +14 -0
  27. package/.agents/rules/persona-gstack-setup-deploy.md +14 -0
  28. package/.agents/rules/persona-gstack-ship.md +14 -0
  29. package/.agents/rules/persona-gstack-unfreeze.md +14 -0
  30. package/.agents/rules/persona-gstack.md +40 -0
  31. package/.agents/rules/recursive-identities.md +22 -0
  32. package/.agents/workflows/autoplan.md +30 -0
  33. package/.agents/workflows/benchmark.md +31 -0
  34. package/.agents/workflows/browse.md +26 -0
  35. package/.agents/workflows/canary.md +33 -0
  36. package/.agents/workflows/careful.md +22 -0
  37. package/.agents/workflows/codex.md +36 -0
  38. package/.agents/workflows/cso.md +29 -0
  39. package/.agents/workflows/design-consultation.md +28 -0
  40. package/.agents/workflows/design-review.md +28 -0
  41. package/.agents/workflows/document-release.md +32 -0
  42. package/.agents/workflows/freeze.md +17 -0
  43. package/.agents/workflows/gstack-upgrade.md +54 -0
  44. package/.agents/workflows/gstack.md +56 -0
  45. package/.agents/workflows/guard.md +18 -0
  46. package/.agents/workflows/investigate.md +37 -0
  47. package/.agents/workflows/land-and-deploy.md +35 -0
  48. package/.agents/workflows/office-hours.md +27 -0
  49. package/.agents/workflows/plan-ceo-review.md +34 -0
  50. package/.agents/workflows/plan-design-review.md +31 -0
  51. package/.agents/workflows/plan-eng-review.md +28 -0
  52. package/.agents/workflows/qa-only.md +28 -0
  53. package/.agents/workflows/qa.md +73 -0
  54. package/.agents/workflows/retro.md +34 -0
  55. package/.agents/workflows/review.md +30 -0
  56. package/.agents/workflows/setup-browser-cookies.md +15 -0
  57. package/.agents/workflows/setup-cookies.md +8 -0
  58. package/.agents/workflows/setup-deploy.md +21 -0
  59. package/.agents/workflows/ship.md +93 -0
  60. package/.agents/workflows/unfreeze.md +12 -0
  61. package/LICENSE +22 -0
  62. package/README.md +189 -0
  63. package/README_KO.md +191 -0
  64. package/bin/install.js +105 -0
  65. package/gstack-origin/.agents/skills/gstack/SKILL.md +651 -0
  66. package/gstack-origin/.agents/skills/gstack-autoplan/SKILL.md +678 -0
  67. package/gstack-origin/.agents/skills/gstack-benchmark/SKILL.md +482 -0
  68. package/gstack-origin/.agents/skills/gstack-browse/SKILL.md +511 -0
  69. package/gstack-origin/.agents/skills/gstack-canary/SKILL.md +486 -0
  70. package/gstack-origin/.agents/skills/gstack-careful/SKILL.md +50 -0
  71. package/gstack-origin/.agents/skills/gstack-cso/SKILL.md +607 -0
  72. package/gstack-origin/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
  73. package/gstack-origin/.agents/skills/gstack-design-review/SKILL.md +988 -0
  74. package/gstack-origin/.agents/skills/gstack-document-release/SKILL.md +604 -0
  75. package/gstack-origin/.agents/skills/gstack-freeze/SKILL.md +67 -0
  76. package/gstack-origin/.agents/skills/gstack-guard/SKILL.md +62 -0
  77. package/gstack-origin/.agents/skills/gstack-investigate/SKILL.md +415 -0
  78. package/gstack-origin/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
  79. package/gstack-origin/.agents/skills/gstack-office-hours/SKILL.md +986 -0
  80. package/gstack-origin/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
  81. package/gstack-origin/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
  82. package/gstack-origin/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
  83. package/gstack-origin/.agents/skills/gstack-qa/SKILL.md +1006 -0
  84. package/gstack-origin/.agents/skills/gstack-qa-only/SKILL.md +626 -0
  85. package/gstack-origin/.agents/skills/gstack-retro/SKILL.md +1065 -0
  86. package/gstack-origin/.agents/skills/gstack-review/SKILL.md +704 -0
  87. package/gstack-origin/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
  88. package/gstack-origin/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
  89. package/gstack-origin/.agents/skills/gstack-ship/SKILL.md +1312 -0
  90. package/gstack-origin/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
  91. package/gstack-origin/.agents/skills/gstack-upgrade/SKILL.md +220 -0
  92. package/gstack-origin/.env.example +5 -0
  93. package/gstack-origin/.github/workflows/skill-docs.yml +17 -0
  94. package/gstack-origin/AGENTS.md +49 -0
  95. package/gstack-origin/ARCHITECTURE.md +359 -0
  96. package/gstack-origin/BROWSER.md +271 -0
  97. package/gstack-origin/CHANGELOG.md +800 -0
  98. package/gstack-origin/CLAUDE.md +284 -0
  99. package/gstack-origin/CONTRIBUTING.md +370 -0
  100. package/gstack-origin/ETHOS.md +129 -0
  101. package/gstack-origin/LICENSE +21 -0
  102. package/gstack-origin/README.md +228 -0
  103. package/gstack-origin/SKILL.md +657 -0
  104. package/gstack-origin/SKILL.md.tmpl +281 -0
  105. package/gstack-origin/TODOS.md +564 -0
  106. package/gstack-origin/VERSION +1 -0
  107. package/gstack-origin/autoplan/SKILL.md +689 -0
  108. package/gstack-origin/autoplan/SKILL.md.tmpl +416 -0
  109. package/gstack-origin/benchmark/SKILL.md +489 -0
  110. package/gstack-origin/benchmark/SKILL.md.tmpl +233 -0
  111. package/gstack-origin/bin/dev-setup +68 -0
  112. package/gstack-origin/bin/dev-teardown +56 -0
  113. package/gstack-origin/bin/gstack-analytics +191 -0
  114. package/gstack-origin/bin/gstack-community-dashboard +113 -0
  115. package/gstack-origin/bin/gstack-config +38 -0
  116. package/gstack-origin/bin/gstack-diff-scope +71 -0
  117. package/gstack-origin/bin/gstack-global-discover.ts +591 -0
  118. package/gstack-origin/bin/gstack-repo-mode +93 -0
  119. package/gstack-origin/bin/gstack-review-log +9 -0
  120. package/gstack-origin/bin/gstack-review-read +12 -0
  121. package/gstack-origin/bin/gstack-slug +15 -0
  122. package/gstack-origin/bin/gstack-telemetry-log +158 -0
  123. package/gstack-origin/bin/gstack-telemetry-sync +127 -0
  124. package/gstack-origin/bin/gstack-update-check +196 -0
  125. package/gstack-origin/browse/SKILL.md +517 -0
  126. package/gstack-origin/browse/SKILL.md.tmpl +141 -0
  127. package/gstack-origin/browse/bin/find-browse +21 -0
  128. package/gstack-origin/browse/bin/remote-slug +14 -0
  129. package/gstack-origin/browse/scripts/build-node-server.sh +48 -0
  130. package/gstack-origin/browse/src/browser-manager.ts +634 -0
  131. package/gstack-origin/browse/src/buffers.ts +137 -0
  132. package/gstack-origin/browse/src/bun-polyfill.cjs +109 -0
  133. package/gstack-origin/browse/src/cli.ts +420 -0
  134. package/gstack-origin/browse/src/commands.ts +111 -0
  135. package/gstack-origin/browse/src/config.ts +150 -0
  136. package/gstack-origin/browse/src/cookie-import-browser.ts +417 -0
  137. package/gstack-origin/browse/src/cookie-picker-routes.ts +207 -0
  138. package/gstack-origin/browse/src/cookie-picker-ui.ts +541 -0
  139. package/gstack-origin/browse/src/find-browse.ts +61 -0
  140. package/gstack-origin/browse/src/meta-commands.ts +269 -0
  141. package/gstack-origin/browse/src/platform.ts +17 -0
  142. package/gstack-origin/browse/src/read-commands.ts +335 -0
  143. package/gstack-origin/browse/src/server.ts +369 -0
  144. package/gstack-origin/browse/src/snapshot.ts +398 -0
  145. package/gstack-origin/browse/src/url-validation.ts +91 -0
  146. package/gstack-origin/browse/src/write-commands.ts +352 -0
  147. package/gstack-origin/browse/test/bun-polyfill.test.ts +72 -0
  148. package/gstack-origin/browse/test/commands.test.ts +1836 -0
  149. package/gstack-origin/browse/test/config.test.ts +250 -0
  150. package/gstack-origin/browse/test/cookie-import-browser.test.ts +397 -0
  151. package/gstack-origin/browse/test/cookie-picker-routes.test.ts +205 -0
  152. package/gstack-origin/browse/test/find-browse.test.ts +50 -0
  153. package/gstack-origin/browse/test/fixtures/basic.html +33 -0
  154. package/gstack-origin/browse/test/fixtures/cursor-interactive.html +22 -0
  155. package/gstack-origin/browse/test/fixtures/dialog.html +15 -0
  156. package/gstack-origin/browse/test/fixtures/empty.html +2 -0
  157. package/gstack-origin/browse/test/fixtures/forms.html +55 -0
  158. package/gstack-origin/browse/test/fixtures/qa-eval-checkout.html +108 -0
  159. package/gstack-origin/browse/test/fixtures/qa-eval-spa.html +98 -0
  160. package/gstack-origin/browse/test/fixtures/qa-eval.html +51 -0
  161. package/gstack-origin/browse/test/fixtures/responsive.html +49 -0
  162. package/gstack-origin/browse/test/fixtures/snapshot.html +55 -0
  163. package/gstack-origin/browse/test/fixtures/spa.html +24 -0
  164. package/gstack-origin/browse/test/fixtures/states.html +17 -0
  165. package/gstack-origin/browse/test/fixtures/upload.html +25 -0
  166. package/gstack-origin/browse/test/gstack-config.test.ts +125 -0
  167. package/gstack-origin/browse/test/gstack-update-check.test.ts +467 -0
  168. package/gstack-origin/browse/test/handoff.test.ts +235 -0
  169. package/gstack-origin/browse/test/path-validation.test.ts +63 -0
  170. package/gstack-origin/browse/test/platform.test.ts +37 -0
  171. package/gstack-origin/browse/test/snapshot.test.ts +467 -0
  172. package/gstack-origin/browse/test/test-server.ts +57 -0
  173. package/gstack-origin/browse/test/url-validation.test.ts +72 -0
  174. package/gstack-origin/canary/SKILL.md +493 -0
  175. package/gstack-origin/canary/SKILL.md.tmpl +220 -0
  176. package/gstack-origin/careful/SKILL.md +59 -0
  177. package/gstack-origin/careful/SKILL.md.tmpl +57 -0
  178. package/gstack-origin/careful/bin/check-careful.sh +112 -0
  179. package/gstack-origin/codex/SKILL.md +677 -0
  180. package/gstack-origin/codex/SKILL.md.tmpl +356 -0
  181. package/gstack-origin/conductor.json +6 -0
  182. package/gstack-origin/cso/SKILL.md +615 -0
  183. package/gstack-origin/cso/SKILL.md.tmpl +376 -0
  184. package/gstack-origin/design-consultation/SKILL.md +625 -0
  185. package/gstack-origin/design-consultation/SKILL.md.tmpl +369 -0
  186. package/gstack-origin/design-review/SKILL.md +998 -0
  187. package/gstack-origin/design-review/SKILL.md.tmpl +262 -0
  188. package/gstack-origin/docs/images/github-2013.png +0 -0
  189. package/gstack-origin/docs/images/github-2026.png +0 -0
  190. package/gstack-origin/docs/skills.md +877 -0
  191. package/gstack-origin/document-release/SKILL.md +613 -0
  192. package/gstack-origin/document-release/SKILL.md.tmpl +357 -0
  193. package/gstack-origin/freeze/SKILL.md +82 -0
  194. package/gstack-origin/freeze/SKILL.md.tmpl +80 -0
  195. package/gstack-origin/freeze/bin/check-freeze.sh +68 -0
  196. package/gstack-origin/gstack-upgrade/SKILL.md +226 -0
  197. package/gstack-origin/gstack-upgrade/SKILL.md.tmpl +224 -0
  198. package/gstack-origin/guard/SKILL.md +82 -0
  199. package/gstack-origin/guard/SKILL.md.tmpl +80 -0
  200. package/gstack-origin/investigate/SKILL.md +435 -0
  201. package/gstack-origin/investigate/SKILL.md.tmpl +196 -0
  202. package/gstack-origin/land-and-deploy/SKILL.md +880 -0
  203. package/gstack-origin/land-and-deploy/SKILL.md.tmpl +575 -0
  204. package/gstack-origin/office-hours/SKILL.md +996 -0
  205. package/gstack-origin/office-hours/SKILL.md.tmpl +624 -0
  206. package/gstack-origin/package.json +55 -0
  207. package/gstack-origin/plan-ceo-review/SKILL.md +1277 -0
  208. package/gstack-origin/plan-ceo-review/SKILL.md.tmpl +838 -0
  209. package/gstack-origin/plan-design-review/SKILL.md +676 -0
  210. package/gstack-origin/plan-design-review/SKILL.md.tmpl +314 -0
  211. package/gstack-origin/plan-eng-review/SKILL.md +836 -0
  212. package/gstack-origin/plan-eng-review/SKILL.md.tmpl +279 -0
  213. package/gstack-origin/qa/SKILL.md +1016 -0
  214. package/gstack-origin/qa/SKILL.md.tmpl +316 -0
  215. package/gstack-origin/qa/references/issue-taxonomy.md +85 -0
  216. package/gstack-origin/qa/templates/qa-report-template.md +126 -0
  217. package/gstack-origin/qa-only/SKILL.md +633 -0
  218. package/gstack-origin/qa-only/SKILL.md.tmpl +101 -0
  219. package/gstack-origin/retro/SKILL.md +1072 -0
  220. package/gstack-origin/retro/SKILL.md.tmpl +833 -0
  221. package/gstack-origin/review/SKILL.md +849 -0
  222. package/gstack-origin/review/SKILL.md.tmpl +259 -0
  223. package/gstack-origin/review/TODOS-format.md +62 -0
  224. package/gstack-origin/review/checklist.md +190 -0
  225. package/gstack-origin/review/design-checklist.md +132 -0
  226. package/gstack-origin/review/greptile-triage.md +220 -0
  227. package/gstack-origin/scripts/analytics.ts +190 -0
  228. package/gstack-origin/scripts/dev-skill.ts +82 -0
  229. package/gstack-origin/scripts/eval-compare.ts +96 -0
  230. package/gstack-origin/scripts/eval-list.ts +116 -0
  231. package/gstack-origin/scripts/eval-select.ts +86 -0
  232. package/gstack-origin/scripts/eval-summary.ts +187 -0
  233. package/gstack-origin/scripts/eval-watch.ts +172 -0
  234. package/gstack-origin/scripts/gen-skill-docs.ts +2414 -0
  235. package/gstack-origin/scripts/skill-check.ts +167 -0
  236. package/gstack-origin/setup +269 -0
  237. package/gstack-origin/setup-browser-cookies/SKILL.md +330 -0
  238. package/gstack-origin/setup-browser-cookies/SKILL.md.tmpl +74 -0
  239. package/gstack-origin/setup-deploy/SKILL.md +459 -0
  240. package/gstack-origin/setup-deploy/SKILL.md.tmpl +220 -0
  241. package/gstack-origin/ship/SKILL.md +1457 -0
  242. package/gstack-origin/ship/SKILL.md.tmpl +528 -0
  243. package/gstack-origin/supabase/config.sh +10 -0
  244. package/gstack-origin/supabase/functions/community-pulse/index.ts +59 -0
  245. package/gstack-origin/supabase/functions/telemetry-ingest/index.ts +135 -0
  246. package/gstack-origin/supabase/functions/update-check/index.ts +37 -0
  247. package/gstack-origin/supabase/migrations/001_telemetry.sql +89 -0
  248. package/gstack-origin/test/analytics.test.ts +277 -0
  249. package/gstack-origin/test/codex-e2e.test.ts +197 -0
  250. package/gstack-origin/test/fixtures/coverage-audit-fixture.ts +76 -0
  251. package/gstack-origin/test/fixtures/eval-baselines.json +7 -0
  252. package/gstack-origin/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
  253. package/gstack-origin/test/fixtures/qa-eval-ground-truth.json +43 -0
  254. package/gstack-origin/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
  255. package/gstack-origin/test/fixtures/review-eval-design-slop.css +86 -0
  256. package/gstack-origin/test/fixtures/review-eval-design-slop.html +41 -0
  257. package/gstack-origin/test/fixtures/review-eval-enum-diff.rb +30 -0
  258. package/gstack-origin/test/fixtures/review-eval-enum.rb +27 -0
  259. package/gstack-origin/test/fixtures/review-eval-vuln.rb +14 -0
  260. package/gstack-origin/test/gemini-e2e.test.ts +173 -0
  261. package/gstack-origin/test/gen-skill-docs.test.ts +1049 -0
  262. package/gstack-origin/test/global-discover.test.ts +187 -0
  263. package/gstack-origin/test/helpers/codex-session-runner.ts +282 -0
  264. package/gstack-origin/test/helpers/e2e-helpers.ts +239 -0
  265. package/gstack-origin/test/helpers/eval-store.test.ts +548 -0
  266. package/gstack-origin/test/helpers/eval-store.ts +689 -0
  267. package/gstack-origin/test/helpers/gemini-session-runner.test.ts +104 -0
  268. package/gstack-origin/test/helpers/gemini-session-runner.ts +201 -0
  269. package/gstack-origin/test/helpers/llm-judge.ts +130 -0
  270. package/gstack-origin/test/helpers/observability.test.ts +283 -0
  271. package/gstack-origin/test/helpers/session-runner.test.ts +96 -0
  272. package/gstack-origin/test/helpers/session-runner.ts +357 -0
  273. package/gstack-origin/test/helpers/skill-parser.ts +206 -0
  274. package/gstack-origin/test/helpers/touchfiles.ts +260 -0
  275. package/gstack-origin/test/hook-scripts.test.ts +373 -0
  276. package/gstack-origin/test/skill-e2e-browse.test.ts +293 -0
  277. package/gstack-origin/test/skill-e2e-deploy.test.ts +279 -0
  278. package/gstack-origin/test/skill-e2e-design.test.ts +614 -0
  279. package/gstack-origin/test/skill-e2e-plan.test.ts +538 -0
  280. package/gstack-origin/test/skill-e2e-qa-bugs.test.ts +194 -0
  281. package/gstack-origin/test/skill-e2e-qa-workflow.test.ts +412 -0
  282. package/gstack-origin/test/skill-e2e-review.test.ts +535 -0
  283. package/gstack-origin/test/skill-e2e-workflow.test.ts +586 -0
  284. package/gstack-origin/test/skill-e2e.test.ts +3325 -0
  285. package/gstack-origin/test/skill-llm-eval.test.ts +787 -0
  286. package/gstack-origin/test/skill-parser.test.ts +179 -0
  287. package/gstack-origin/test/skill-routing-e2e.test.ts +605 -0
  288. package/gstack-origin/test/skill-validation.test.ts +1520 -0
  289. package/gstack-origin/test/telemetry.test.ts +278 -0
  290. package/gstack-origin/test/touchfiles.test.ts +262 -0
  291. package/gstack-origin/unfreeze/SKILL.md +40 -0
  292. package/gstack-origin/unfreeze/SKILL.md.tmpl +38 -0
  293. package/package.json +38 -0
  294. package/scripts/install-antigravity-skill.ps1 +33 -0
  295. package/scripts/install-antigravity-skill.sh +41 -0
  296. package/scripts/sync-gstack-origin.ps1 +37 -0
  297. package/scripts/sync-gstack-origin.sh +35 -0
@@ -0,0 +1,359 @@
1
+ # Architecture
2
+
3
+ This document explains **why** gstack is built the way it is. For setup and commands, see CLAUDE.md. For contributing, see CONTRIBUTING.md.
4
+
5
+ ## The core idea
6
+
7
+ gstack gives Claude Code a persistent browser and a set of opinionated workflow skills. The browser is the hard part — everything else is Markdown.
8
+
9
+ The key insight: an AI agent interacting with a browser needs **sub-second latency** and **persistent state**. If every command cold-starts a browser, you're waiting 3-5 seconds per tool call. If the browser dies between commands, you lose cookies, tabs, and login sessions. So gstack runs a long-lived Chromium daemon that the CLI talks to over localhost HTTP.
10
+
11
+ ```
12
+ Claude Code gstack
13
+ ───────── ──────
14
+ ┌──────────────────────┐
15
+ Tool call: $B snapshot -i │ CLI (compiled binary)│
16
+ ─────────────────────────→ │ • reads state file │
17
+ │ • POST /command │
18
+ │ to localhost:PORT │
19
+ └──────────┬───────────┘
20
+ │ HTTP
21
+ ┌──────────▼───────────┐
22
+ │ Server (Bun.serve) │
23
+ │ • dispatches command │
24
+ │ • talks to Chromium │
25
+ │ • returns plain text │
26
+ └──────────┬───────────┘
27
+ │ CDP
28
+ ┌──────────▼───────────┐
29
+ │ Chromium (headless) │
30
+ │ • persistent tabs │
31
+ │ • cookies carry over │
32
+ │ • 30min idle timeout │
33
+ └───────────────────────┘
34
+ ```
35
+
36
+ First call starts everything (~3s). Every call after: ~100-200ms.
37
+
38
+ ## Why Bun
39
+
40
+ Node.js would work. Bun is better here for three reasons:
41
+
42
+ 1. **Compiled binaries.** `bun build --compile` produces a single ~58MB executable. No `node_modules` at runtime, no `npx`, no PATH configuration. The binary just runs. This matters because gstack installs into `~/.claude/skills/` where users don't expect to manage a Node.js project.
43
+
44
+ 2. **Native SQLite.** Cookie decryption reads Chromium's SQLite cookie database directly. Bun has `new Database()` built in — no `better-sqlite3`, no native addon compilation, no gyp. One less thing that breaks on different machines.
45
+
46
+ 3. **Native TypeScript.** The server runs as `bun run server.ts` during development. No compilation step, no `ts-node`, no source maps to debug. The compiled binary is for deployment; source files are for development.
47
+
48
+ 4. **Built-in HTTP server.** `Bun.serve()` is fast, simple, and doesn't need Express or Fastify. The server handles ~10 routes total. A framework would be overhead.
49
+
50
+ The bottleneck is always Chromium, not the CLI or server. Bun's startup speed (~1ms for the compiled binary vs ~100ms for Node) is nice but not the reason we chose it. The compiled binary and native SQLite are.
51
+
52
+ ## The daemon model
53
+
54
+ ### Why not start a browser per command?
55
+
56
+ Playwright can launch Chromium in ~2-3 seconds. For a single screenshot, that's fine. For a QA session with 20+ commands, it's 40+ seconds of browser startup overhead. Worse: you lose all state between commands. Cookies, localStorage, login sessions, open tabs — all gone.
57
+
58
+ The daemon model means:
59
+
60
+ - **Persistent state.** Log in once, stay logged in. Open a tab, it stays open. localStorage persists across commands.
61
+ - **Sub-second commands.** After the first call, every command is just an HTTP POST. ~100-200ms round-trip including Chromium's work.
62
+ - **Automatic lifecycle.** The server auto-starts on first use, auto-shuts down after 30 minutes idle. No process management needed.
63
+
64
+ ### State file
65
+
66
+ The server writes `.gstack/browse.json` (atomic write via tmp + rename, mode 0o600):
67
+
68
+ ```json
69
+ { "pid": 12345, "port": 34567, "token": "uuid-v4", "startedAt": "...", "binaryVersion": "abc123" }
70
+ ```
71
+
72
+ 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.
73
+
74
+ ### Port selection
75
+
76
+ Random port between 10000-60000 (retry up to 5 on collision). This means 10 Conductor workspaces can each run their own browse daemon with zero configuration and zero port conflicts. The old approach (scanning 9400-9409) broke constantly in multi-workspace setups.
77
+
78
+ ### Version auto-restart
79
+
80
+ The build writes `git rev-parse HEAD` to `browse/dist/.version`. On each CLI invocation, if the binary's version doesn't match the running server's `binaryVersion`, the CLI kills the old server and starts a new one. This prevents the "stale binary" class of bugs entirely — rebuild the binary, next command picks it up automatically.
81
+
82
+ ## Security model
83
+
84
+ ### Localhost only
85
+
86
+ The HTTP server binds to `localhost`, not `0.0.0.0`. It's not reachable from the network.
87
+
88
+ ### Bearer token auth
89
+
90
+ Every server session generates a random UUID token, written to the state file with mode 0o600 (owner-only read). Every HTTP request must include `Authorization: Bearer <token>`. If the token doesn't match, the server returns 401.
91
+
92
+ This prevents other processes on the same machine from talking to your browse server. The cookie picker UI (`/cookie-picker`) and health check (`/health`) are exempt — they're localhost-only and don't execute commands.
93
+
94
+ ### Cookie security
95
+
96
+ Cookies are the most sensitive data gstack handles. The design:
97
+
98
+ 1. **Keychain access requires user approval.** First cookie import per browser triggers a macOS Keychain dialog. The user must click "Allow" or "Always Allow." gstack never silently accesses credentials.
99
+
100
+ 2. **Decryption happens in-process.** Cookie values are decrypted in memory (PBKDF2 + AES-128-CBC), loaded into the Playwright context, and never written to disk in plaintext. The cookie picker UI never displays cookie values — only domain names and counts.
101
+
102
+ 3. **Database is read-only.** gstack copies the Chromium cookie DB to a temp file (to avoid SQLite lock conflicts with the running browser) and opens it read-only. It never modifies your real browser's cookie database.
103
+
104
+ 4. **Key caching is per-session.** The Keychain password + derived AES key are cached in memory for the server's lifetime. When the server shuts down (idle timeout or explicit stop), the cache is gone.
105
+
106
+ 5. **No cookie values in logs.** Console, network, and dialog logs never contain cookie values. The `cookies` command outputs cookie metadata (domain, name, expiry) but values are truncated.
107
+
108
+ ### Shell injection prevention
109
+
110
+ The browser registry (Comet, Chrome, Arc, Brave, Edge) is hardcoded. Database paths are constructed from known constants, never from user input. Keychain access uses `Bun.spawn()` with explicit argument arrays, not shell string interpolation.
111
+
112
+ ## The ref system
113
+
114
+ Refs (`@e1`, `@e2`, `@c1`) are how the agent addresses page elements without writing CSS selectors or XPath.
115
+
116
+ ### How it works
117
+
118
+ ```
119
+ 1. Agent runs: $B snapshot -i
120
+ 2. Server calls Playwright's page.accessibility.snapshot()
121
+ 3. Parser walks the ARIA tree, assigns sequential refs: @e1, @e2, @e3...
122
+ 4. For each ref, builds a Playwright Locator: getByRole(role, { name }).nth(index)
123
+ 5. Stores Map<string, RefEntry> on the BrowserManager instance (role + name + Locator)
124
+ 6. Returns the annotated tree as plain text
125
+
126
+ Later:
127
+ 7. Agent runs: $B click @e3
128
+ 8. Server resolves @e3 → Locator → locator.click()
129
+ ```
130
+
131
+ ### Why Locators, not DOM mutation
132
+
133
+ The obvious approach is to inject `data-ref="@e1"` attributes into the DOM. This breaks on:
134
+
135
+ - **CSP (Content Security Policy).** Many production sites block DOM modification from scripts.
136
+ - **React/Vue/Svelte hydration.** Framework reconciliation can strip injected attributes.
137
+ - **Shadow DOM.** Can't reach inside shadow roots from the outside.
138
+
139
+ Playwright Locators are external to the DOM. They use the accessibility tree (which Chromium maintains internally) and `getByRole()` queries. No DOM mutation, no CSP issues, no framework conflicts.
140
+
141
+ ### Ref lifecycle
142
+
143
+ Refs are cleared on navigation (the `framenavigated` event on the main frame). This is correct — after navigation, all locators are stale. The agent must run `snapshot` again to get fresh refs. This is by design: stale refs should fail loudly, not click the wrong element.
144
+
145
+ ### Ref staleness detection
146
+
147
+ SPAs can mutate the DOM without triggering `framenavigated` (e.g. React router transitions, tab switches, modal opens). This makes refs stale even though the page URL didn't change. To catch this, `resolveRef()` performs an async `count()` check before using any ref:
148
+
149
+ ```
150
+ resolveRef(@e3) → entry = refMap.get("e3")
151
+ → count = await entry.locator.count()
152
+ → if count === 0: throw "Ref @e3 is stale — element no longer exists. Run 'snapshot' to get fresh refs."
153
+ → if count > 0: return { locator }
154
+ ```
155
+
156
+ This fails fast (~5ms overhead) instead of letting Playwright's 30-second action timeout expire on a missing element. The `RefEntry` stores `role` and `name` metadata alongside the Locator so the error message can tell the agent what the element was.
157
+
158
+ ### Cursor-interactive refs (@c)
159
+
160
+ The `-C` flag finds elements that are clickable but not in the ARIA tree — things styled with `cursor: pointer`, elements with `onclick` attributes, or custom `tabindex`. These get `@c1`, `@c2` refs in a separate namespace. This catches custom components that frameworks render as `<div>` but are actually buttons.
161
+
162
+ ## Logging architecture
163
+
164
+ Three ring buffers (50,000 entries each, O(1) push):
165
+
166
+ ```
167
+ Browser events → CircularBuffer (in-memory) → Async flush to .gstack/*.log
168
+ ```
169
+
170
+ Console messages, network requests, and dialog events each have their own buffer. Flushing happens every 1 second — the server appends only new entries since the last flush. This means:
171
+
172
+ - HTTP request handling is never blocked by disk I/O
173
+ - Logs survive server crashes (up to 1 second of data loss)
174
+ - Memory is bounded (50K entries × 3 buffers)
175
+ - Disk files are append-only, readable by external tools
176
+
177
+ The `console`, `network`, and `dialog` commands read from the in-memory buffers, not disk. Disk files are for post-mortem debugging.
178
+
179
+ ## SKILL.md template system
180
+
181
+ ### The problem
182
+
183
+ SKILL.md files tell Claude how to use the browse commands. If the docs list a flag that doesn't exist, or miss a command that was added, the agent hits errors. Hand-maintained docs always drift from code.
184
+
185
+ ### The solution
186
+
187
+ ```
188
+ SKILL.md.tmpl (human-written prose + placeholders)
189
+
190
+ gen-skill-docs.ts (reads source code metadata)
191
+
192
+ SKILL.md (committed, auto-generated sections)
193
+ ```
194
+
195
+ Templates contain the workflows, tips, and examples that require human judgment. Placeholders are filled from source code at build time:
196
+
197
+ | Placeholder | Source | What it generates |
198
+ |-------------|--------|-------------------|
199
+ | `{{COMMAND_REFERENCE}}` | `commands.ts` | Categorized command table |
200
+ | `{{SNAPSHOT_FLAGS}}` | `snapshot.ts` | Flag reference with examples |
201
+ | `{{PREAMBLE}}` | `gen-skill-docs.ts` | Startup block: update check, session tracking, contributor mode, AskUserQuestion format |
202
+ | `{{BROWSE_SETUP}}` | `gen-skill-docs.ts` | Binary discovery + setup instructions |
203
+ | `{{BASE_BRANCH_DETECT}}` | `gen-skill-docs.ts` | Dynamic base branch detection for PR-targeting skills (ship, review, qa, plan-ceo-review) |
204
+ | `{{QA_METHODOLOGY}}` | `gen-skill-docs.ts` | Shared QA methodology block for /qa and /qa-only |
205
+ | `{{DESIGN_METHODOLOGY}}` | `gen-skill-docs.ts` | Shared design audit methodology for /plan-design-review and /design-review |
206
+ | `{{REVIEW_DASHBOARD}}` | `gen-skill-docs.ts` | Review Readiness Dashboard for /ship pre-flight |
207
+ | `{{TEST_BOOTSTRAP}}` | `gen-skill-docs.ts` | Test framework detection, bootstrap, CI/CD setup for /qa, /ship, /design-review |
208
+
209
+ This is structurally sound — if a command exists in code, it appears in docs. If it doesn't exist, it can't appear.
210
+
211
+ ### The preamble
212
+
213
+ Every skill starts with a `{{PREAMBLE}}` block that runs before the skill's own logic. It handles five things in a single bash command:
214
+
215
+ 1. **Update check** — calls `gstack-update-check`, reports if an upgrade is available.
216
+ 2. **Session tracking** — touches `~/.gstack/sessions/$PPID` and counts active sessions (files modified in the last 2 hours). When 3+ sessions are running, all skills enter "ELI16 mode" — every question re-grounds the user on context because they're juggling windows.
217
+ 3. **Contributor mode** — reads `gstack_contributor` from config. When true, the agent files casual field reports to `~/.gstack/contributor-logs/` when gstack itself misbehaves.
218
+ 4. **AskUserQuestion format** — universal format: context, question, `RECOMMENDATION: Choose X because ___`, lettered options. Consistent across all skills.
219
+ 5. **Search Before Building** — before building infrastructure or unfamiliar patterns, search first. Three layers of knowledge: tried-and-true (Layer 1), new-and-popular (Layer 2), first-principles (Layer 3). When first-principles reasoning reveals conventional wisdom is wrong, the agent names the "eureka moment" and logs it. See `ETHOS.md` for the full builder philosophy.
220
+
221
+ ### Why committed, not generated at runtime?
222
+
223
+ Three reasons:
224
+
225
+ 1. **Claude reads SKILL.md at skill load time.** There's no build step when a user invokes `/browse`. The file must already exist and be correct.
226
+ 2. **CI can validate freshness.** `gen:skill-docs --dry-run` + `git diff --exit-code` catches stale docs before merge.
227
+ 3. **Git blame works.** You can see when a command was added and in which commit.
228
+
229
+ ### Template test tiers
230
+
231
+ | Tier | What | Cost | Speed |
232
+ |------|------|------|-------|
233
+ | 1 — Static validation | Parse every `$B` command in SKILL.md, validate against registry | Free | <2s |
234
+ | 2 — E2E via `claude -p` | Spawn real Claude session, run each skill, check for errors | ~$3.85 | ~20min |
235
+ | 3 — LLM-as-judge | Sonnet scores docs on clarity/completeness/actionability | ~$0.15 | ~30s |
236
+
237
+ Tier 1 runs on every `bun test`. Tiers 2+3 are gated behind `EVALS=1`. The idea is: catch 95% of issues for free, use LLMs only for judgment calls.
238
+
239
+ ## Command dispatch
240
+
241
+ Commands are categorized by side effects:
242
+
243
+ - **READ** (text, html, links, console, cookies, ...): No mutations. Safe to retry. Returns page state.
244
+ - **WRITE** (goto, click, fill, press, ...): Mutates page state. Not idempotent.
245
+ - **META** (snapshot, screenshot, tabs, chain, ...): Server-level operations that don't fit neatly into read/write.
246
+
247
+ This isn't just organizational. The server uses it for dispatch:
248
+
249
+ ```typescript
250
+ if (READ_COMMANDS.has(cmd)) → handleReadCommand(cmd, args, bm)
251
+ if (WRITE_COMMANDS.has(cmd)) → handleWriteCommand(cmd, args, bm)
252
+ if (META_COMMANDS.has(cmd)) → handleMetaCommand(cmd, args, bm, shutdown)
253
+ ```
254
+
255
+ The `help` command returns all three sets so agents can self-discover available commands.
256
+
257
+ ## Error philosophy
258
+
259
+ Errors are for AI agents, not humans. Every error message must be actionable:
260
+
261
+ - "Element not found" → "Element not found or not interactable. Run `snapshot -i` to see available elements."
262
+ - "Selector matched multiple elements" → "Selector matched multiple elements. Use @refs from `snapshot` instead."
263
+ - Timeout → "Navigation timed out after 30s. The page may be slow or the URL may be wrong."
264
+
265
+ Playwright's native errors are rewritten through `wrapError()` to strip internal stack traces and add guidance. The agent should be able to read the error and know what to do next without human intervention.
266
+
267
+ ### Crash recovery
268
+
269
+ The server doesn't try to self-heal. If Chromium crashes (`browser.on('disconnected')`), the server exits immediately. The CLI detects the dead server on the next command and auto-restarts. This is simpler and more reliable than trying to reconnect to a half-dead browser process.
270
+
271
+ ## E2E test infrastructure
272
+
273
+ ### Session runner (`test/helpers/session-runner.ts`)
274
+
275
+ E2E tests spawn `claude -p` as a completely independent subprocess — not via the Agent SDK, which can't nest inside Claude Code sessions. The runner:
276
+
277
+ 1. Writes the prompt to a temp file (avoids shell escaping issues)
278
+ 2. Spawns `sh -c 'cat prompt | claude -p --output-format stream-json --verbose'`
279
+ 3. Streams NDJSON from stdout for real-time progress
280
+ 4. Races against a configurable timeout
281
+ 5. Parses the full NDJSON transcript into structured results
282
+
283
+ The `parseNDJSON()` function is pure — no I/O, no side effects — making it independently testable.
284
+
285
+ ### Observability data flow
286
+
287
+ ```
288
+ skill-e2e-*.test.ts
289
+
290
+ │ generates runId, passes testName + runId to each call
291
+
292
+ ┌─────┼──────────────────────────────┐
293
+ │ │ │
294
+ │ runSkillTest() evalCollector
295
+ │ (session-runner.ts) (eval-store.ts)
296
+ │ │ │
297
+ │ per tool call: per addTest():
298
+ │ ┌──┼──────────┐ savePartial()
299
+ │ │ │ │ │
300
+ │ ▼ ▼ ▼ ▼
301
+ │ [HB] [PL] [NJ] _partial-e2e.json
302
+ │ │ │ │ (atomic overwrite)
303
+ │ │ │ │
304
+ │ ▼ ▼ ▼
305
+ │ e2e- prog- {name}
306
+ │ live ress .ndjson
307
+ │ .json .log
308
+
309
+ │ on failure:
310
+ │ {name}-failure.json
311
+
312
+ │ ALL files in ~/.gstack-dev/
313
+ │ Run dir: e2e-runs/{runId}/
314
+
315
+ │ eval-watch.ts
316
+ │ │
317
+ │ ┌─────┴─────┐
318
+ │ read HB read partial
319
+ │ └─────┬─────┘
320
+ │ ▼
321
+ │ render dashboard
322
+ │ (stale >10min? warn)
323
+ ```
324
+
325
+ **Split ownership:** session-runner owns the heartbeat (current test state), eval-store owns partial results (completed test state). The watcher reads both. Neither component knows about the other — they share data only through the filesystem.
326
+
327
+ **Non-fatal everything:** All observability I/O is wrapped in try/catch. A write failure never causes a test to fail. The tests themselves are the source of truth; observability is best-effort.
328
+
329
+ **Machine-readable diagnostics:** Each test result includes `exit_reason` (success, timeout, error_max_turns, error_api, exit_code_N), `timeout_at_turn`, and `last_tool_call`. This enables `jq` queries like:
330
+ ```bash
331
+ jq '.tests[] | select(.exit_reason == "timeout") | .last_tool_call' ~/.gstack-dev/evals/_partial-e2e.json
332
+ ```
333
+
334
+ ### Eval persistence (`test/helpers/eval-store.ts`)
335
+
336
+ The `EvalCollector` accumulates test results and writes them in two ways:
337
+
338
+ 1. **Incremental:** `savePartial()` writes `_partial-e2e.json` after each test (atomic: write `.tmp`, `fs.renameSync`). Survives kills.
339
+ 2. **Final:** `finalize()` writes a timestamped eval file (e.g. `e2e-20260314-143022.json`). The partial file is never cleaned up — it persists alongside the final file for observability.
340
+
341
+ `eval:compare` diffs two eval runs. `eval:summary` aggregates stats across all runs in `~/.gstack-dev/evals/`.
342
+
343
+ ### Test tiers
344
+
345
+ | Tier | What | Cost | Speed |
346
+ |------|------|------|-------|
347
+ | 1 — Static validation | Parse `$B` commands, validate against registry, observability unit tests | Free | <5s |
348
+ | 2 — E2E via `claude -p` | Spawn real Claude session, run each skill, scan for errors | ~$3.85 | ~20min |
349
+ | 3 — LLM-as-judge | Sonnet scores docs on clarity/completeness/actionability | ~$0.15 | ~30s |
350
+
351
+ Tier 1 runs on every `bun test`. Tiers 2+3 are gated behind `EVALS=1`. The idea: catch 95% of issues for free, use LLMs only for judgment calls and integration testing.
352
+
353
+ ## What's intentionally not here
354
+
355
+ - **No WebSocket streaming.** HTTP request/response is simpler, debuggable with curl, and fast enough. Streaming would add complexity for marginal benefit.
356
+ - **No MCP protocol.** MCP adds JSON schema overhead per request and requires a persistent connection. Plain HTTP + plain text output is lighter on tokens and easier to debug.
357
+ - **No multi-user support.** One server per workspace, one user. The token auth is defense-in-depth, not multi-tenancy.
358
+ - **No Windows/Linux cookie decryption.** macOS Keychain is the only supported credential store. Linux (GNOME Keyring/kwallet) and Windows (DPAPI) are architecturally possible but not implemented.
359
+ - **No iframe support.** Playwright can handle iframes but the ref system doesn't cross frame boundaries yet. This is the most-requested missing feature.
@@ -0,0 +1,271 @@
1
+ # Browser — technical details
2
+
3
+ This document covers the command reference and internals of gstack's headless browser.
4
+
5
+ ## Command reference
6
+
7
+ | Category | Commands | What for |
8
+ |----------|----------|----------|
9
+ | Navigate | `goto`, `back`, `forward`, `reload`, `url` | Get to a page |
10
+ | Read | `text`, `html`, `links`, `forms`, `accessibility` | Extract content |
11
+ | Snapshot | `snapshot [-i] [-c] [-d N] [-s sel] [-D] [-a] [-o] [-C]` | Get refs, diff, annotate |
12
+ | Interact | `click`, `fill`, `select`, `hover`, `type`, `press`, `scroll`, `wait`, `viewport`, `upload` | Use the page |
13
+ | Inspect | `js`, `eval`, `css`, `attrs`, `is`, `console`, `network`, `dialog`, `cookies`, `storage`, `perf` | Debug and verify |
14
+ | Visual | `screenshot [--viewport] [--clip x,y,w,h] [sel\|@ref] [path]`, `pdf`, `responsive` | See what Claude sees |
15
+ | Compare | `diff <url1> <url2>` | Spot differences between environments |
16
+ | Dialogs | `dialog-accept [text]`, `dialog-dismiss` | Control alert/confirm/prompt handling |
17
+ | Tabs | `tabs`, `tab`, `newtab`, `closetab` | Multi-page workflows |
18
+ | Cookies | `cookie-import`, `cookie-import-browser` | Import cookies from file or real browser |
19
+ | Multi-step | `chain` (JSON from stdin) | Batch commands in one call |
20
+ | Handoff | `handoff [reason]`, `resume` | Switch to visible Chrome for user takeover |
21
+
22
+ All selector arguments accept CSS selectors, `@e` refs after `snapshot`, or `@c` refs after `snapshot -C`. 50+ commands total plus cookie import.
23
+
24
+ ## How it works
25
+
26
+ gstack's browser is a compiled CLI binary that talks to a persistent local Chromium daemon over HTTP. The CLI is a thin client — it reads a state file, sends a command, and prints the response to stdout. The server does the real work via [Playwright](https://playwright.dev/).
27
+
28
+ ```
29
+ ┌─────────────────────────────────────────────────────────────────┐
30
+ │ Claude Code │
31
+ │ │
32
+ │ "browse goto https://staging.myapp.com" │
33
+ │ │ │
34
+ │ ▼ │
35
+ │ ┌──────────┐ HTTP POST ┌──────────────┐ │
36
+ │ │ browse │ ──────────────── │ Bun HTTP │ │
37
+ │ │ CLI │ localhost:rand │ server │ │
38
+ │ │ │ Bearer token │ │ │
39
+ │ │ compiled │ ◄────────────── │ Playwright │──── Chromium │
40
+ │ │ binary │ plain text │ API calls │ (headless) │
41
+ │ └──────────┘ └──────────────┘ │
42
+ │ ~1ms startup persistent daemon │
43
+ │ auto-starts on first call │
44
+ │ auto-stops after 30 min idle │
45
+ └─────────────────────────────────────────────────────────────────┘
46
+ ```
47
+
48
+ ### Lifecycle
49
+
50
+ 1. **First call**: CLI checks `.gstack/browse.json` (in the project root) for a running server. None found — it spawns `bun run browse/src/server.ts` in the background. The server launches headless Chromium via Playwright, picks a random port (10000-60000), generates a bearer token, writes the state file, and starts accepting HTTP requests. This takes ~3 seconds.
51
+
52
+ 2. **Subsequent calls**: CLI reads the state file, sends an HTTP POST with the bearer token, prints the response. ~100-200ms round trip.
53
+
54
+ 3. **Idle shutdown**: After 30 minutes with no commands, the server shuts down and cleans up the state file. Next call restarts it automatically.
55
+
56
+ 4. **Crash recovery**: If Chromium crashes, the server exits immediately (no self-healing — don't hide failure). The CLI detects the dead server on the next call and starts a fresh one.
57
+
58
+ ### Key components
59
+
60
+ ```
61
+ browse/
62
+ ├── src/
63
+ │ ├── cli.ts # Thin client — reads state file, sends HTTP, prints response
64
+ │ ├── server.ts # Bun.serve HTTP server — routes commands to Playwright
65
+ │ ├── browser-manager.ts # Chromium lifecycle — launch, tabs, ref map, crash handling
66
+ │ ├── snapshot.ts # Accessibility tree → @ref assignment → Locator map + diff/annotate/-C
67
+ │ ├── read-commands.ts # Non-mutating commands (text, html, links, js, css, is, dialog, etc.)
68
+ │ ├── write-commands.ts # Mutating commands (click, fill, select, upload, dialog-accept, etc.)
69
+ │ ├── meta-commands.ts # Server management, chain, diff, snapshot routing
70
+ │ ├── cookie-import-browser.ts # Decrypt + import cookies from real Chromium browsers
71
+ │ ├── cookie-picker-routes.ts # HTTP routes for interactive cookie picker UI
72
+ │ ├── cookie-picker-ui.ts # Self-contained HTML/CSS/JS for cookie picker
73
+ │ └── buffers.ts # CircularBuffer<T> + console/network/dialog capture
74
+ ├── test/ # Integration tests + HTML fixtures
75
+ └── dist/
76
+ └── browse # Compiled binary (~58MB, Bun --compile)
77
+ ```
78
+
79
+ ### The snapshot system
80
+
81
+ The browser's key innovation is ref-based element selection, built on Playwright's accessibility tree API:
82
+
83
+ 1. `page.locator(scope).ariaSnapshot()` returns a YAML-like accessibility tree
84
+ 2. The snapshot parser assigns refs (`@e1`, `@e2`, ...) to each element
85
+ 3. For each ref, it builds a Playwright `Locator` (using `getByRole` + nth-child)
86
+ 4. The ref-to-Locator map is stored on `BrowserManager`
87
+ 5. Later commands like `click @e3` look up the Locator and call `locator.click()`
88
+
89
+ No DOM mutation. No injected scripts. Just Playwright's native accessibility API.
90
+
91
+ **Ref staleness detection:** SPAs can mutate the DOM without navigation (React router, tab switches, modals). When this happens, refs collected from a previous `snapshot` may point to elements that no longer exist. To handle this, `resolveRef()` runs an async `count()` check before using any ref — if the element count is 0, it throws immediately with a message telling the agent to re-run `snapshot`. This fails fast (~5ms) instead of waiting for Playwright's 30-second action timeout.
92
+
93
+ **Extended snapshot features:**
94
+ - `--diff` (`-D`): Stores each snapshot as a baseline. On the next `-D` call, returns a unified diff showing what changed. Use this to verify that an action (click, fill, etc.) actually worked.
95
+ - `--annotate` (`-a`): Injects temporary overlay divs at each ref's bounding box, takes a screenshot with ref labels visible, then removes the overlays. Use `-o <path>` to control the output path.
96
+ - `--cursor-interactive` (`-C`): Scans for non-ARIA interactive elements (divs with `cursor:pointer`, `onclick`, `tabindex>=0`) using `page.evaluate`. Assigns `@c1`, `@c2`... refs with deterministic `nth-child` CSS selectors. These are elements the ARIA tree misses but users can still click.
97
+
98
+ ### Screenshot modes
99
+
100
+ The `screenshot` command supports four modes:
101
+
102
+ | Mode | Syntax | Playwright API |
103
+ |------|--------|----------------|
104
+ | Full page (default) | `screenshot [path]` | `page.screenshot({ fullPage: true })` |
105
+ | Viewport only | `screenshot --viewport [path]` | `page.screenshot({ fullPage: false })` |
106
+ | Element crop | `screenshot "#sel" [path]` or `screenshot @e3 [path]` | `locator.screenshot()` |
107
+ | Region clip | `screenshot --clip x,y,w,h [path]` | `page.screenshot({ clip })` |
108
+
109
+ Element crop accepts CSS selectors (`.class`, `#id`, `[attr]`) or `@e`/`@c` refs from `snapshot`. Auto-detection: `@e`/`@c` prefix = ref, `.`/`#`/`[` prefix = CSS selector, `--` prefix = flag, everything else = output path.
110
+
111
+ Mutual exclusion: `--clip` + selector and `--viewport` + `--clip` both throw errors. Unknown flags (e.g. `--bogus`) also throw.
112
+
113
+ ### Authentication
114
+
115
+ Each server session generates a random UUID as a bearer token. The token is written to the state file (`.gstack/browse.json`) with chmod 600. Every HTTP request must include `Authorization: Bearer <token>`. This prevents other processes on the machine from controlling the browser.
116
+
117
+ ### Console, network, and dialog capture
118
+
119
+ The server hooks into Playwright's `page.on('console')`, `page.on('response')`, and `page.on('dialog')` events. All entries are kept in O(1) circular buffers (50,000 capacity each) and flushed to disk asynchronously via `Bun.write()`:
120
+
121
+ - Console: `.gstack/browse-console.log`
122
+ - Network: `.gstack/browse-network.log`
123
+ - Dialog: `.gstack/browse-dialog.log`
124
+
125
+ The `console`, `network`, and `dialog` commands read from the in-memory buffers, not disk.
126
+
127
+ ### User handoff
128
+
129
+ When the headless browser can't proceed (CAPTCHA, MFA, complex auth), `handoff` opens a visible Chrome window at the exact same page with all cookies, localStorage, and tabs preserved. The user solves the problem manually, then `resume` returns control to the agent with a fresh snapshot.
130
+
131
+ ```bash
132
+ $B handoff "Stuck on CAPTCHA at login page" # opens visible Chrome
133
+ # User solves CAPTCHA...
134
+ $B resume # returns to headless with fresh snapshot
135
+ ```
136
+
137
+ The browser auto-suggests `handoff` after 3 consecutive failures. State is fully preserved across the switch — no re-login needed.
138
+
139
+ ### Dialog handling
140
+
141
+ Dialogs (alert, confirm, prompt) are auto-accepted by default to prevent browser lockup. The `dialog-accept` and `dialog-dismiss` commands control this behavior. For prompts, `dialog-accept <text>` provides the response text. All dialogs are logged to the dialog buffer with type, message, and action taken.
142
+
143
+ ### JavaScript execution (`js` and `eval`)
144
+
145
+ `js` runs a single expression, `eval` runs a JS file. Both support `await` — expressions containing `await` are automatically wrapped in an async context:
146
+
147
+ ```bash
148
+ $B js "await fetch('/api/data').then(r => r.json())" # works
149
+ $B js "document.title" # also works (no wrapping needed)
150
+ $B eval my-script.js # file with await works too
151
+ ```
152
+
153
+ For `eval` files, single-line files return the expression value directly. Multi-line files need explicit `return` when using `await`. Comments containing "await" don't trigger wrapping.
154
+
155
+ ### Multi-workspace support
156
+
157
+ Each workspace gets its own isolated browser instance with its own Chromium process, tabs, cookies, and logs. State is stored in `.gstack/` inside the project root (detected via `git rev-parse --show-toplevel`).
158
+
159
+ | Workspace | State file | Port |
160
+ |-----------|------------|------|
161
+ | `/code/project-a` | `/code/project-a/.gstack/browse.json` | random (10000-60000) |
162
+ | `/code/project-b` | `/code/project-b/.gstack/browse.json` | random (10000-60000) |
163
+
164
+ No port collisions. No shared state. Each project is fully isolated.
165
+
166
+ ### Environment variables
167
+
168
+ | Variable | Default | Description |
169
+ |----------|---------|-------------|
170
+ | `BROWSE_PORT` | 0 (random 10000-60000) | Fixed port for the HTTP server (debug override) |
171
+ | `BROWSE_IDLE_TIMEOUT` | 1800000 (30 min) | Idle shutdown timeout in ms |
172
+ | `BROWSE_STATE_FILE` | `.gstack/browse.json` | Path to state file (CLI passes to server) |
173
+ | `BROWSE_SERVER_SCRIPT` | auto-detected | Path to server.ts |
174
+
175
+ ### Performance
176
+
177
+ | Tool | First call | Subsequent calls | Context overhead per call |
178
+ |------|-----------|-----------------|--------------------------|
179
+ | Chrome MCP | ~5s | ~2-5s | ~2000 tokens (schema + protocol) |
180
+ | Playwright MCP | ~3s | ~1-3s | ~1500 tokens (schema + protocol) |
181
+ | **gstack browse** | **~3s** | **~100-200ms** | **0 tokens** (plain text stdout) |
182
+
183
+ The context overhead difference compounds fast. In a 20-command browser session, MCP tools burn 30,000-40,000 tokens on protocol framing alone. gstack burns zero.
184
+
185
+ ### Why CLI over MCP?
186
+
187
+ MCP (Model Context Protocol) works well for remote services, but for local browser automation it adds pure overhead:
188
+
189
+ - **Context bloat**: every MCP call includes full JSON schemas and protocol framing. A simple "get the page text" costs 10x more context tokens than it should.
190
+ - **Connection fragility**: persistent WebSocket/stdio connections drop and fail to reconnect.
191
+ - **Unnecessary abstraction**: Claude Code already has a Bash tool. A CLI that prints to stdout is the simplest possible interface.
192
+
193
+ gstack skips all of this. Compiled binary. Plain text in, plain text out. No protocol. No schema. No connection management.
194
+
195
+ ## Acknowledgments
196
+
197
+ The browser automation layer is built on [Playwright](https://playwright.dev/) by Microsoft. Playwright's accessibility tree API, locator system, and headless Chromium management are what make ref-based interaction possible. The snapshot system — assigning `@ref` labels to accessibility tree nodes and mapping them back to Playwright Locators — is built entirely on top of Playwright's primitives. Thank you to the Playwright team for building such a solid foundation.
198
+
199
+ ## Development
200
+
201
+ ### Prerequisites
202
+
203
+ - [Bun](https://bun.sh/) v1.0+
204
+ - Playwright's Chromium (installed automatically by `bun install`)
205
+
206
+ ### Quick start
207
+
208
+ ```bash
209
+ bun install # install dependencies + Playwright Chromium
210
+ bun test # run integration tests (~3s)
211
+ bun run dev <cmd> # run CLI from source (no compile)
212
+ bun run build # compile to browse/dist/browse
213
+ ```
214
+
215
+ ### Dev mode vs compiled binary
216
+
217
+ During development, use `bun run dev` instead of the compiled binary. It runs `browse/src/cli.ts` directly with Bun, so you get instant feedback without a compile step:
218
+
219
+ ```bash
220
+ bun run dev goto https://example.com
221
+ bun run dev text
222
+ bun run dev snapshot -i
223
+ bun run dev click @e3
224
+ ```
225
+
226
+ The compiled binary (`bun run build`) is only needed for distribution. It produces a single ~58MB executable at `browse/dist/browse` using Bun's `--compile` flag.
227
+
228
+ ### Running tests
229
+
230
+ ```bash
231
+ bun test # run all tests
232
+ bun test browse/test/commands # run command integration tests only
233
+ bun test browse/test/snapshot # run snapshot tests only
234
+ bun test browse/test/cookie-import-browser # run cookie import unit tests only
235
+ ```
236
+
237
+ Tests spin up a local HTTP server (`browse/test/test-server.ts`) serving HTML fixtures from `browse/test/fixtures/`, then exercise the CLI commands against those pages. 203 tests across 3 files, ~15 seconds total.
238
+
239
+ ### Source map
240
+
241
+ | File | Role |
242
+ |------|------|
243
+ | `browse/src/cli.ts` | Entry point. Reads `.gstack/browse.json`, sends HTTP to the server, prints response. |
244
+ | `browse/src/server.ts` | Bun HTTP server. Routes commands to the right handler. Manages idle timeout. |
245
+ | `browse/src/browser-manager.ts` | Chromium lifecycle — launch, tab management, ref map, crash detection. |
246
+ | `browse/src/snapshot.ts` | Parses accessibility tree, assigns `@e`/`@c` refs, builds Locator map. Handles `--diff`, `--annotate`, `-C`. |
247
+ | `browse/src/read-commands.ts` | Non-mutating commands: `text`, `html`, `links`, `js`, `css`, `is`, `dialog`, `forms`, etc. Exports `getCleanText()`. |
248
+ | `browse/src/write-commands.ts` | Mutating commands: `goto`, `click`, `fill`, `upload`, `dialog-accept`, `useragent` (with context recreation), etc. |
249
+ | `browse/src/meta-commands.ts` | Server management, chain routing, diff (DRY via `getCleanText`), snapshot delegation. |
250
+ | `browse/src/cookie-import-browser.ts` | Decrypt Chromium cookies via macOS Keychain + PBKDF2/AES-128-CBC. Auto-detects installed browsers. |
251
+ | `browse/src/cookie-picker-routes.ts` | HTTP routes for `/cookie-picker/*` — browser list, domain search, import, remove. |
252
+ | `browse/src/cookie-picker-ui.ts` | Self-contained HTML generator for the interactive cookie picker (dark theme, no frameworks). |
253
+ | `browse/src/buffers.ts` | `CircularBuffer<T>` (O(1) ring buffer) + console/network/dialog capture with async disk flush. |
254
+
255
+ ### Deploying to the active skill
256
+
257
+ The active skill lives at `~/.claude/skills/gstack/`. After making changes:
258
+
259
+ 1. Push your branch
260
+ 2. Pull in the skill directory: `cd ~/.claude/skills/gstack && git pull`
261
+ 3. Rebuild: `cd ~/.claude/skills/gstack && bun run build`
262
+
263
+ Or copy the binary directly: `cp browse/dist/browse ~/.claude/skills/gstack/browse/dist/browse`
264
+
265
+ ### Adding a new command
266
+
267
+ 1. Add the handler in `read-commands.ts` (non-mutating) or `write-commands.ts` (mutating)
268
+ 2. Register the route in `server.ts`
269
+ 3. Add a test case in `browse/test/commands.test.ts` with an HTML fixture if needed
270
+ 4. Run `bun test` to verify
271
+ 5. Run `bun run build` to compile