Skip to content

Commit e8893a1

Browse files
garrytanclaude
andauthored
v1.20.0.0 feat: browser-skills runtime + gbrain-support carryover (#1233)
* feat(gbrain-sync): queue primitives + writer shims Adds bin/gstack-brain-enqueue (atomic append to sync queue) and bin/gstack-jsonl-merge (git merge driver, ts-sort with SHA-256 fallback). Wires one backgrounded enqueue call into learnings-log, timeline-log, review-log, and developer-profile --migrate. question-log and question-preferences stay local per Codex v2 decision. gstack-config gains gbrain_sync_mode (off/artifacts-only/full) and gbrain_sync_mode_prompted keys, plus GSTACK_HOME env alignment so tests don't leak into real ~/.gstack/config.yaml. * feat(gbrain-sync): --once drain + secret scan + push bin/gstack-brain-sync is the core sync binary. Subcommands: --once (drain queue, allowlist-filter, privacy-class-filter, secret-scan staged diff, commit with template, push with fetch+merge retry), --status, --skip-file <path>, --drop-queue --yes, --discover-new (cursor-based detection of artifact writes that skip the shim). Secret regex families: AWS keys, GitHub tokens (ghp_/gho_/ghu_/ghs_/ ghr_/github_pat_), OpenAI sk-, PEM blocks, JWTs, bearer-token-in-JSON. On hit: unstage, preserve queue, print remediation hint (--skip-file or edit), exit clean. No daemon — invoked by preamble at skill boundaries. * feat(gbrain-sync): init, restore, uninstall, consumer registry bin/gstack-brain-init: idempotent first-run. git init ~/.gstack/, .gitignore=*, canonical .brain-allowlist + .brain-privacy-map.json, pre-commit secret-scan hook (defense-in-depth), merge driver registration via git config, gh repo create --private OR arbitrary --remote <url>, initial push, ~/.gstack-brain-remote.txt for new-machine discovery, GBrain consumer registration via HTTP POST. bin/gstack-brain-restore: safe new-machine bootstrap. Refuses clobber of existing allowlisted files, clones to staging, rsync-copies tracked files, re-registers merge drivers (required — not cloned from remote), rehydrates consumers.json, prompts for per-consumer tokens. bin/gstack-brain-uninstall: clean off-ramp. Removes .git + .brain-* files + consumers.json + config keys. Preserves user data (learnings, plans, retros, profile). Optional --delete-remote for GitHub repos. bin/gstack-brain-consumer + bin/gstack-brain-reader (symlink alias): registry management. Internal 'consumer' term; user-facing 'reader' per DX review decision. * feat(gbrain-sync): preamble block — privacy gate + boundary sync scripts/resolvers/preamble/generate-brain-sync-block.ts emits bash that runs at every skill invocation: - Detects ~/.gstack-brain-remote.txt on machines without local .git and surfaces a restore-available hint (does NOT auto-run restore). - Runs gstack-brain-sync --once at skill start to drain any pending writes (and at skill end via prose instruction). - Once-per-day auto-pull (cached via .brain-last-pull) for append-only JSONL files. - Emits BRAIN_SYNC: status line every skill run. Also emits prose for the host LLM to fire the one-time privacy stop-gate (full / artifacts-only / off) when gbrain is detected and gbrain_sync_mode_prompted is false. Wired into preamble.ts composition. * test(gbrain-sync): 27-test consolidated suite test/brain-sync.test.ts covers: - Config: validation, defaults, GSTACK_HOME env isolation - Enqueue: no-op gates, skip list, concurrent atomicity, JSON escape - JSONL merge driver: 3-way + ts-sort + SHA-256 fallback - Init + sync: canonical file creation, merge driver registration, push-reject + fetch+merge retry path - Init refuses different remote (idempotency) - Cross-machine restore round-trip (machine A write → machine B sees) - Secret scan across all 6 regex families (AWS, GH, OpenAI, PEM, JWT, bearer-JSON). --skip-file unblock remediation - Uninstall removes sync config, preserves user data - --discover-new idempotence via mtime+size cursor Behaviors verified via integration smokes during implementation. Known follow-up: bun-test 5s default timeout needs 30s wrapper for spawnSync-heavy tests. * docs(gbrain-sync): user guide + error lookup + README section docs/gbrain-sync.md: setup walkthrough, privacy modes, cross-machine workflow, secret protection, two-machine conflict handling, uninstall, troubleshooting reference. docs/gbrain-sync-errors.md: problem/cause/fix index for every user-visible error. Patterned on Rust's error docs + Stripe's API error reference. README.md: 'Cross-machine memory with GBrain sync' section near the top (discovery moment), plus docs-table entry. * chore: bump version and changelog (v1.7.0.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: regenerate SKILL.md files for gbrain-sync preamble block Re-runs bun run gen:skill-docs after adding generateBrainSyncBlock to scripts/resolvers/preamble.ts in a2aa8a0. CI check-freshness caught the drift. All 36 SKILL.md files regenerated with the new skill-start bash block + privacy-gate prose + skill-end sync instructions baked in. * fix(test): session-awareness reads AskUserQuestion Format from a Tier 2+ SKILL.md The test was reading ROOT/SKILL.md (browse skill, Tier 1) which never contained '## AskUserQuestion Format' — that section is only emitted for Tier 2+ skills by scripts/resolvers/preamble.ts. As a result the agent was prompted with an empty format guide and only emitted 'RECOMMENDATION' intermittently, making the test flaky. Pre-existing on main (same ROOT/SKILL.md shape there) — surfaced now because the agent run didn't hit the RECOMMENDATION/recommend/option a fallback strings in this particular attempt. Fix: read from office-hours/SKILL.md (Tier 3, always has the section) with a fallback that scans for the first top-level skill dir whose SKILL.md contains the header. Future template moves won't break this test again. * feat(browse): domain-skills storage + state machine New module browse/src/domain-skills.ts implements the per-site notes the agent writes for itself, persisted as type:"domain" rows alongside /learn's per-project learnings. Three scopes layered: per-project default, global by explicit promotion. Project-active shadows global for the same host. State machine (T6 — codex outside-voice): quarantined --3 uses w/o flag--> active(project) --promote--> global ^ | +----- classifier flag during use - Append-only JSONL with O_APPEND for atomic small writes - Tolerant parser drops partial trailing line on read - Tombstone for deletes (compactor cleans up later) - Version log per (host, scope) enables rollback - Hostname derived from active tab top-level origin (T3 confused-deputy fix) - writeSkill rejects classifier_score >= 0.85 with structured error Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browse): domain-skills storage + state machine 14 tests covering: - T3 hostname normalization (lowercase, www. strip, port/path/query strip, subdomain-exact preserved) - T4 scope shadowing (per-project active shadows global for same host) - T5 persistence (version monotonicity, tolerant parser drops partial line) - T6 state machine (quarantined → active after N=3 uses, classifier-flag blocks promotion, save-time score >= 0.85 rejected) - Rollback by version log (restore prior body, advance version counter) - Tombstone deletion (read returns null after delete) All 14 pass in 27ms via bun test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): $B domain-skill subcommands Wire the domain-skills storage layer into the browse CLI as a META command: $B domain-skill save save body from stdin or --from-file (host derived from active tab — T3) $B domain-skill list list all skills visible to current project $B domain-skill show <host> print skill body $B domain-skill edit <host> open in $EDITOR $B domain-skill promote-to-global <host> cross-project promotion (T4) $B domain-skill rollback <host> [--global] restore prior version $B domain-skill rm <host> [--global] tombstone Save path runs L1-L3 content filters from content-security.ts (importable in compiled binary, unlike L4 ML classifier — see CLAUDE.md). The L4 classifier scan happens in sidebar-agent at prompt-injection load time. Output is structured (problem + cause + suggested-action) per DX D7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): $B cdp escape hatch — deny-default allowlist + two-tier mutex Codex T2: flip CDP posture to deny-default. Allowed methods enumerated in cdp-allowlist.ts with (scope: tab|browser, output: trusted|untrusted, justification) per entry. Initial allowlist (~25 methods) covers: - Accessibility tree extraction (read-only) - DOM/CSS inspection (read-only) - Performance metrics - Tracing - Emulation viewport/UA override - Page screenshot/PDF capture (output is binary, no marker injection vector) - Network.enable/disable (no bodies/cookies — those are exfil surfaces) - Runtime.getProperties (NO evaluate/callFunctionOn — those would be RCE) Page.navigate is INTENTIONALLY NOT allowed; agents use $B goto which goes through the URL blocklist. Codex T7: two-tier mutex. tab-scoped methods take per-tab lock; browser- scoped take global lock that blocks all tab locks. 5s acquire timeout yields CDPMutexAcquireTimeout (no silent hangs). All lock acquires use try/finally so errors don't leak the lock. Path A from spike: uses Playwright's newCDPSession() per page. No second WebSocket, no need for --remote-debugging-port. CDPSession is cached per page in a WeakMap and cleared on page close. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browse): CDP allowlist + two-tier mutex 13 tests: - Allowlist linter: every entry has 4 required fields, no duplicates, justification length > 20 chars - Deny-list verification: dangerous methods (Runtime.evaluate, Page.navigate, Network.getResponseBody, Browser.close, Target.attachToTarget, etc.) are NOT allowed (Codex T2 categories 4-7) - Per-tab mutex serializes ops on same tab - Per-tab mutex allows parallel ops across different tabs - Global lock blocks tab locks; tab locks block global lock - Acquire timeout yields CDPMutexAcquireTimeout (no silent hang) - Timeout error names the tab id and the timeout budget Also extends Network.disable justification to satisfy linter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): telemetry signals + project-slug helper Lightweight telemetry per DX D9: piggybacks on ~/.gstack/analytics/ pattern. Hostname + aggregate counters only, no body content. GSTACK_TELEMETRY_OFF=1 silences. Fire-and-forget — never blocks calling path. Signals fired so far: - domain_skill_saved {host, scope, state, bytes} - domain_skill_save_blocked {host, reason} (domain_skill_fired and cdp_method_* fired in subsequent commits.) Also extracts project-slug resolution into project-slug.ts so server.ts and domain-skill-commands.ts share one cached lookup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): sidebar prompt-context injection + CDP telemetry server.ts spawnClaude now: - Imports per-project domain skill matching the active tab's hostname via readDomainSkill() - Wraps the body in UNTRUSTED EXTERNAL CONTENT envelope (so the L4 classifier in sidebar-agent sees it at load time per Eng D4) - Appends as <domain-skill source="..." host="..." version="..."> block - Fires domain_skill_fired telemetry (host, source, version) - Calls recordSkillUse fire-and-forget so the auto-promote-after-N=3 state machine advances on each successful prompt injection System prompt also gets a one-liner introducing $B domain-skill commands to agents (DX D4 start-of-task discoverability hint). cdp-bridge.ts fires: - cdp_method_denied (drives next allow-list growth) - cdp_method_lock_acquire_ms (P50/P99 quantile observability) - cdp_method_called (allowed methods) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browse): telemetry module 3 tests covering: - logTelemetry writes JSONL with ts injected - GSTACK_TELEMETRY_OFF=1 silences all events - logTelemetry never throws on disk failures Uses GSTACK_HOME env var to redirect writes to a tmp dir; the telemetry module reads HOME lazily so test mutations take effect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: domain-skills reference + error lookup table docs/domain-skills.md mirrors the layered shape of docs/gbrain-sync.md (DX D8): how agents use it, state machine, storage layout, security model (L1-L3 + L4 layered defense), error reference table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(readme): browser-harness-js plug + domain-skills section New "Domain skills + raw CDP escape hatch" section under "The sprint" covering both v1.8.0.0 features. Plugs browser-use/browser-harness-js as the no-rails alternative for users who want raw CDP without gstack's security stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.8.0.0) Branch-scoped bump on top of merged 1.7.0.0 base. CHANGELOG entry covers the full v1.8.0.0 scope: $B domain-skill, $B cdp escape hatch, two-tier mutex, telemetry signals, sidebar prompt-context injection. Includes Codex outside-voice trail (7 of 20 findings resolved, 12 mooted by T1 scope drop). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * todos: 7 follow-ups from v1.8.0.0 review trail P1: Self-authoring $B commands with out-of-process worker isolation (Codex T1 deferred from v1.8.0.0 — needs real isolation design) P2: Migrate /learn to SQLite (Codex T5 long-term primitive fix) P2: Remove plan-mode handshake from /plan-devex-review (skill bug) P3: GBrain skillpack publishing for domain-skills P3: Replay/record demonstrated flows to domain-skills P3: $B commands review batch-mode UX (alternative to inline approval) P3: Heuristic command-gap watcher (DX D4 alternative C) Each entry has the standard What/Why/Pros/Cons/Context/Effort/Priority/ Depends-on shape so anyone picking these up later has full context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): lazy GSTACK_HOME resolution in domain-skills Module-level constants (GLOBAL_FILE, derived path) were evaluated at module-load and cached. When E2E and unit tests run in the same Bun test pass and set GSTACK_HOME differently, the second test sees the first test's path. Switch to lazy gstackHome() / globalFile() / projectFile() helpers so process.env mutations take effect. Mirrors the pattern already used in telemetry.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browse): E2E gate-tier tests for domain-skills + CDP domain-skills-e2e.test.ts (4 tests): - save derives host from active tab top-level origin (T3) - save lands quarantined; list surfaces it - readSkill returns null until 3 uses without flag promote to active (T6) - save without an active page errors with structured guidance cdp-e2e.test.ts (8 tests): - Accessibility.getFullAXTree returns wrapped JSON (allowed, untrusted-output) - Performance.getMetrics returns plain JSON (allowed, trusted-output) - Runtime.evaluate DENIED with structured guidance (T2 RCE block) - Page.navigate DENIED (must use $B goto for blocklist routing) - Network.getResponseBody DENIED (exfil block) - malformed JSON params surfaces clear error - non Domain.method format surfaces clear error - $B cdp help returns help text Both files boot a real Chromium via BrowserManager.launch() and exercise the dispatch handlers end-to-end. Total 12 E2E tests in <2s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: regenerate SKILL.md files with new $B commands bun run gen:skill-docs picks up the domain-skill and cdp META_COMMANDS entries added in commands.ts. Both top-level SKILL.md and browse/SKILL.md now list the new commands in their Meta and Inspection tables. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(fixtures): regenerate ship SKILL.md golden baselines for v1.7.0.0 Pre-existing failures inherited from garrytan/gbrain-support: the GBrain Sync preamble block (added in v1.7.0.0) appears in regenerated SKILL.md output but the golden baselines in test/fixtures/golden/ were never updated. Three failures fixed: golden-file regression > Claude ship skill matches golden baseline golden-file regression > Codex ship skill matches golden baseline golden-file regression > Factory ship skill matches golden baseline Goldens regenerated by copying the current ship/SKILL.md, codex .agents/skills/gstack-ship/SKILL.md, and .factory/skills/gstack-ship/SKILL.md files. Diff is the v1.7.0.0 GBrain Sync preamble block + privacy stop-gate (no behavioral changes — just preamble text). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(brain-sync): bearer-token regex catches values with leading space Pre-existing bug from v1.7.0.0: the bearer-token-json secret pattern required values matching [A-Za-z0-9_./+=-]{16,}, which rejected the "Bearer <token>" form because the literal space after "Bearer" wasn't in the character class. Real Authorization headers use "Bearer <token>" syntax, and the test fixture '"authorization":"Bearer abcdef1234567890abcdef1234567890"' sat unscanned despite being a leak-class secret. One-character fix: add space to the value character class. Test 'gstack-brain-sync secret scan > blocks bearer-json' now passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(brain-sync): GSTACK_HOME isolation test compares mtime, not content Pre-existing flaky test: the GSTACK_HOME-overrides-real-config test asserted the real ~/.gstack/config.yaml does NOT contain "gbrain_sync_mode: full" after the test. That fails for any user whose real config legitimately has that key set from prior usage — the test's invariant is "the command did not modify the real file," not "the real file lacks any specific value." Switch to mtime + content snapshot: capture both BEFORE running the command, then verify both are unchanged after. Also add a positive assertion that the tmpHome config DID get the new key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(skill-validation): exempt deliberate large fixtures from 2MB limit Pre-existing failure: the "git tracks no files larger than 2MB" test caught browse/test/fixtures/security-bench-haiku-responses.json (28.8MB of replay data committed in v1.6.4.0 for security benchmark gate tests). The test exists to catch accidentally-committed binaries (Mach-O dist binaries, etc), not to forbid all large files. Add an explicit LARGE_FIXTURE_EXEMPTIONS allowlist so deliberate replay fixtures pass the gate while accidental binaries still fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skill-token): mint scoped tokens per skill spawn Wraps token-registry.createToken/revokeToken with skill-specific clientId encoding (skill:<name>:<spawn-id>) and read+write defaults. Skill scripts get a per-spawn capability token bound to browser-driving commands; the daemon root token never leaves the harness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse-client): SDK for browser-skill scripts Thin wrapper over POST /command with bearer auth. Resolves daemon port + token from GSTACK_PORT + GSTACK_SKILL_TOKEN env vars first (set by $B skill run when spawning), falls back to .gstack/browse.json for standalone debug runs. Convenience methods cover the read+write surface skills typically need: goto, click, fill, text, html, snapshot, links, forms, accessibility, attrs, media, data, scroll, press, type, select, wait, hover, screenshot. Low-level command(cmd, args) escape hatch for anything else. This is the canonical SDK source. Each browser-skill ships a sibling copy at <skill>/_lib/browse-client.ts so each skill is fully portable and version-pinned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browser-skills): 3-tier storage helpers listBrowserSkills() walks project > global > bundled (first-wins), parses SKILL.md frontmatter, no INDEX.json. readBrowserSkill() does the same for a single name. tombstoneBrowserSkill() moves a skill into .tombstones/<name>-<ts>/ for recoverability. Frontmatter parser handles the subset browser-skills need: scalars (host, description, trusted, version, source), string lists (triggers), and arg-mapping lists ([{name, description}, ...]). Quoted values handle colons; trusted defaults to false. Bundled tier path is auto-detected from the binary install location; project tier comes from git rev-parse; global is ~/.gstack/. All tier paths are overridable for hermetic tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browser-skills): \$B skill list/show/run/test/rm subcommands handleSkillCommand dispatches to per-subcommand handlers; spawnSkill is the load-bearing function that: 1. Mints a per-spawn scoped token (read+write only) bound to the skill name + spawn-id. 2. Builds the spawn env: - trusted: passes process.env minus GSTACK_TOKEN (defense in depth). - untrusted: minimal allowlist (LANG, LC_ALL, TERM, TZ) + locked PATH; explicitly drops anything matching TOKEN/KEY/SECRET/etc. Also drops AWS_/AZURE_/GCP_/GOOGLE_APPLICATION_/ANTHROPIC_/OPENAI_/ GITHUB_/GH_/SSH_/GPG_/NPM_TOKEN/PYPI_ patterns. 3. Always injects GSTACK_PORT + GSTACK_SKILL_TOKEN last (cannot be overridden by parent env). 4. Spawns bun run script.ts -- <args> with cwd=skillDir, captures stdout (1MB cap), stderr, and timeout-kills past the deadline. 5. Revokes the token in finally{}, always. list output prints the resolved tier inline so "why did it run that one?" never becomes a debugging mystery (Codex finding #4 mitigation). server.ts threads the listen port to meta-commands via MetaCommandOpts.daemonPort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browser-skills): bundled hackernews-frontpage reference skill Smallest interesting browser-skill: scrapes HN front page, returns 30 stories as JSON. No auth, stable HTML, fully fixture-tested. Files: SKILL.md frontmatter + prose script.ts exports parseStoriesFromHtml(html) main: goto + html + parse + JSON.stringify _lib/browse-client.ts vendored copy of the SDK fixtures/hn-2026-04-26.html captured front page (5 stories) script.test.ts 13 assertions against the fixture The parser is a pure function over HTML so script.test.ts runs without a daemon (just imports parseStoriesFromHtml and asserts). This exercises every Phase 1 component end-to-end: - browse-client SDK (script imports browse from ./_lib/) - 3-tier lookup (hackernews-frontpage lives in the bundled tier) - scoped tokens (read+write is enough for goto + html) - spawn lifecycle (\$B skill run hackernews-frontpage) - file-fixture testing (\$B skill test hackernews-frontpage) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(skill-validation): cover bundled browser-skills Adds 7 assertions per bundled skill at <root>/browser-skills/<name>/: - SKILL.md exists - frontmatter parses with required fields (name/host/triggers/args) - script.ts exists - _lib/browse-client.ts exists and matches the canonical SDK byte-for-byte - script.test.ts exists - script.ts imports browse from ./_lib/browse-client The byte-identical SDK check enforces the version-pinning contract: when the canonical SDK at browse/src/browse-client.ts changes, every bundled skill's _lib/ copy must be re-synced or this test fails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(designs): add BROWSER_SKILLS_V1 design doc Captures the 13 locked decisions, two-axis trust model (daemon-side scoped tokens + process-side env access), 3-tier lookup, file layout, and full responses to all 8 Codex outside-voice findings. Includes Phase 2-4 sketches for future branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(todos): replace self-authoring-\$B P1 with browser-skills phases Phase 1 of the browser-skills design shipped on this branch (sidesteps the in-daemon isolation problem the original P1 was blocked on). The new entries enumerate the work that remains: P1: Phase 2 (/scrape + /automate skill templates) P2: Phase 3 (resolver injection at session start) P2: Phase 4 (eval infra + fixture staleness + OS sandbox) Cross-references docs/designs/BROWSER_SKILLS_V1.md for the full architecture and the 8 Codex review findings + responses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.9.0.0 — browser-skills runtime VERSION 1.8.0.0 → 1.9.0.0. CHANGELOG entry leads with what humans can do today (hand-write deterministic browser scripts, run them in 200ms via \$B skill run). Notes explicitly that agent authoring lands in next release; no fabricated perf numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browser-skills-e2e): exercise dispatch with bundled hackernews-frontpage Covers the full \$B skill list/show/test pipeline against the real bundled reference skill (defaultTierPaths picks up <repo>/browser-skills/). Verifies frontmatter shape, the three-tier walk surfaces the bundled entry, and \$B skill test successfully runs the bundled script.test.ts in a child bun process. \$B skill run end-to-end against the live network is intentionally NOT covered here (would be flaky against news.ycombinator.com); the spawn lifecycle is exercised in browser-skill-commands.test.ts using inline synthetic skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: regen SKILL.md to surface the skill META command bun run gen:skill-docs picked up the new \`skill\` command from COMMAND_DESCRIPTIONS in browse/src/commands.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: bump v1.9.0.0 → v1.13.0.0 Main shipped through v1.11.1.0 while this branch was in flight; v1.12.x is presumed claimed by another in-flight branch. Use v1.13.0.0 as the next available slot. Updated VERSION, package.json, and the CHANGELOG header. Entry body unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: bump v1.13.0.0 → v1.16.0.0 Main shipped v1.13.0.0 (claude outside-voice skill), v1.14.0.0 (sidebar REPL), and v1.15.0.0 (slim preamble + plan-mode E2E) while this branch was in flight. Use v1.16.0.0 as the next available slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse-skills): atomic write helper for /skillify (D3) stageSkill writes a candidate skill into ~/.gstack/.tmp/skillify-<spawnId>/ with restrictive perms. commitSkill does an atomic fs.renameSync into the final tier path with realpath/lstat discipline (refuses symlinked staging dirs, refuses to clobber existing skills). discardStaged is the cleanup path for test failures and approval rejections, idempotent and bounded to the per-spawn wrapper. validateSkillName enforces lowercase/digits/ dashes only, no path-escape characters. Implements the D3 contract from the v1.19.0.0 plan review: never a half-written skill on disk. Test fail or approval reject = rm -rf the temp dir, no tombstone for never-approved skills. Closes Codex finding #5 (atomic skill packaging) for Phase 2a. 34 unit assertions covering: stage validation, file-path escape rejection, permission check, atomic rename, clobber refusal, symlink refusal, project tier unresolved, idempotent discard, end-to-end happy + simulated test failure + approval reject paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(scrape): /scrape <intent> skill template One entry point for pulling page data. Three paths under the hood: 1. Match — agent reads $B skill list, semantically matches the user's intent against each skill's triggers + description + host. Confident match = $B skill run <name> in ~200ms. 2. Prototype — no match, drive the page with $B goto/text/html/links etc. Return JSON, append a one-line "say /skillify" nudge. 3. Mutating refusal — verbs like submit/click/fill route to /automate (Phase 2b P0); /scrape is read-only by contract. Match decision lives in the agent, not the daemon. No new code in browse/src/, no expanded daemon command surface, no new prompt-injection blast radius. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skillify): /skillify codifies last /scrape into permanent skill The productivity multiplier. /scrape discovers the flow; /skillify writes it as deterministic Playwright-via-browse-client code so the next /scrape on the same intent runs in ~200ms. 11-step flow with three locked contracts from the v1.19.0.0 plan review: D1 — Provenance guard. Walk back ≤10 agent turns for a clearly-bounded /scrape result. Refuse with one specific message if cold. No silent synthesis from chat fragments. D2 — Synthesis input slice. Extract ONLY the final-attempt $B calls that produced the JSON the user accepted, plus the user's intent string. Drop failed selectors, drop unrelated chat, drop earlier-session content. Closes Codex finding #6 by picking option (b) from the design doc: re-prompt from agent's own context, not a structured recorder. D3 — Atomic write. Stage to ~/.gstack/.tmp/skillify-<spawnId>/, run $B skill test against the temp dir, only rename into the final tier path on test pass + user approval. Test fail or approval reject = rm -rf the temp dir entirely. Default tier: global (~/.gstack/browser-skills/<name>/). --project flag overrides to per-project. Generated test must include at least one ★★ assertion (parsed JSON has expected shape + non-empty key fields), not a smoke ★ assertion. Bun runtime distribution (Codex finding #7) carries over to Phase 4. Documented in the skill's Limits section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(browser-skills): gate-tier E2E for /scrape + /skillify (D4) Five scenarios cover the productivity loop and the contracts locked during the v1.19.0.0 plan review: scrape-match-path — intent matching bundled hackernews-frontpage routes via $B skill run, no prototype phase scrape-prototype-path — no matching skill, drives $B against a local file:// fixture, returns JSON, suggests /skillify skillify-happy-path — /scrape then /skillify; skill written to ~/.gstack/browser-skills/<name>/ with the full file tree; SKILL.md prose body must not contain conversation fragments (D2) skillify-provenance-refusal — cold /skillify with no prior /scrape refuses with the D1 message; nothing on disk (D1) skillify-approval-reject — /scrape then /skillify but reject in the approval gate; temp dir is removed, nothing at the final tier path (D3) All five gate-tier (~$0.50-$1.50 each, ~$5 total per CI run). Set EVALS=1 to enable. Uses local file:// fixtures so prototype + skillify scenarios run deterministically without network. Touchfiles registers all 5 entries with proper deps on scrape/**, skillify/**, browse/src/browser-skill-write.ts, and the Phase 1 runtime modules. The match-path test depends on the bundled hackernews-frontpage skill so its touchfile includes browser-skills/hackernews-frontpage/**. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(browser-skills): TODOS Phase 2a + design doc D1-D4 decisions TODOS.md: - Narrows existing P1 (was "/scrape and /automate") to "/scrape and /skillify" — the /scrape + /skillify wedge ships in this branch. Codex finding #6 (synthesis) removed from Cons (resolved by D2); finding #7 (Bun runtime) stays as the open carry-over. - Adds new ## P0 above PACING_UPDATES_V0 for the /automate follow-up. Same skillify pattern as /scrape, different trust profile (per-step confirmation gate when running non-codified). Reuses /skillify and the D3 helper as-is. Effort M. BROWSER_SKILLS_V1.md: - Phase table re-organized into 1, 2a, 2b, 3, 4. Phase 1 + Phase 2a consolidate into v1.19.0.0 ship (the v1.16.0.0 branch-internal bump never landed on main). - New "Phase 2a" sub-section captures the four decisions locked during /plan-eng-review: D1 — provenance guard (≤10 turn walk-back, refuse if cold) D2 — synthesis input slice (final-attempt $B calls only, closes Codex finding #6) D3 — atomic write discipline (temp-dir-then-rename via new browse/src/browser-skill-write.ts helper) D4 — full test scope (5 gate E2E + 1 unit + smoke) - New "Phase 2b" sketch for /automate: same skillify machinery, per-mutating-step confirmation gate, deferred to next branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.16.0.0 -> v1.19.0.0 — browser-skills Phase 1 + 2a Consolidates the v1.16.0.0 branch-internal bump (Phase 1 runtime, never landed on main) with Phase 2a (/scrape + /skillify + atomic-write helper) into one v1.19.0.0 ship per CLAUDE.md "Never orphan branch-internal versions" rule. Headline: Browser-skills land end-to-end. /scrape <intent> first call drives the page; second call runs the codified script in 200ms. The unified CHANGELOG entry covers: - Phase 1 runtime: $B skill list/show/run/test/rm, scoped tokens, 3-tier storage, bundled hackernews-frontpage reference. - Phase 2a: /scrape + /skillify gstack skills, browser-skill-write.ts atomic helper, 5 gate-tier E2E + 34 unit assertions. Numbers table updated: 5 new modules (+browser-skill-write), 2 new gstack skills, 6 of 8 Codex outside-voice findings resolved (synthesis #6 closed by D2; Bun runtime #7 + OS sandbox #1 stay deferred to Phase 4). /automate (Phase 2b) is split out as P0 in TODOS for the next branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(commands): tighten descriptions for LLM-judge baseline pinning The skill-llm-eval test "baseline score pinning" failed CI on three retry attempts: judge gave command_reference.actionability=3, baseline demands ≥4. Judge cited 8 specific gaps in COMMAND_DESCRIPTIONS. This commit closes 7 of 8 by tightening the descriptions: - press: documents that key names are case-sensitive Playwright keys, shows modifier syntax (Shift+Enter, Control+A), links the full key list. Removes the "is this case-sensitive?" guesswork. - is: documents that <sel> accepts either a CSS selector OR an @ref token from a prior snapshot, and that property values are case- sensitive. - scroll: documents that there is no --by/--to amount option, points at `js window.scrollTo(0, N)` for pixel-precise scrolling. - js / eval: clarifies that both run in the same JS sandbox, the difference is just inline expr (js) vs file (eval). - storage: clarifies sessionStorage is read-only via this command, points at `js sessionStorage.setItem(...)` for the write path. - chain: walks through how to invoke (pipe a JSON array of arrays to $B chain), confirms it stops at the first error. - cdp: explains how to discover allowed methods (read cdp-allowlist.ts) + shows a concrete example invocation. - domain-skill: explains that the "classifier flag" is set automatically by the L4 prompt-injection scan (agents do not set it manually); enumerates the full lifecycle verbs. The 8th gap (storage set syntax conflict) is also resolved as part of the storage rewrite. Two pipe-character bugs caught by the existing `no command description contains pipe character` guard at `test/gen-skill-docs.test.ts:595`: the chain example originally used `echo '[...]' | $B chain` (literal pipe) and the cdp description used `tab|browser` / `trusted|untrusted` (also literal pipes). Both rewritten to keep markdown table cells intact. Verification: 696/0 pass on skill-validation + gen-skill-docs after regen across all hosts. The CI llm-judge eval will re-run against the new SKILL.md and should hit actionability ≥4 reliably. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(browser): rewrite BROWSER.md as complete reference Full rewrite covering the gstack browser surface as of v1.19.0.0. Up from 488 to 1,299 lines, 26 top-level sections. Adds previously-undocumented subsystems: - The productivity loop: /scrape + /skillify with D1 (provenance guard), D2 (final-attempt-only synthesis), D3 (atomic-write discipline) contracts. - Browser-skills runtime: anatomy, three-tier storage, scoped tokens, trust model (capability + env axes), sibling SDK distribution, atomic-write helper, bundled hackernews-frontpage reference. - Domain-skills: per-site agent notes with quarantined → active → global state machine and the L4-classifier auto-promotion gate. - Pair-agent: dual-listener architecture, 26-command tunnel allowlist, canDispatchOverTunnel pure gate, three token types (root, setup key, scoped), denial log path + salt model. - Security stack L1-L6: layer table, thresholds (BLOCK/WARN/LOG_ONLY/ SOLO_CONTENT_BLOCK), ensemble rule, classifier model paths, env knobs. - Side Panel deep dive: Terminal pane (Claude PTY) as the primary surface with Activity/Refs/Inspector as debug overlays, WS auth via Sec-WebSocket-Protocol, gstackInjectToTerminal cross-pane plumbing. - CDP escape hatch: $B cdp deny-default allowlist, $B inspect CSS inspector, $B ux-audit page structure extraction. - Meta commands previously undocumented: tabs/frames/state/watch/inbox/ tab-each, with usage and storage paths. - Authentication: three token types with lifetimes, SSE session cookie, PTY session cookie, token registry behavior. - Full source map: 30+ file inventory of browse/src/ vs the old 11-file list. Preserves from before: architecture diagram, daemon lifecycle, snapshot ref staleness, screenshot modes, goto file:// vs load-html semantics, batch endpoint, JS await wrapping, env vars, performance numbers vs MCP, Playwright acknowledgments, dev guide. Cross-links to ARCHITECTURE.md, CLAUDE.md, docs/REMOTE_BROWSER_ACCESS.md, docs/designs/BROWSER_SKILLS_V1.md, scrape/SKILL.md, skillify/SKILL.md, TODOS.md so anyone landing on BROWSER.md can navigate to the load-bearing companion docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server): tab-ownership gate keys on tabPolicy, not isWrite Browser-skill spawns hit `403: Tab not owned by your agent` on every first run because the gate at server.ts:639 fired for any non-root write, regardless of the token's tabPolicy. The bundled hackernews-frontpage reference skill failed identically. Every /skillify-generated skill failed identically. The user's natural tabs have no claimed owner — by design — so any skill driving them via `goto` (a write) was 403'd. The intent in skill-token.ts:79 was always correct: `tabPolicy: 'shared'` with the comment "skill scripts may switch tabs as needed." The enforcement just ignored it. Two surgical changes: browser-manager.ts:checkTabAccess — gate now keys on options.ownOnly only. Shared-policy tokens (skill spawns, default scoped clients) get permissive access — root-equivalent for the tab gate. Own-only tokens (pair-agent over the ngrok tunnel) still require ownership for every read and write. isWrite stays in the signature for callers that want to log or branch elsewhere; it no longer gates the decision. server.ts:639 — gate predicate narrowed from (WRITE_COMMANDS.has(command) || tokenInfo.tabPolicy === 'own-only') to just tokenInfo.tabPolicy === 'own-only' The 'newtab' exemption stays. Shared tokens skip the gate entirely; own-only tokens still hit it. Comment block above the gate updated to document the new predicate intent. Pair-agent isolation is intact. Tunnel tokens still default to tabPolicy: 'own-only', still must `newtab` first to get a tab they can drive, still can't dispatch any of the 23 commands outside the tunnel allowlist. The capability gate (scope checks) and rate limits already constrain what local scoped clients can do; tab ownership was never a security boundary for them — only for pair-agent. This release makes the enforcement match the original design intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(server): lock the shared-vs-own-only tab gate contract The pre-fix tests at tab-isolation.test.ts:43,57 encoded the broken behavior as the contract — they specifically asserted "scoped agent cannot write to unowned tab," which was the exact failure mode that broke browser-skills. They passed because they tested the wrong invariant. This commit replaces those tests with explicit shared-vs-own-only coverage that documents what each policy actually means: - Shared scoped agents (skill spawns, default scoped clients) can read AND write any tab — unowned, their own, or another agent's. The capability is gated by scope checks + rate limits, not by tab ownership. - Own-only scoped agents (pair-agent over tunnel) cannot read OR write any tab they don't own. Pre-fix this case was conflated with shared writes; now it's explicit. 9 unit assertions on checkTabAccess, up from 6. Each test names the policy axis it's covering so a future refactor can't quietly flip the contract. Adds source-shape regression test 10a in server-auth.test.ts: "tab gate predicate is own-only-scoped, not write-scoped." The gate's `if (...)` line MUST contain `tabPolicy === 'own-only'` and MUST NOT contain `WRITE_COMMANDS.has(command) ||`. If a future refactor re-introduces the write-scoped gate, this fails immediately in free-tier `bun test`. Updates the marker for the existing newtab-excluded test to match the new comment block ("Tab ownership check (own-only tokens / pair-agent isolation)"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v1.19.0.0 -> v1.20.0.0 — fix tab-ownership footgun Patch release on top of v1.19.0.0. The shipping headline of v1.19.0.0 (/scrape + /skillify productivity loop) was broken on first run in any session where the daemon already had a tab. Bundled hackernews-frontpage failed identically. Every /skillify-generated skill failed identically. The fix narrows the tab-ownership gate from "any non-root write" to "tabPolicy === 'own-only' only." Pair-agent isolation (the v1.6.0.0 threat model) is intact; local skill spawns get their original behavior back. VERSION: 1.19.0.0 -> 1.20.0.0 package.json version: synced. CHANGELOG entry leads with the user-visible impact: the productivity loop works again, no half-second-stalls of confused 403s. Includes before/after metrics on the bundled reference skill and the broken- contract pre-fix tests that hid the regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(claude): sharpen CHANGELOG rule — diff between main and ship Codifies what was already implicit in the existing "Never orphan branch-internal versions" + "Only document what shipped between main and this change" sections, but with sharper language and concrete NEVER examples. The rule: a CHANGELOG entry is the diff between main and the shipping branch — what users get when they upgrade. NOT how the branch got there. Branch-internal version bumps, mid-branch bug fixes, plan review outcomes, and patch narratives all belong in PR descriptions and commit messages, not in CHANGELOG. Adds explicit examples of phrasing to NEVER use: - "v1.X had a bug that v1.Y fixes" (mentions a branch-internal version) - "The shipping headline of v1.X was broken because..." (apologizes for never-released state) - "Pre-fix tests encoded the broken behavior" (contributor's victory lap, not user benefit) - "Two surgical edits, both in the dispatch path" (micro-narrative of the patch) The constructive replacement: describe the released system as a property, not as a fix. "Browser-skills run end-to-end with the expected tab-access semantics." If a property is worth calling out, document it in the trust-model section, not as a "we fixed X" callout. Pairs with feedback_no_shame_changelog and feedback_changelog_harden_against_critics memories — entries should read as a flex even to a hostile screenshotter, never admit prior breakage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changelog): consolidate v1.20.0.0 as the diff vs main Rewrites the v1.20.0.0 entry to describe what users get when they upgrade from main (v1.17.0.0) to this release: browser-skills end-to-end. Drops all branch-internal narrative — Phase 1 / Phase 2a labels, the v1.8.0.0 P1 history paragraph, the test-counts-by-phase split, and the patch micro-narrative for the tab-policy semantics. The previously-separate v1.19.0.0 entry (a branch-internal version that never landed on main) collapses into v1.20.0.0 per the "Never orphan branch-internal versions" rule. Tab-access policies are now documented as a property of the trust model: `'shared'` (skill spawns) is permissive, `'own-only'` (pair-agent over the tunnel) is strict. No "fix" framing, no mention of an intermediate state where it was broken. Adds the BROWSER.md rewrite and the new tab-isolation + server-auth source-shape regression tests to the itemized changes. The reverse-chronological order remains: v1.20.0.0 → v1.17.0.0 → v1.16.0.0 → v1.15.0.0 → ... Gaps (v1.18, v1.19) are fine — those were branch-internal version numbers that never landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 675717e commit e8893a1

53 files changed

Lines changed: 10625 additions & 340 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BROWSER.md

Lines changed: 1106 additions & 294 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,106 @@
11
# Changelog
22

3+
## [1.20.0.0] - 2026-04-28
4+
5+
## **Browser-skills land. `/scrape <intent>` first call drives the page; second call runs the codified script in 200ms.**
6+
7+
Browser-skills are deterministic Playwright scripts that run as standalone Bun processes via `$B skill run`. They live in three storage tiers (project > global > bundled), get a per-spawn scoped capability token, and ship with `_lib/browse-client.ts` so each skill is fully self-contained. The bundled reference is `hackernews-frontpage` — try `$B skill run hackernews-frontpage` and you get the HN front page as JSON in 200ms.
8+
9+
The agent authors them. `/scrape <intent>` is the single entry point for pulling page data — it matches existing skills via the `triggers:` array on first call, or drives `$B goto`/`$B html`/etc. on a brand-new intent and returns JSON. After a successful prototype, `/skillify` codifies the flow: it walks back through the conversation, extracts the final-attempt `$B` calls (no failed selectors, no chat fragments), synthesizes `script.ts` + `script.test.ts` + a captured fixture, stages everything to `~/.gstack/.tmp/skillify-<spawnId>/`, runs the test there, and asks before renaming into the final tier path. Test failure or rejection: `rm -rf` the temp dir, no half-written skill ever appears in `$B skill list`. Next `/scrape` with a matching intent routes via `$B skill list` + `$B skill run <name>`. ~30s prototype becomes ~200ms forever after.
10+
11+
Mutating-flow sibling `/automate` is tracked as P0 in `TODOS.md` for the next release. Scraping is the safer wedge to validate the skillify pattern (failure mode: wrong data); mutating actions need the per-step confirmation gate that `/automate` adds on top.
12+
13+
The architecture sidesteps the in-daemon isolation problem by running skill scripts *outside* the daemon as standalone Bun processes. Each script gets a per-spawn scoped capability token bound to the read+write command surface; the daemon root token never leaves the harness. Two token policies share the same registry but enforce independently: `tabPolicy: 'shared'` (default for skill spawns) is permissive on tab access — a skill can drive any tab, gated only by scope checks and rate limits. `tabPolicy: 'own-only'` (pair-agent over the ngrok tunnel) is strict — the token can only access tabs it owns, must `newtab` first to get a tab to drive, can't reach the user's natural tabs. Trust boundaries are at the daemon, not in process-side env scrubbing.
14+
15+
### What you can now do
16+
17+
- **Run a bundled skill:** `$B skill run hackernews-frontpage` returns JSON.
18+
- **Scrape with one verb:** `/scrape latest hacker news stories`. First call matches the bundled skill via the `triggers:` array and runs in 200ms. New intent? It prototypes via `$B`, returns JSON, and suggests `/skillify`.
19+
- **Codify a prototype:** `/skillify` walks back through the conversation, finds the last `/scrape` result, synthesizes the script + test + fixture, stages to a temp dir, runs the test, and asks before committing to `~/.gstack/browser-skills/<name>/`.
20+
- **List what's available:** `$B skill list` walks three tiers (project > global > bundled) and prints the resolved tier inline.
21+
- **Test a skill against a fixture:** `$B skill test hackernews-frontpage` runs the bundled `script.test.ts` against a captured HTML snapshot, no live network.
22+
- **Read a skill's contract:** `$B skill show hackernews-frontpage` prints SKILL.md.
23+
- **Tombstone a user-tier skill:** `$B skill rm <name> [--global]` moves it to `.tombstones/<name>-<ts>/`. Bundled skills are read-only.
24+
25+
### The numbers that matter
26+
27+
Source: 155 unit assertions across `browse/test/{skill-token,browse-client,browser-skills-storage,browser-skill-commands,browser-skill-write,tab-isolation,server-auth}.test.ts`, `browser-skills/hackernews-frontpage/script.test.ts`, and `test/skill-validation.test.ts`. Plus 5 gate-tier E2E scenarios in `test/skill-e2e-skillify.test.ts`. All free-tier tests pass in under two seconds; the gate-tier E2E adds ~$5 to a CI run.
28+
29+
| Surface | Shape |
30+
|---|---|
31+
| Latency on a codified intent | ~200ms (vs ~30s prototype on first call) |
32+
| New `$B` command | `skill` (5 subcommands: list, show, run, test, rm) |
33+
| New gstack skills | 2 (`/scrape`, `/skillify`); `/automate` tracked as P0 in TODOS |
34+
| New modules | 5 (`browse-client.ts`, `browser-skills.ts`, `browser-skill-commands.ts`, `skill-token.ts`, `browser-skill-write.ts`) |
35+
| Bundled reference skills | 1 (`hackernews-frontpage`) |
36+
| Storage tiers | 3 (project > global > bundled, first-wins) |
37+
| SDK distribution model | sibling-file: each skill ships `_lib/browse-client.ts` (~3KB, byte-identical to canonical) |
38+
| Daemon-side capability default | scoped session token, `read+write` only (no `eval`/`js`/`cookies`/`storage`) |
39+
| Process-side env default | scrubbed: drops $HOME, $PATH user-paths, anything matching TOKEN/KEY/SECRET, AWS_*, OPENAI_*, GITHUB_*, etc. |
40+
| Tab access policy | `'shared'` (skill spawns) = permissive, gated by scope only. `'own-only'` (pair-agent tunnel) = strict ownership for every read + write. |
41+
| Atomic-write contract | temp-dir-then-rename via `browse/src/browser-skill-write.ts`. Test fail OR approval reject = `rm -rf` the temp dir. Never a half-written skill on disk. |
42+
43+
### What this means for builders
44+
45+
The compounding loop is closed. The first time you ask the agent to scrape a page, it pays the prototype cost. The second time on the same intent (rephrased or not), it runs the codified script in 200ms. Multiply across every recurring data-pull task you have, release-notes scraping, leaderboard checks, dashboard captures, and the time savings compound across sessions.
46+
47+
The agent-authoring contract is tight: `/skillify` extracts only the final-attempt `$B` calls from the conversation (no failed selectors, no chat fragments leak into the on-disk artifact), writes to a temp dir, runs the auto-generated `script.test.ts` there, and only commits on test pass + your approval. If anything fails, the temp dir vanishes, no broken skill ever appears in `$B skill list`.
48+
49+
Mutating flows (form fills, click sequences, multi-step automations) ship next as `/automate` (P0 in `TODOS.md`). Same skillify machinery, different trust profile: per-mutating-step confirmation gate when running non-codified, unattended once committed. Scraping's failure mode is benign (wrong data) and mutation's isn't (unintended writes); the staged rollout validates the skillify pattern with the safer half first.
50+
51+
Pair-agent operators get the same isolation guarantees they had before. The dual-listener tunnel architecture is intact: a remote agent over ngrok can't read or write tabs the local user is using. Tunnel tokens get `tabPolicy: 'own-only'`, must `newtab` first to drive a tab, and only the 26-command tunnel allowlist is reachable.
52+
53+
### Itemized changes
54+
55+
#### Added — `$B skill` runtime
56+
57+
- `$B skill list|show|run|test|rm <name?>`. Five subcommands. List walks 3 tiers (project > global > bundled) and prints the resolved tier inline so "why did it run that one?" is never a debugging mystery. Run mints a per-spawn scoped capability token, spawns `bun run script.ts -- <args>` with cwd locked to the skill dir, captures stdout (1MB cap) and stderr, and revokes the token on exit.
58+
- `browse/src/browse-client.ts`. Canonical SDK (~250 LOC). Reads `GSTACK_PORT` + `GSTACK_SKILL_TOKEN` from env first (set by `$B skill run`), falls back to `<project>/.gstack/browse.json` for standalone debug runs. Convenience methods cover the read+write surface: goto, click, fill, text, html, snapshot, links, forms, accessibility, attrs, media, data, scroll, press, type, select, wait, hover, screenshot. Low-level `command(cmd, args)` escape hatch for anything else.
59+
- `browse/src/browser-skills.ts`. Three-tier storage helpers. `listBrowserSkills()` walks project > global > bundled (first-wins), parses SKILL.md frontmatter, no INDEX.json. `readBrowserSkill(name)` does the same for a single name. `tombstoneBrowserSkill(name, tier)` moves a skill into `.tombstones/<name>-<ts>/` for recoverability.
60+
- `browse/src/skill-token.ts`. Wraps `token-registry.createToken/revokeToken` with skill-specific clientId encoding (`skill:<name>:<spawn-id>`), read+write defaults, and `tabPolicy: 'shared'`. TTL = spawn timeout + 30s slack.
61+
- `browser-skills/hackernews-frontpage/`. Bundled reference skill (SKILL.md, script.ts, _lib/browse-client.ts, fixtures/hn-2026-04-26.html, script.test.ts). Smallest interesting browser-skill: scrapes HN front page, returns 30 stories as JSON, no auth, stable HTML.
62+
63+
#### Added — `/scrape` + `/skillify` gstack skills
64+
65+
- `scrape/SKILL.md.tmpl` + generated `scrape/SKILL.md`. `/scrape <intent>` is one entry point with three paths: match (intent matches an existing skill's `triggers:``$B skill run <name>` in 200ms), prototype (drive `$B` primitives, return JSON, suggest `/skillify`), refusal (mutating intents route to `/automate`). Match decision lives in the agent, not the daemon, no new code in `browse/src/`, no expanded daemon command surface.
66+
- `skillify/SKILL.md.tmpl` + generated `skillify/SKILL.md`. 11-step flow: provenance guard (walk back ≤10 turns for a bounded `/scrape` result, refuse if cold), name + tier + trigger proposal via `AskUserQuestion`, synthesize `script.ts` from final-attempt `$B` calls only, capture fixture, write `script.test.ts`, copy canonical SDK byte-identical to `_lib/browse-client.ts`, write SKILL.md frontmatter (`source: agent`, `trusted: false`), stage to temp dir, run `$B skill test`, approval gate, atomic rename to final tier path.
67+
- `browse/src/browser-skill-write.ts`. Atomic-write helper. `stageSkill()` writes files to `~/.gstack/.tmp/skillify-<spawnId>/<name>/` with restrictive perms. `commitSkill()` does an atomic `fs.renameSync` into the final tier path with `realpath`/`lstat` discipline (refuses to follow symlinked staging dirs, refuses to clobber existing skills). `discardStaged()` is the cleanup path for test failures and approval rejections. `rm -rf` is idempotent and bounded to the per-spawn wrapper. `validateSkillName()` enforces lowercase letters/digits/dashes only, no `..` or path-escape characters.
68+
69+
#### Trust model — scoped tokens
70+
71+
Every spawned skill gets its own scoped token. The shape:
72+
73+
- **Capability scope.** Read + write only by default. No `eval`, `js`, `cookies`, `storage`. Single-use clientId encodes skill name + spawn id. Revoked when the spawn exits or times out (TTL = timeout + 30s slack).
74+
- **Process env.** `trusted: true` frontmatter passes `process.env` minus `GSTACK_TOKEN`. `trusted: false` (default) drops everything except a minimal allowlist (LANG, LC_ALL, TERM, TZ) and pattern-strips secrets (TOKEN/KEY/SECRET/PASSWORD/AWS_*/ANTHROPIC_*/OPENAI_*/GITHUB_*).
75+
- **Tab access policy.** `tabPolicy: 'shared'` (skill spawns, default scoped clients): permissive, can read or write any tab, gated only by scope checks + rate limits. `tabPolicy: 'own-only'` (pair-agent over the tunnel): strict, the token can only access tabs it owns. The two policies enforce independently in `browser-manager.ts:checkTabAccess`. The capability gate already constrains what shared tokens can do; tab ownership only matters for pair-agent isolation.
76+
77+
#### Changed
78+
79+
- `browse/src/commands.ts` registers `skill` as a META command.
80+
- `browse/src/server.ts` threads the local listen port (`LOCAL_LISTEN_PORT`) to meta-command dispatch so `$B skill run` knows which port to point spawned scripts at. The tab-ownership gate predicate at the dispatcher fires for `tabPolicy === 'own-only'` only; shared tokens skip it.
81+
- `browse/src/browser-manager.ts:checkTabAccess` keys on `options.ownOnly`. Shared tokens and root pass unconditionally; own-only tokens require ownership for every read and write.
82+
- `browse/src/meta-commands.ts` dispatches `skill` to `handleSkillCommand`.
83+
- `BROWSER.md` rewritten to a complete reference: 1,299 lines, 26 sections covering the productivity loop, browser-skills runtime, domain-skills, pair-agent dual-listener, sidebar agent + terminal PTY, security stack L1-L6, full source map.
84+
- `docs/designs/BROWSER_SKILLS_V1.md` adds the design for the productivity loop's four contracts (provenance guard, synthesis input slice, atomic write, full test coverage). Phase table organized into 1, 2a, 2b, 3, 4.
85+
- `TODOS.md` lists `/automate` as P0 above the existing `PACING_UPDATES_V0` entry.
86+
87+
#### Tests
88+
89+
- `browse/test/browser-skill-write.test.ts` — 34 assertions covering the atomic-write contract: stage validation, file-path escape rejection, atomic rename, clobber refusal, symlink refusal, idempotent discard, end-to-end happy + failure paths.
90+
- `browse/test/tab-isolation.test.ts` — 9 assertions on `checkTabAccess` with explicit shared-vs-own-only coverage: shared agents can read/write any tab; own-only agents can only access their own claimed tabs.
91+
- `browse/test/server-auth.test.ts` — source-shape regression that fails if a future refactor reintroduces `WRITE_COMMANDS.has(command) ||` into the tab-ownership gate predicate.
92+
- `test/skill-validation.test.ts` extends to cover bundled browser-skills: each must have SKILL.md + script.ts + _lib/browse-client.ts (byte-identical to canonical) + script.test.ts, with frontmatter satisfying the host/triggers/args contract.
93+
- `test/skill-e2e-skillify.test.ts` — 5 gate-tier E2E scenarios (`claude -p` driven, deterministic against local file:// fixtures): match path routes to bundled skill, prototype path drives `$B` and emits JSON, skillify happy writes complete skill tree, provenance refusal leaves nothing on disk, approval-gate reject removes the temp dir.
94+
- `test/helpers/touchfiles.ts` registers all 5 new E2E entries with deps on `scrape/**`, `skillify/**`, `browse/src/browser-skill-write.ts`, plus the runtime modules.
95+
96+
#### For contributors
97+
98+
- The browser-skill SKILL.md frontmatter has a hard contract enforced by `parseSkillFile()` and `test/skill-validation.test.ts`. Required: `host` (string), `triggers` (string list), `args` (mapping list). Optional: `trusted` (bool, defaults false), `version`, `source` (`human`/`agent`), `description`.
99+
- The canonical SDK at `browse/src/browse-client.ts` and the sibling at `browser-skills/hackernews-frontpage/_lib/browse-client.ts` MUST be byte-identical. The skill-validation test fails the build otherwise. When the canonical SDK changes, update every bundled skill's `_lib/` copy. Agent-authored skills via `/skillify` get a freshly-copied SDK at synthesis time, so they're frozen at the version they were authored against (no drift possible).
100+
- The atomic-write helper enforces "no half-written skills." Always call `stageSkill` → run tests → `commitSkill` (success) OR `discardStaged` (failure). Never write directly to the final tier path. The helper's `validateSkillName` is the only naming gate, keep it tight (lowercase letters/digits/dashes, ≤64 chars, no consecutive dashes, no leading digit).
101+
- `checkTabAccess` policy: `ownOnly` is the only signal that constrains access. `isWrite` stays in the signature for callers that want to log or branch elsewhere, but doesn't gate the decision. Adding new policy axes (e.g., per-skill tab quotas) belongs in `docs/designs/`, not as a sneaky `isWrite` overload.
102+
- `/automate` and the Phase 4 follow-ups (Bun runtime distribution, OS FS sandbox, fixture-staleness detection) are tracked in `docs/designs/BROWSER_SKILLS_V1.md` and `TODOS.md`. The `/automate` skill reuses `/skillify` and `browser-skill-write.ts` as-is; new code is the per-mutating-step confirmation gate.
103+
3104
## [1.17.0.0] - 2026-04-26
4105

5106
## **Your gstack memory now actually lives in gbrain.**

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,31 @@ MINOR again on top (e.g., main at v1.14.0.0, your branch lands v1.15.0.0).
489489
own version bump and CHANGELOG entry. The entry describes what THIS branch adds —
490490
not what was already on main.
491491

492+
**The CHANGELOG entry is the diff between main and the shipping branch — what users
493+
get when they upgrade. NOT how the branch got there.** A reader landing on the entry
494+
should learn what they can do now that they couldn't before; they should not learn
495+
about the branch's internal version bumps, the bugs we caught and fixed mid-branch,
496+
the plan reviews we ran, or the commits we squashed. That is branch development
497+
narrative. It belongs in PR descriptions and commit messages, not CHANGELOG.
498+
499+
**Never reference branch-internal versions in a CHANGELOG entry.** If your branch
500+
bumped VERSION from v1.5.0.0 → v1.5.1.0 → v1.6.0.0 during development and only the
501+
final v1.6.0.0 ships to main, the entry must read as if v1.5.1.0 never existed.
502+
Concretely, NEVER write:
503+
- "v1.5.1.0 had a bug that v1.6.0.0 fixes" — readers don't know about v1.5.1.0; it's
504+
a branch-internal artifact.
505+
- "The shipping headline of v1.5.1.0 was broken because..." — same reason. From main's
506+
perspective, v1.5.1.0 was never released.
507+
- "Pre-fix tests encoded the broken behavior" — that's a contributor's victory lap,
508+
not a user benefit.
509+
- "Two surgical edits, both in the dispatch path" — micro-narrative of the patch.
510+
511+
Instead, describe the released system: "Browser-skills run end-to-end with the
512+
expected tab-access semantics." If a property of the shipped system is worth calling
513+
out (e.g., "skill spawns get permissive tab access; pair-agent tunnel tokens require
514+
ownership"), document it as a property, not as a fix. The shipped system is what
515+
the user gets; the path to that system is invisible to them.
516+
492517
**When to write the CHANGELOG entry:**
493518
- At `/ship` time (Step 13), not during development or mid-branch.
494519
- The entry covers ALL commits on this branch vs the base branch.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,15 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
241241

242242
Set `gstack-config set checkpoint_mode continuous` and skills auto-commit your work as you go with a `WIP:` prefix plus a structured `[gstack-context]` body (decisions, remaining work, failed approaches). Survives crashes and context switches. `/context-restore` reads those commits to reconstruct session state. `/ship` filter-squashes WIP commits before the PR (preserving non-WIP commits) so bisect stays clean. Push is opt-in via `checkpoint_push=true` — default is local-only so you don't trigger CI on every WIP commit.
243243

244+
### Domain skills + raw CDP escape hatch
245+
246+
Two new browser primitives compound the gstack agent over time:
247+
248+
- **`$B domain-skill save`** — agent saves a per-site note (e.g., "LinkedIn's Apply button lives in an iframe") that fires automatically next time it visits that hostname. Quarantined → active after 3 successful uses → optional cross-project promotion via `$B domain-skill promote-to-global`. Storage lives alongside `/learn`'s per-project learnings file. Full reference: **[docs/domain-skills.md](docs/domain-skills.md)**.
249+
- **`$B cdp <Domain.method>`** — raw Chrome DevTools Protocol escape hatch for the rare case curated commands miss. Deny-default: methods must be explicitly added to `browse/src/cdp-allowlist.ts` with a one-line justification. Two-tier mutex serializes browser-scoped CDP calls against per-tab work. Output for data-exfil methods is wrapped in the UNTRUSTED envelope.
250+
251+
> Want raw CDP with no rails, no allowlist, no daemon — just thin transport from agent to Chrome? [browser-use/browser-harness-js](https://github.com/browser-use/browser-harness-js) is a different philosophy (agent-authored helpers vs gstack's curated commands) and a good fit if you don't want gstack's security stack. The two can coexist: gstack's `$B cdp` and harness can both attach to the same Chrome via Playwright's `newCDPSession`.
252+
244253
**[Deep dives with examples and philosophy for every skill →](docs/skills.md)**
245254

246255
### Karpathy's four failure modes? Already covered.

0 commit comments

Comments
 (0)